Skip to content

Commit

Permalink
Merge pull request #330 from City-of-Helsinki/fix/HP-2307-a11y-servic…
Browse files Browse the repository at this point in the history
…e-connections-close-focus

fix: a11y service connections fix element focus on closing accordion HP-2307
  • Loading branch information
mikkojamG committed Apr 16, 2024
2 parents 08e43c7 + 33ff50a commit a6cc25a
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 12 deletions.
72 changes: 60 additions & 12 deletions src/common/expandingPanel/ExpandingPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import {
Button,
Expand Down Expand Up @@ -35,23 +36,20 @@ function ExpandingPanel({
dataTestId,
onChange,
}: Props): React.ReactElement {
const container = useRef<HTMLDivElement | null>(null);
const [beforeCloseButtonClick, setBeforeCloseButtonClick] = useState(false);
const [hasMounted, setHasMounted] = useState(false);
const { t } = useTranslation();

const handleContainerRef = (ref: HTMLDivElement) => {
// If ref is not saved yet we are about in the first render.
// In that case we can scroll this element into view.
if (!container.current && scrollIntoViewOnMount && ref) {
ref.scrollIntoView();
}

container.current = ref;
};

const { isOpen, buttonProps, contentProps, openAccordion } = useAccordion({
initiallyOpen,
});

const container = useRef<HTMLDivElement | null>(null);
const titleButtonRef = useRef<HTMLButtonElement | null>(null);

useEffect(() => {
setHasMounted(true);
}, []);

useLayoutEffect(() => {
if (initiallyOpen && isOpen !== initiallyOpen) {
openAccordion();
Expand All @@ -63,13 +61,53 @@ function ExpandingPanel({
onChange(isOpen);
}
}, [isOpen, onChange]);

useEffect(() => {
if (!hasMounted) {
return undefined;
}

const timer = setTimeout(() => {
if (titleButtonRef.current) {
titleButtonRef.current.focus();
}

if (beforeCloseButtonClick === true) {
setBeforeCloseButtonClick(false);
buttonProps.onClick();
}
}, 50);

// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [beforeCloseButtonClick]);

const handleContainerRef = (ref: HTMLDivElement) => {
// If ref is not saved yet we are about in the first render.
// In that case we can scroll this element into view.
if (!container.current && scrollIntoViewOnMount && ref) {
ref.scrollIntoView();
}

container.current = ref;
};

const onCloseButtonActivate = () => {
setBeforeCloseButtonClick(true);
};

const Icon = isOpen ? IconAngleUp : IconAngleDown;
const buttonText = isOpen
? t('expandingPanel.hideInformation')
: t('expandingPanel.showInformation');
const buttonTestId = dataTestId
? { 'data-testid': `${dataTestId}-toggle-button` }
: null;
const secondaryButtonTestId = dataTestId
? { 'data-testid': `${dataTestId}-secondary-toggle-button` }
: null;

return (
<div
className={classNames(commonStyles['content-box'], styles['container'])}
Expand All @@ -79,6 +117,7 @@ function ExpandingPanel({
<h2>{title}</h2>
<div className={styles['right-side-information']}>
<Button
ref={titleButtonRef}
title={title}
variant={'supplementary'}
iconRight={<Icon aria-hidden />}
Expand All @@ -103,6 +142,15 @@ function ExpandingPanel({
variant={'supplementary'}
iconRight={<Icon aria-hidden />}
{...buttonProps}
{...secondaryButtonTestId}
onKeyDown={event => {
if (event.key === 'Enter') {
onCloseButtonActivate();
}
}}
onClick={() => {
onCloseButtonActivate();
}}
>
{t('expandingPanel.closeButtonText')}
</Button>
Expand Down
16 changes: 16 additions & 0 deletions src/common/test/testingLibraryTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export type TestTools = RenderResult & {
) => Promise<string | undefined>;
fetch: () => Promise<void>;
clickElement: (selector: ElementSelector) => Promise<HTMLElement | null>;
keydownEnterElement: (
selector: ElementSelector
) => Promise<HTMLElement | null>;
submit: (props?: {
waitForOnSaveNotification?: WaitForElementAndValueProps;
waitForAfterSaveNotification?: WaitForElementAndValueProps;
Expand Down Expand Up @@ -241,6 +244,18 @@ export const renderComponentWithMocksAndContexts = async (
return Promise.resolve(button);
};

const keydownEnterElement: TestTools['keydownEnterElement'] = async selector => {
const button = getElement(selector);

await waitFor(() => {
fireEvent.keyDown(button as Element, {
key: 'Enter',
charCode: 13,
});
});
return Promise.resolve(button);
};

const isDisabled: TestTools['isDisabled'] = element =>
!!element && element.getAttribute('disabled') !== null;

Expand Down Expand Up @@ -349,6 +364,7 @@ export const renderComponentWithMocksAndContexts = async (
getTextOrInputValue,
fetch,
clickElement,
keydownEnterElement,
submit,
isDisabled,
setInputValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function ServiceConnection({
title={service.title || ''}
showInformationText
initiallyOpen={isActive}
dataTestId={encodedServiceName}
>
<p>{service.description}</p>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ describe('<ServiceConnection /> ', () => {
querySelector: `button[title="${service.title}"]`,
});

const getSecondaryExpandableSelector = (
service: ServiceConnectionData
): ElementSelector => ({
testId: `${encodeServiceName(service)}-secondary-toggle-button`,
});

const TestingComponent = ({
service,
isActive,
Expand Down Expand Up @@ -134,6 +140,58 @@ describe('<ServiceConnection /> ', () => {
);
});
});
it(`Clicking secondary close button should close accordion and focus on primary button`, async () => {
await act(async () => {
const { clickElement, waitForElement, getElement } = await initTests(
defaultServiceConnectionData,
true
);

await waitForElement(
getServiceInformationSelector(defaultServiceConnectionData)
);
await waitForElement(
getSecondaryExpandableSelector(defaultServiceConnectionData)
);

await clickElement(
getSecondaryExpandableSelector(defaultServiceConnectionData)
);

await waitFor(async () => {
expect(
getElement(getExpandableSelector(defaultServiceConnectionData))
).toHaveFocus();
});
});
});

it(`Keydown on secondary close button should close accordion and focus on primary button`, async () => {
await act(async () => {
const {
keydownEnterElement,
waitForElement,
getElement,
} = await initTests(defaultServiceConnectionData, true);

await waitForElement(
getServiceInformationSelector(defaultServiceConnectionData)
);
await waitForElement(
getSecondaryExpandableSelector(defaultServiceConnectionData)
);

await keydownEnterElement(
getSecondaryExpandableSelector(defaultServiceConnectionData)
);

await waitFor(async () => {
expect(
getElement(getExpandableSelector(defaultServiceConnectionData))
).toHaveFocus();
});
});
});
it(`Clicking the remove button calls the onDeletion() with service data`, async () => {
await act(async () => {
const { clickElement } = await initTests();
Expand Down

0 comments on commit a6cc25a

Please sign in to comment.