Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: accordion: deep accessibility updates #823

2 changes: 1 addition & 1 deletion src/components/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ Custom.args = {
className="octuple-content"
style={{ color: 'var(--grey-tertiary-color)', fontWeight: 400 }}
>
Supporting text
<span>Supporting text</span>
</div>
</Stack>
<Stack
Expand Down
19 changes: 13 additions & 6 deletions src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,15 @@ describe('Accordion', () => {
const { container } = render(
<Accordion {...accordionProps} size={AccordionSize.Large} />
);
const summary = container.querySelector('.accordion-summary');
fireEvent.click(summary);
const summaryClickableArea = container.querySelector('.clickable-area');
fireEvent.click(summaryClickableArea);
await waitFor(() =>
expect(
container.getElementsByClassName('accordion-summary-expanded')
).toHaveLength(1)
);
expect(container.querySelector('.show')).toBeTruthy();
fireEvent.click(summary);
fireEvent.click(summaryClickableArea);
await waitFor(() =>
expect(
container.getElementsByClassName('accordion-summary-expanded')
Expand Down Expand Up @@ -149,8 +149,8 @@ describe('Accordion', () => {
expect(container).toMatchSnapshot();
});

test('Accordion renders custom content', () => {
const { container } = render(
test('Accordion renders custom content and its buttons are clickable', () => {
const { container, getByRole, getByText } = render(
<Accordion
{...accordionProps}
expanded={true}
Expand Down Expand Up @@ -187,7 +187,7 @@ describe('Accordion', () => {
fontWeight: 400,
}}
>
Supporting text
<span>Supporting text</span>
</div>
</Stack>
<Stack
Expand Down Expand Up @@ -218,6 +218,13 @@ describe('Accordion', () => {
}
/>
);
const textElement = getByText('Supporting text');
expect(textElement).toBeInTheDocument();
buttons.forEach((button) => {
const buttonElement = getByRole('button', { name: button.ariaLabel });
fireEvent.click(buttonElement);
expect(buttonElement).toBeEnabled();
});
expect(() => container).not.toThrowError();
expect(container).toMatchSnapshot();
});
Expand Down
158 changes: 105 additions & 53 deletions src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ThemeContext, {
} from '../ConfigProvider/ThemeContext';
import {
AccordionBodyProps,
AccordionLocale,
AccordionProps,
AccordionShape,
AccordionSize,
Expand All @@ -24,6 +25,10 @@ import { Badge } from '../Badge';
import { Button, ButtonShape, ButtonVariant } from '../Button';
import { Icon, IconName } from '../Icon';
import { eventKeys, mergeClasses, uniqueId } from '../../shared/utilities';
import LocaleReceiver, {
useLocaleReceiver,
} from '../LocaleProvider/LocaleReceiver';
import enUS from './Locale/en_US';

import styles from './accordion.module.scss';
import themedComponentStyles from './accordion.theme.module.scss';
Expand All @@ -34,7 +39,9 @@ export const AccordionSummary: FC<AccordionSummaryProps> = ({
badgeProps,
children,
classNames,
collapseAriaLabelText,
disabled,
expandAriaLabelText,
expandButtonProps,
expanded,
expandIconProps,
Expand Down Expand Up @@ -77,24 +84,28 @@ export const AccordionSummary: FC<AccordionSummaryProps> = ({
);

return (
<div
aria-expanded={expanded}
aria-controls={`${id}-content`}
className={headerClassnames}
onClick={onClick}
onKeyDown={handleKeyDown}
id={`${id}-header`}
role="button"
tabIndex={0}
{...rest}
>
<div className={headerClassnames} id={`${id}-header`} {...rest}>
<div
dkilgore-eightfold marked this conversation as resolved.
Show resolved Hide resolved
aria-controls={`${id}-content`}
aria-label={expanded ? collapseAriaLabelText : expandAriaLabelText}
aria-expanded={expanded}
className={styles.clickableArea}
onClick={onClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
></div>
<div className={styles.accordionHeaderContainer}>
{iconProps && <Icon {...iconProps} />}
<span className={styles.accordionHeader}>{children}</span>
<div className={styles.accordionHeader}>
{typeof children === 'string' ? <span>{children}</span> : children}
</div>
{badgeProps && <Badge classNames={styles.badge} {...badgeProps} />}
</div>
<Button
{...expandButtonProps}
aria-controls={`${id}-content`}
ariaLabel={expanded ? collapseAriaLabelText : expandAriaLabelText}
aria-expanded={expanded}
disabled={disabled}
gradient={gradient}
iconProps={{ classNames: iconButtonClassNames, ...expandIconProps }}
Expand Down Expand Up @@ -174,25 +185,28 @@ export const AccordionBody: FC<AccordionBodyProps> = ({
};

export const Accordion: FC<AccordionProps> = React.forwardRef(
(
{
(props: AccordionProps, ref: Ref<HTMLDivElement>) => {
const {
badgeProps,
bodyProps,
bordered = true,
children,
classNames,
collapseAriaLabelText: defaultCollapseAriaLabelText,
configContextProps = {
noGradientContext: false,
noThemeContext: false,
},
disabled,
expandAriaLabelText: defaultExpandAriaLabelText,
expandButtonProps,
expanded = false,
expandIconProps = { path: IconName.mdiChevronDown },
gradient = false,
headerProps,
iconProps,
id = uniqueId('accordion-'),
locale = enUS,
onAccordionChange,
renderContentAlways = true,
shape = AccordionShape.Pill,
Expand All @@ -201,9 +215,7 @@ export const Accordion: FC<AccordionProps> = React.forwardRef(
theme,
themeContainerId,
...rest
},
ref: Ref<HTMLDivElement>
) => {
} = props;
const [isExpanded, setIsExpanded] = useState<boolean>(expanded);

const contextualGradient: Gradient = useContext(GradientContext);
Expand All @@ -220,6 +232,38 @@ export const Accordion: FC<AccordionProps> = React.forwardRef(
setIsExpanded(expanded);
}, [expanded]);

// ============================ Strings ===========================
const [accordionLocale] = useLocaleReceiver('Accordion');
let mergedLocale: AccordionLocale;

if (props.locale) {
mergedLocale = props.locale;
} else {
mergedLocale = accordionLocale || props.locale;
}

const [collapseAriaLabelText, setCollapseAriaLabelText] = useState<string>(
defaultCollapseAriaLabelText
);
const [expandAriaLabelText, setExpandAriaLabelText] = useState<string>(
defaultExpandAriaLabelText
);

// Locs: if the prop isn't provided use the loc defaults.
// If the mergedLocale is changed, update.
useEffect(() => {
setCollapseAriaLabelText(
props.collapseAriaLabelText
? props.collapseAriaLabelText
: mergedLocale.lang!.collapseAriaLabelText
);
setExpandAriaLabelText(
props.expandAriaLabelText
? props.expandAriaLabelText
: mergedLocale.lang!.expandAriaLabelText
);
}, [mergedLocale]);

const toggleAccordion = (expand: boolean): void => {
setIsExpanded(expand);
onAccordionChange?.(expand);
Expand All @@ -238,41 +282,49 @@ export const Accordion: FC<AccordionProps> = React.forwardRef(
);

return (
<ThemeContextProvider
componentClassName={themedComponentStyles.theme}
containerId={themeContainerId}
theme={mergedTheme}
>
<div className={accordionContainerStyle} ref={ref} {...rest}>
<AccordionSummary
badgeProps={badgeProps}
disabled={disabled}
expanded={isExpanded}
expandIconProps={expandIconProps}
expandButtonProps={expandButtonProps}
gradient={gradient}
iconProps={iconProps}
id={id}
onIconButtonClick={() => toggleAccordion(!isExpanded)}
onClick={() => toggleAccordion(!isExpanded)}
size={size}
{...headerProps}
>
{summary}
</AccordionSummary>
<AccordionBody
bordered={bordered}
expanded={isExpanded}
gradient={gradient}
id={id}
renderContentAlways={renderContentAlways}
size={size}
{...bodyProps}
>
{children}
</AccordionBody>
</div>
</ThemeContextProvider>
<LocaleReceiver componentName={'Accordion'} defaultLocale={enUS}>
{(_contextLocale: AccordionLocale) => {
return (
<ThemeContextProvider
componentClassName={themedComponentStyles.theme}
containerId={themeContainerId}
theme={mergedTheme}
>
<div className={accordionContainerStyle} ref={ref} {...rest}>
<AccordionSummary
badgeProps={badgeProps}
collapseAriaLabelText={collapseAriaLabelText}
disabled={disabled}
expandAriaLabelText={expandAriaLabelText}
expanded={isExpanded}
expandIconProps={expandIconProps}
expandButtonProps={expandButtonProps}
gradient={gradient}
iconProps={iconProps}
id={id}
onIconButtonClick={() => toggleAccordion(!isExpanded)}
onClick={() => toggleAccordion(!isExpanded)}
size={size}
{...headerProps}
>
{summary}
</AccordionSummary>
<AccordionBody
bordered={bordered}
expanded={isExpanded}
gradient={gradient}
id={id}
renderContentAlways={renderContentAlways}
size={size}
{...bodyProps}
>
{children}
</AccordionBody>
</div>
</ThemeContextProvider>
);
}}
</LocaleReceiver>
);
}
);
32 changes: 32 additions & 0 deletions src/components/Accordion/Accordion.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ import { IconProps } from '../Icon';
import { BadgeProps } from '../Badge';
import { ButtonProps } from '../Button';

type Locale = {
/**
* The Accordion locale.
*/
locale: string;
/**
* The Accordion `Collapse content` aria label string.
*/
collapseAriaLabelText?: string;
/**
* The Accordion `Expand content` aria label string.
*/
expandAriaLabelText?: string;
};

export type AccordionLocale = {
lang: Locale;
};

export enum AccordionShape {
Pill = 'pill',
Rectangle = 'rectangle',
Expand All @@ -21,6 +40,10 @@ interface AccordionBaseProps extends OcBaseProps<HTMLDivElement> {
* @default true
*/
bordered?: boolean;
/**
* The Accordion `Collapse content` aria label string.
*/
collapseAriaLabelText?: string;
/**
* Configure how contextual props are consumed
*/
Expand All @@ -29,6 +52,10 @@ interface AccordionBaseProps extends OcBaseProps<HTMLDivElement> {
* If the accordion is disabled
*/
disabled?: boolean;
/**
* The Accordion `Expand content` aria label string.
*/
expandAriaLabelText?: string;
/**
* Accordion is in an expanded state or not
* @default false
Expand All @@ -48,6 +75,11 @@ interface AccordionBaseProps extends OcBaseProps<HTMLDivElement> {
* @default false
*/
gradient?: boolean;
/**
* The Accordion locale.
* @default 'enUS'
*/
locale?: AccordionLocale;
/**
* The onClick callback for the accordion.
* @param event
Expand Down
11 changes: 11 additions & 0 deletions src/components/Accordion/Locale/ar_SA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { AccordionLocale } from '../Accordion.types';

const locale: AccordionLocale = {
lang: {
locale: 'ar_SA',
collapseAriaLabelText: 'طي المحتوى',
expandAriaLabelText: 'توسيع المحتوى',
},
};

export default locale;
11 changes: 11 additions & 0 deletions src/components/Accordion/Locale/bg_BG.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { AccordionLocale } from '../Accordion.types';

const locale: AccordionLocale = {
lang: {
locale: 'bg_BG',
collapseAriaLabelText: 'Свиване на съдържанието',
expandAriaLabelText: 'Разширяване на съдържанието',
},
};

export default locale;
11 changes: 11 additions & 0 deletions src/components/Accordion/Locale/cs_CZ.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { AccordionLocale } from '../Accordion.types';

const locale: AccordionLocale = {
lang: {
locale: 'cs_CZ',
collapseAriaLabelText: 'Sbalit obsah',
expandAriaLabelText: 'Rozbalit obsah',
},
};

export default locale;
Loading
Loading