diff --git a/.changeset/famous-cows-hug.md b/.changeset/famous-cows-hug.md new file mode 100644 index 0000000000..3ebfa271e2 --- /dev/null +++ b/.changeset/famous-cows-hug.md @@ -0,0 +1,5 @@ +--- +'@adyen/adyen-web': patch +--- + +Fix accessibility issue with opening klarna widget with enter button diff --git a/packages/lib/src/components/Donation/components/FixedAmounts.tsx b/packages/lib/src/components/Donation/components/FixedAmounts.tsx index ae86fc757f..1c3e44ec2f 100644 --- a/packages/lib/src/components/Donation/components/FixedAmounts.tsx +++ b/packages/lib/src/components/Donation/components/FixedAmounts.tsx @@ -11,7 +11,7 @@ interface FixedAmountsProps { values: Array; status: Status; onAmountSelected: ({ target }) => void; - onDonateButtonClicked: (amount: number) => void; + onDonateButtonClicked: () => void; } export default function FixedAmounts(props: FixedAmountsProps) { const { currency, values, selectedAmount, status, onAmountSelected, onDonateButtonClicked } = props; diff --git a/packages/lib/src/components/Klarna/KlarnaPayments.tsx b/packages/lib/src/components/Klarna/KlarnaPayments.tsx index b8ddfbc495..4f05a5895a 100644 --- a/packages/lib/src/components/Klarna/KlarnaPayments.tsx +++ b/packages/lib/src/components/Klarna/KlarnaPayments.tsx @@ -6,6 +6,7 @@ import { KlarnaContainer } from './components/KlarnaContainer/KlarnaContainer'; import { TxVariants } from '../tx-variants'; import type { KlarnaAction, KlarnaAdditionalDetailsData, KlarnaComponentRef, KlarnaConfiguration } from './types'; import type { ICore } from '../../core/types'; +import { PayButtonFunctionProps } from '../internal/UIElement/types'; class KlarnaPayments extends UIElement { public static type = TxVariants.klarna; @@ -38,7 +39,7 @@ class KlarnaPayments extends UIElement { }; } - public payButton = props => { + public payButton = (props: PayButtonFunctionProps) => { return ; }; diff --git a/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.test.tsx b/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.test.tsx index b6fb7b04b8..ad269aff59 100644 --- a/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.test.tsx +++ b/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.test.tsx @@ -1,5 +1,6 @@ import { h } from 'preact'; -import { fireEvent, render, screen } from '@testing-library/preact'; +import { render, screen } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; import { KlarnaWidget } from './KlarnaWidget'; import Script from '../../../../utils/Script'; import { KLARNA_WIDGET_URL } from '../../constants'; @@ -7,6 +8,7 @@ import { KlarnaWidgetAuthorizeResponse, type KlarnaWidgetProps } from '../../typ import { CoreProvider } from '../../../../core/Context/CoreProvider'; import { mock } from 'jest-mock-extended'; import { AnalyticsModule } from '../../../../types/global-types'; +import { PayButtonFunctionProps } from '../../../internal/UIElement/types'; jest.mock('../../../../utils/Script', () => { return jest.fn().mockImplementation(() => { @@ -42,7 +44,7 @@ describe('KlarnaWidget', () => { const paymentData = 'test'; const paymentMethodType = 'klarna'; const sdkData = { client_token: '123', payment_method_category: 'paynow' }; - const payButton = props => ( + const payButton = (props: PayButtonFunctionProps) => ( @@ -115,11 +117,12 @@ describe('KlarnaWidget', () => { describe('Pay with Klarna widget', () => { test('should call the onComplete if the payment is authorized', async () => { + const user = userEvent.setup(); const authRes = { approved: true, show_form: true, authorization_token: 'abc' }; klarnaObj.Payments.load = getKlarnaActionImp({ show_form: true }); klarnaObj.Payments.authorize = getKlarnaActionImp(authRes); customRender(props); - fireEvent.click(await screen.findByTestId(/pay-with-klarna/i)); + await user.click(await screen.findByTestId(/pay-with-klarna/i)); expect(onComplete).toHaveBeenCalledWith({ data: { paymentData, @@ -129,20 +132,65 @@ describe('KlarnaWidget', () => { } }); }); + + test('should call the onComplete if the payment is authorized after pay button is triggered with {Enter} key', async () => { + const user = userEvent.setup(); + const authRes = { approved: true, show_form: true, authorization_token: 'abc' }; + klarnaObj.Payments.load = getKlarnaActionImp({ show_form: true }); + klarnaObj.Payments.authorize = getKlarnaActionImp(authRes); + customRender(props); + + const payButton = await screen.findByTestId(/pay-with-klarna/i); + payButton.focus(); + await user.keyboard('{Enter}'); + + expect(onComplete).toHaveBeenCalledWith({ + data: { + paymentData, + details: { + authorization_token: authRes.authorization_token + } + } + }); + }); + + test('should call the onComplete if the payment is authorized after pay button is triggered with {Space} key', async () => { + const user = userEvent.setup(); + const authRes = { approved: true, show_form: true, authorization_token: 'abc' }; + klarnaObj.Payments.load = getKlarnaActionImp({ show_form: true }); + klarnaObj.Payments.authorize = getKlarnaActionImp(authRes); + customRender(props); + + const payButton = await screen.findByTestId(/pay-with-klarna/i); + payButton.focus(); + await user.keyboard('{Space}'); + + expect(onComplete).toHaveBeenCalledWith({ + data: { + paymentData, + details: { + authorization_token: authRes.authorization_token + } + } + }); + }); + test('should call the onError if the payment is not authorized temporarily', async () => { + const user = userEvent.setup(); klarnaObj.Payments.load = getKlarnaActionImp({ show_form: true }); const authRes = { approved: false, show_form: true }; klarnaObj.Payments.authorize = getKlarnaActionImp(authRes); customRender(props); - fireEvent.click(await screen.findByTestId(/pay-with-klarna/i)); + await user.click(await screen.findByTestId(/pay-with-klarna/i)); expect(onError).toHaveBeenCalledWith(authRes); }); test('should call the onComplete if the payment is not authorized permanently', async () => { + const user = userEvent.setup(); klarnaObj.Payments.load = getKlarnaActionImp({ show_form: true }); klarnaObj.Payments.authorize = getKlarnaActionImp({ show_form: false }); customRender(props); - fireEvent.click(await screen.findByTestId(/pay-with-klarna/i)); + await user.click(await screen.findByTestId(/pay-with-klarna/i)); expect(onComplete).toHaveBeenCalledWith({ data: { paymentData, diff --git a/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.tsx b/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.tsx index 2642a0e675..4f5919326c 100644 --- a/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.tsx +++ b/packages/lib/src/components/Klarna/components/KlarnaWidget/KlarnaWidget.tsx @@ -32,7 +32,7 @@ export function KlarnaWidget({ sdkData, paymentMethodType, widgetInitializationT container: klarnaWidgetRef.current, payment_method_category: sdkData.payment_method_category }, - function (res) { + function (res: { show_form: boolean; error: unknown }) { // If show_form: true is received together with an error, something fixable is wrong and the consumer // needs to take action before moving forward // If show_form: false, the payment method in the loaded widget will not be offered for this order @@ -81,6 +81,19 @@ export function KlarnaWidget({ sdkData, paymentMethodType, widgetInitializationT } }, [sdkData.payment_method_category, props.onComplete, props.onError]); + /** + * TODO: Clean this up when we have a different solution for handling on click in the BaseElement class + * We need this specifically for handling ENTER keypresses from the keyboard + * because the UIElement class has an on keypress handler which can trigger a components submit function + * ENTER key press on this button should not trigger this behaviour since the Klarna script has already been loaded + */ + const handleKeyDown = (e: h.JSX.TargetedKeyboardEvent) => { + if (e.key === 'Enter' || e.code === 'Enter') { + e.preventDefault(); + authorizeKlarna(); + } + }; + /** * Initializes Klarna SDK if it is already available and reinitialize * it when the init time refreshes @@ -113,7 +126,12 @@ export function KlarnaWidget({ sdkData, paymentMethodType, widgetInitializationT return (
- {payButton({ status, disabled: status === 'loading', onClick: authorizeKlarna })} + {payButton({ + status, + disabled: status === 'loading', + onClick: authorizeKlarna, + onKeyDown: handleKeyDown + })}
); } diff --git a/packages/lib/src/components/internal/Button/Button.test.tsx b/packages/lib/src/components/internal/Button/Button.test.tsx index 252697ecad..0a35ff836b 100644 --- a/packages/lib/src/components/internal/Button/Button.test.tsx +++ b/packages/lib/src/components/internal/Button/Button.test.tsx @@ -2,8 +2,9 @@ import { h } from 'preact'; import { mount } from 'enzyme'; import Button from './Button'; import { CoreProvider } from '../../../core/Context/CoreProvider'; +import { ButtonProps } from './types'; -const getWrapper = props => { +const getWrapper = (props: ButtonProps) => { return mount(