diff --git a/README.md b/README.md
index d642d652..5a566df0 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,9 @@ import 'react-accessible-accordion/dist/fancy-example.css';
We recommend that you copy them into your own app and modify them to suit your
needs, particularly if you're using your own `className`s.
+The accordion trigger is built using native button and heading elements which
+have default browser styling, these can be overridden in your stylesheet.
+
## Component API
### Accordion
@@ -137,9 +140,9 @@ Class(es) to apply to the 'heading' element.
#### aria-level : `number` [*optional*, default: `3`]
-Semantics to apply to the 'heading' element. A value of `1` would make your
-heading element hierarchically equivalent to an `
` tag, and likewise a value
-of `6` would make it equivalent to an `` tag.
+Will determine which 'heading' element is used in the markup. A value of `1`
+would make your element an `` tag, and likewise a value of `6` would make it
+an `` tag.
### AccordionItemButton
@@ -185,7 +188,8 @@ you, including:
- Applying appropriate aria attributes (`aria-expanded`, `aria-controls`,
`aria-disabled`, `aria-hidden` and `aria-labelledby`).
-- Applying appropriate `role` attributes (`button`, `heading`, `region`).
+- Applying appropriate `role` attributes (`region`).
+- Using semantic HTML elements (`h1` - `h6`, `button`).
- Applying appropriate `tabindex` attributes.
- Applying keyboard interactivity ('space', 'end', 'tab', 'up', 'down', 'home'
and 'end' keys).
@@ -196,10 +200,10 @@ spec-compliant:
- Only ever use
[phrasing content](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content)
inside of your `AccordionItemHeading` component. If in doubt, use text only.
-- Always provide an `aria-level` prop to your `AccordionItemHeading`
- component, _especially_ if you are nesting accordions. This attribute is a
- signal used by assistive technologies (eg. screenreaders) to determine which
- heading level (ie. `h1`-`h6`) to treat your heading as.
+- Remember to provide an `aria-level` prop to your `AccordionItemHeading`
+ component, when you are nesting accordions. The levels are used by assistive
+ technologies (eg. screenreaders) to infer structure, by default each heading
+ uses `h3` .
If you have any questions about your implementation, then please don't be afraid
to get in touch via our
@@ -225,8 +229,8 @@ description, as written above. By "accordion-like", we mean components which
have collapsible items but require bespoke interactive mechanisms in order to
expand, collapse and 'disable' them. This includes (but is not limited to)
multi-step forms, like those seen in many cart/checkout flows, which we believe
-require (other) complex markup in order to be considered 'accessible'.
-This also includes disclosure widgets.
+require (other) complex markup in order to be considered 'accessible'. This also
+includes disclosure widgets.
### How do I disable an item?
diff --git a/demo/src/main.css b/demo/src/main.css
index 75a215fb..b3a47012 100644
--- a/demo/src/main.css
+++ b/demo/src/main.css
@@ -134,11 +134,14 @@ footer {
margin-top: 10px;
}
.code__button {
- font-weight: bold;
cursor: pointer;
display: inline-block;
padding: 10px 0;
color: var(--colorPageLinks);
+ background-color: transparent;
+ font: inherit;
+ font-weight: bold;
+ border: none;
text-decoration: underline solid var(--colorGreenShadow);
}
.code__button:hover {
diff --git a/integration/wai-aria.spec.js b/integration/wai-aria.spec.js
index b7b2141c..0bbf748f 100644
--- a/integration/wai-aria.spec.js
+++ b/integration/wai-aria.spec.js
@@ -237,42 +237,16 @@ describe('WAI ARIA Spec', () => {
});
describe('WAI-ARIA Roles, States, and Properties', () => {
- it(`The title of each accordion header is contained in an element with
- role button.`, async () => {
- // TODO: Use 'title' elements inside the headings.
-
- const { browser, page, buttonsHandles } = await setup();
- expect(buttonsHandles).toHaveLength(3);
- for (const buttonHandle of buttonsHandles) {
- expect(
- await page.evaluate(
- (button) => button.getAttribute('role'),
- buttonHandle,
- ),
- ).toBe('button');
- }
- });
-
- it(`Each accordion header button is wrapped in an element with role
- heading that has a value set for aria-level that is appropriate for
- the information architecture of the page.`, async () => {
- const { browser, page, buttonsHandles } = await setup();
- expect(buttonsHandles).toHaveLength(3);
- for (const buttonHandle of buttonsHandles) {
- expect(
- await page.evaluate(
- (button) => button.parentElement.getAttribute('role'),
- buttonHandle,
- ),
- ).toBe('heading');
-
+ it(`The title of each accordion header is contained in a heading tag`, async () => {
+ const { browser, page, headingsHandles } = await setup();
+ expect(headingsHandles).toHaveLength(3);
+ for (const headingsHandle of headingsHandles) {
expect(
await page.evaluate(
- (button) =>
- button.parentElement.getAttribute('aria-level'),
- buttonHandle,
+ (heading) => heading.tagName,
+ headingsHandle,
),
- ).toBeTruthy();
+ ).toContain('H3');
}
});
diff --git a/src/components/AccordionContext.tsx b/src/components/AccordionContext.tsx
index 1509fed2..a7f6936d 100644
--- a/src/components/AccordionContext.tsx
+++ b/src/components/AccordionContext.tsx
@@ -3,7 +3,6 @@
import * as React from 'react';
import AccordionStore, {
InjectedButtonAttributes,
- InjectedHeadingAttributes,
InjectedPanelAttributes,
} from '../helpers/AccordionStore';
import { ID } from './ItemContext';
@@ -28,7 +27,6 @@ export interface AccordionContext {
uuid: ID,
dangerouslySetExpanded?: boolean,
): InjectedPanelAttributes;
- getHeadingAttributes(uuid: ID): InjectedHeadingAttributes;
getButtonAttributes(
uuid: ID,
dangerouslySetExpanded?: boolean,
@@ -78,11 +76,6 @@ export class Provider extends React.PureComponent<
return this.state.getPanelAttributes(key, dangerouslySetExpanded);
};
- getHeadingAttributes = (): InjectedHeadingAttributes => {
- // uuid: UUID
- return this.state.getHeadingAttributes();
- };
-
getButtonAttributes = (
key: ID,
dangerouslySetExpanded?: boolean,
@@ -102,7 +95,6 @@ export class Provider extends React.PureComponent<
isItemDisabled: this.isItemDisabled,
isItemExpanded: this.isItemExpanded,
getPanelAttributes: this.getPanelAttributes,
- getHeadingAttributes: this.getHeadingAttributes,
getButtonAttributes: this.getButtonAttributes,
}}
>
diff --git a/src/components/AccordionItemButton.tsx b/src/components/AccordionItemButton.tsx
index 253a19e9..e5321e5d 100644
--- a/src/components/AccordionItemButton.tsx
+++ b/src/components/AccordionItemButton.tsx
@@ -7,12 +7,12 @@ import {
focusPreviousSiblingOf,
} from '../helpers/focus';
import keycodes from '../helpers/keycodes';
-import { DivAttributes } from '../helpers/types';
import { assertValidHtmlId } from '../helpers/id';
+import { ButtonAttributes } from '../helpers/types';
import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
-type Props = DivAttributes & {
+type Props = ButtonAttributes & {
toggleExpanded(): void;
};
@@ -21,7 +21,9 @@ const AccordionItemButton = ({
className = 'accordion__button',
...rest
}: Props) => {
- const handleKeyPress = (evt: React.KeyboardEvent): void => {
+ const handleKeyPress = (
+ evt: React.KeyboardEvent,
+ ): void => {
const keyCode = evt.key;
if (
@@ -74,11 +76,9 @@ const AccordionItemButton = ({
}
return (
-
+ ButtonAttributes,
+ Exclude
>;
const AccordionItemButtonWrapper: React.SFC = (
diff --git a/src/components/AccordionItemHeading.spec.tsx b/src/components/AccordionItemHeading.spec.tsx
index 6999560e..ccf72f50 100644
--- a/src/components/AccordionItemHeading.spec.tsx
+++ b/src/components/AccordionItemHeading.spec.tsx
@@ -59,6 +59,39 @@ describe('AccordionItem', () => {
});
});
+ describe('aria-level prop', () => {
+ it('is h3 by default', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(getByTestId(UUIDS.FOO).tagName).toEqual('H3');
+ });
+
+ it('can be overridden', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(getByTestId(UUIDS.FOO).tagName).toEqual('H4');
+ });
+ });
+
describe('children prop', () => {
it('is respected', () => {
const { getByText } = render(
diff --git a/src/components/AccordionItemHeading.tsx b/src/components/AccordionItemHeading.tsx
index e71b4876..28dd4577 100644
--- a/src/components/AccordionItemHeading.tsx
+++ b/src/components/AccordionItemHeading.tsx
@@ -1,17 +1,12 @@
import * as React from 'react';
-import { InjectedHeadingAttributes } from '../helpers/AccordionStore';
import DisplayName from '../helpers/DisplayName';
-import { DivAttributes } from '../helpers/types';
import { assertValidHtmlId } from '../helpers/id';
+import { HeadingAttributes } from '../helpers/types';
-import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
-
-type Props = DivAttributes;
-
-const defaultProps = {
- className: 'accordion__heading',
- 'aria-level': 3,
-};
+interface AccordionItemHeadingProps extends HeadingAttributes {
+ className?: string;
+ 'aria-level'?: number;
+}
export const SPEC_ERROR = `AccordionItemButton may contain only one child element, which must be an instance of AccordionItemButton.
@@ -21,12 +16,30 @@ From the WAI-ARIA spec (https://www.w3.org/TR/wai-aria-practices-1.1/#accordion)
`;
-export class AccordionItemHeading extends React.PureComponent {
- static defaultProps: typeof defaultProps = defaultProps;
+const Heading = React.forwardRef(
+ (
+ {
+ 'aria-level': ariaLevel = 3,
+ className = 'accordion__heading',
+ ...props
+ }: AccordionItemHeadingProps,
+ ref,
+ ) => {
+ const headingTag = `h${ariaLevel}`;
+ return React.createElement(headingTag, {
+ className,
+ ...props,
+ ref,
+ 'data-accordion-component': 'AccordionItemHeading',
+ });
+ },
+);
+Heading.displayName = 'Heading';
- ref: HTMLDivElement | undefined;
+export class AccordionItemHeading extends React.PureComponent {
+ ref: HTMLHeadingElement | undefined;
- static VALIDATE(ref: HTMLDivElement | undefined): void | never {
+ static VALIDATE(ref: HTMLHeadingElement | undefined): void | never {
if (ref === undefined) {
throw new Error('ref is undefined');
}
@@ -43,7 +56,7 @@ export class AccordionItemHeading extends React.PureComponent {
}
}
- setRef = (ref: HTMLDivElement): void => {
+ setRef = (ref: HTMLHeadingElement): void => {
this.ref = ref;
};
@@ -56,36 +69,19 @@ export class AccordionItemHeading extends React.PureComponent {
}
render(): JSX.Element {
- return (
-
- );
+ return ;
}
}
-type WrapperProps = Pick<
- DivAttributes,
- Exclude
->;
-
-const AccordionItemHeadingWrapper: React.SFC = (
- props: WrapperProps,
-): JSX.Element => (
-
- {(itemContext: ItemContext): JSX.Element => {
- const { headingAttributes } = itemContext;
-
- if (props.id) {
- assertValidHtmlId(props.id);
- }
-
- return ;
- }}
-
-);
+const AccordionItemHeadingWrapper: React.FC = (
+ props: AccordionItemHeadingProps,
+): JSX.Element => {
+ if (props.id) {
+ assertValidHtmlId(props.id);
+ }
+
+ return ;
+};
AccordionItemHeadingWrapper.displayName = DisplayName.AccordionItemHeading;
diff --git a/src/components/ItemContext.tsx b/src/components/ItemContext.tsx
index 99310a56..c6e15e9d 100644
--- a/src/components/ItemContext.tsx
+++ b/src/components/ItemContext.tsx
@@ -3,7 +3,6 @@
import * as React from 'react';
import {
InjectedButtonAttributes,
- InjectedHeadingAttributes,
InjectedPanelAttributes,
} from '../helpers/AccordionStore';
import {
@@ -30,7 +29,6 @@ export type ItemContext = {
expanded: boolean;
disabled: boolean;
panelAttributes: InjectedPanelAttributes;
- headingAttributes: InjectedHeadingAttributes;
buttonAttributes: InjectedButtonAttributes;
toggleExpanded(): void;
};
@@ -57,7 +55,6 @@ const Provider = ({
uuid,
dangerouslySetExpanded,
);
- const headingAttributes = accordionContext.getHeadingAttributes(uuid);
const buttonAttributes = accordionContext.getButtonAttributes(
uuid,
dangerouslySetExpanded,
@@ -71,7 +68,6 @@ const Provider = ({
disabled,
toggleExpanded: toggleExpanded,
panelAttributes,
- headingAttributes,
buttonAttributes,
}}
>
diff --git a/src/css/fancy-example.css b/src/css/fancy-example.css
index dd75d55a..c13d3462 100644
--- a/src/css/fancy-example.css
+++ b/src/css/fancy-example.css
@@ -11,7 +11,10 @@
.accordion__item + .accordion__item {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
-
+.accordion__heading {
+ margin: 0;
+ font: inherit;
+}
.accordion__button {
background-color: #f4f4f4;
color: #444;
@@ -20,6 +23,8 @@
width: 100%;
text-align: left;
border: none;
+ font: inherit;
+ margin: 0;
}
.accordion__button:hover {
diff --git a/src/helpers/AccordionStore.ts b/src/helpers/AccordionStore.ts
index 3c85e3da..a52352d0 100644
--- a/src/helpers/AccordionStore.ts
+++ b/src/helpers/AccordionStore.ts
@@ -8,17 +8,11 @@ export interface InjectedPanelAttributes {
hidden: boolean | undefined;
}
-export interface InjectedHeadingAttributes {
- role: string;
-}
-
export interface InjectedButtonAttributes {
id: string;
'aria-controls': string;
'aria-expanded': boolean;
'aria-disabled': boolean;
- role: string;
- tabIndex: number;
}
export default class AccordionStore {
@@ -96,13 +90,6 @@ export default class AccordionStore {
};
};
- public readonly getHeadingAttributes = (): // uuid: UUID,
- InjectedHeadingAttributes => {
- return {
- role: 'heading',
- };
- };
-
public readonly getButtonAttributes = (
uuid: ID,
dangerouslySetExpanded?: boolean,
@@ -115,8 +102,6 @@ export default class AccordionStore {
'aria-disabled': disabled,
'aria-expanded': expanded,
'aria-controls': this.getPanelId(uuid),
- role: 'button',
- tabIndex: 0,
};
};
diff --git a/src/helpers/types.ts b/src/helpers/types.ts
index 7a23b7ce..414e93de 100644
--- a/src/helpers/types.ts
+++ b/src/helpers/types.ts
@@ -1,3 +1,5 @@
import * as React from 'react';
export type DivAttributes = React.HTMLAttributes;
+export type ButtonAttributes = React.HTMLAttributes;
+export type HeadingAttributes = React.HTMLAttributes;