Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Add A11y implementation",
"packageName": "@fluentui/react-accordion",
"email": "bsunderhus@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Updates react-accordion examples",
"packageName": "@fluentui/react-examples",
"email": "bsunderhus@microsoft.com",
"dependentChangeType": "patch"
}
7 changes: 7 additions & 0 deletions packages/react-accordion/Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ The root level component serves context and common API between all children.

```ts
export interface AccordionProps extends ComponentProps, React.HTMLAttributes<HTMLElement> {
/**
* Indicates if keyboard navigation is available
*/
navigable?: boolean;
/**
* Indicates if Accordion support multiple Panels opened at the same time
*/
Expand Down Expand Up @@ -388,6 +392,9 @@ Expected DOM output

## Behaviors

- Keyboard navigation should be optional and native tabbing used by default.
- Circular Navigation should be optional and disabled by default.
Comment thread
bsunderhus marked this conversation as resolved.

### Useful references

The below references were used to decide an appropriate keyboard interactions from an a11y perspective.
Expand Down
9 changes: 7 additions & 2 deletions packages/react-accordion/etc/react-accordion.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const Accordion: React.ForwardRefExoticComponent<AccordionProps & React.R
//
// @public (undocumented)
export interface AccordionContextValue extends AccordionHeaderCommonProps {
// (undocumented)
navigable: boolean;
openItems: number[];
requestToggle: NonNullable<AccordionProps['onToggle']>;
}
Expand Down Expand Up @@ -93,7 +95,7 @@ export interface AccordionItemContextValue {
// (undocumented)
disabled: boolean;
// (undocumented)
onHeaderClick(ev: React.MouseEvent<HTMLElement>): void;
onHeaderClick(ev: React.MouseEvent | React.KeyboardEvent): void;
// (undocumented)
open: boolean;
}
Expand Down Expand Up @@ -146,8 +148,9 @@ export interface AccordionProps extends ComponentProps, AccordionHeaderCommonPro
defaultIndex?: AccordionIndex;
index?: AccordionIndex;
multiple?: boolean;
navigable?: boolean;
// (undocumented)
onToggle?(event: React.MouseEvent<HTMLElement>, index: number): void;
onToggle?(event: React.MouseEvent | React.KeyboardEvent, index: number): void;
}

// @public
Expand All @@ -161,6 +164,8 @@ export interface AccordionState extends AccordionProps {
descendants: AccordionDescendant[];
// (undocumented)
multiple: boolean;
// (undocumented)
navigable: boolean;
ref: React.MutableRefObject<HTMLElement>;
setDescendants: React.Dispatch<React.SetStateAction<AccordionDescendant[]>>;
}
Expand Down
1 change: 1 addition & 0 deletions packages/react-accordion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"@fluentui/react-context-selector": "^9.0.0-alpha.2",
"@fluentui/react-make-styles": "^9.0.0-alpha.20",
"@fluentui/react-tabster": "^9.0.0-alpha.14",
"@fluentui/react-theme": "^9.0.0-alpha.8",
"@fluentui/react-theme-provider": "^9.0.0-alpha.19",
"@fluentui/react-utilities": "^9.0.0-alpha.15",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type AccordionHeaderCommonProps = Pick<
'expandIcon' | 'expandIconPosition' | 'icon' | 'button' | 'size' | 'inline'
>;
export interface AccordionContextValue extends AccordionHeaderCommonProps {
navigable: boolean;
/**
* The list of opened panels by index
*/
Expand All @@ -23,6 +24,10 @@ export interface AccordionContextValue extends AccordionHeaderCommonProps {
}

export interface AccordionProps extends ComponentProps, AccordionHeaderCommonProps, React.HTMLAttributes<HTMLElement> {
/**
* Indicates if keyboard navigation is available
*/
navigable?: boolean;
/**
* Indicates if Accordion support multiple Panels opened at the same time
*/
Expand All @@ -39,14 +44,15 @@ export interface AccordionProps extends ComponentProps, AccordionHeaderCommonPro
* Default value for the uncontrolled state of the panel
*/
defaultIndex?: AccordionIndex;
onToggle?(event: React.MouseEvent<HTMLElement>, index: number): void;
onToggle?(event: React.MouseEvent | React.KeyboardEvent, index: number): void;
}

export interface AccordionState extends AccordionProps {
/**
* Ref to the root slot
*/
ref: React.MutableRefObject<HTMLElement>;
navigable: boolean;
multiple: boolean;
collapsible: boolean;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ export const useAccordion = (
ref: useMergedRefs(ref, React.useRef(null)),
collapsible: false,
multiple: false,
navigable: false,
},
defaultProps,
resolveShorthandProps(props, accordionShorthandProps),
);
const [context, descendants, setDescendants] = useCreateAccordionContextValue(state);
Object.assign(state, { context, descendants, setDescendants });

state.context = context;
state.descendants = descendants;
state.setDescendants = setDescendants;
return state;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const AccordionDescendantContext = createDescendantContext<AccordionDesce

export const AccordionContext = createContext<AccordionContextValue>({
openItems: [],
navigable: false,
requestToggle() {
/* noop */
},
Expand All @@ -22,7 +23,19 @@ export const AccordionContext = createContext<AccordionContextValue>({
* Creates the context to be provided for AccordionItem components
*/
export function useCreateAccordionContextValue(state: AccordionState) {
const { index, multiple, collapsible, onToggle, size, inline, icon, expandIcon, expandIconPosition, button } = state;
const {
index,
multiple,
collapsible,
onToggle,
size,
inline,
icon,
navigable,
expandIcon,
expandIconPosition,
button,
} = state;
const [descendants, setDescendants] = useDescendantsInit<AccordionDescendant>();
const normalizedIndex = React.useMemo(() => (index !== undefined ? normalizeIndex(index) : undefined), [index]);
const [openItems, setOpenItems] = useControllableValue<number[], HTMLElement>(normalizedIndex!, () =>
Expand All @@ -42,6 +55,7 @@ export function useCreateAccordionContextValue(state: AccordionState) {
);
});
const context: AccordionContextValue = {
navigable,
inline,
icon,
openItems,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exports[`AccordionHeader renders a default state 1`] = `
className=""
id="accordion-header-21"
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
useMergedRefs,
useId,
useDescendants,
shouldPreventDefaultOnKeyDown,
useEventCallback,
} from '@fluentui/react-utilities';
import {
AccordionHeaderExpandIconPosition,
Expand Down Expand Up @@ -78,6 +80,20 @@ export const useAccordionHeader = (
defaultProps,
resolveShorthandProps(props, accordionHeaderShorthandProps),
);
const originalButtonKeyDown = state.button.onKeyDown;
state.button.onKeyDown = useEventCallback((ev: React.KeyboardEvent<HTMLElement>) => {
if (shouldPreventDefaultOnKeyDown(ev)) {
if (disabled) {
ev.preventDefault();
ev.stopPropagation();
return;
}
ev.preventDefault();
onAccordionHeaderClick(ev);
}
originalButtonKeyDown?.(ev);
});

useAccordionItemDescendant(
{
element: state.ref.current,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ComponentProps, Descendant } from '@fluentui/react-utilities';
export interface AccordionItemContextValue {
open: boolean;
disabled: boolean;
onHeaderClick(ev: React.MouseEvent<HTMLElement>): void;
onHeaderClick(ev: React.MouseEvent | React.KeyboardEvent): void;
}

export interface AccordionItemProps extends ComponentProps, React.HTMLAttributes<HTMLElement> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
} from '@fluentui/react-utilities';
import { AccordionItemProps, AccordionItemState, AccordionItemDescendant } from './AccordionItem.types';
import { useCreateAccordionItemContextValue } from './useAccordionItemContext';
import { getTabsterAttribute } from '@fluentui/react-tabster';
import { useContextSelector } from '@fluentui/react-context-selector';
import { AccordionContext } from '../Accordion/useAccordionContext';

/**
* Consts listing which props are shorthand props.
Expand Down Expand Up @@ -44,6 +47,13 @@ export const useAccordionItem = (
state.descendants = descendants;
state.setDescendants = setDescendants;
state.context = useCreateAccordionItemContextValue(state);
const navigable = useContextSelector(AccordionContext, ctx => ctx.navigable);
const tabsterAttributes = getTabsterAttribute({
groupper: {},
});
Comment thread
bsunderhus marked this conversation as resolved.
if (navigable) {
Object.assign(state, tabsterAttributes);
}
return state;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export function useCreateAccordionItemContextValue(state: AccordionItemState) {
});
const requestToggle = useContextSelector(AccordionContext, ctx => ctx.requestToggle);
const open = useContextSelector(AccordionContext, ctx => ctx.openItems.includes(index));
const onAccordionHeaderClick = React.useCallback((ev: React.MouseEvent<HTMLElement>) => requestToggle(ev, index), [
requestToggle,
index,
]);
const onAccordionHeaderClick = React.useCallback(
(ev: React.MouseEvent | React.KeyboardEvent) => requestToggle(ev, index),
[requestToggle, index],
);
const context = React.useMemo<AccordionItemContextValue>(
() => ({
open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,30 @@ export const AccordionExample = (props: AccordionProps) => {
<Accordion {...props} icon={props.icon ? <RocketIcon /> : undefined}>
<AccordionItem>
<AccordionHeader>Accordion Header 1</AccordionHeader>
<AccordionPanel>Accordion Panel 1</AccordionPanel>
<AccordionPanel>
<div>
<button>Button 1</button>
</div>
<div>Accordion Panel 1</div>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionHeader>Accordion Header 2</AccordionHeader>
<AccordionPanel>Accordion Panel 2</AccordionPanel>
<AccordionPanel>
<div>
<button>Button 2</button>
</div>
<div>Accordion Panel 2</div>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionHeader>Accordion Header 3</AccordionHeader>
<AccordionPanel>Accordion Panel 3</AccordionPanel>
<AccordionPanel>
<div>
<button>Button 3</button>
</div>
<div>Accordion Panel 3</div>
</AccordionPanel>
</AccordionItem>
</Accordion>
);
Expand All @@ -26,6 +41,14 @@ AccordionExample.argTypes = {
defaultValue: false,
control: 'boolean',
},
navigable: {
defaultValue: false,
control: 'boolean',
},
circular: {
defaultValue: false,
control: 'boolean',
},
multiple: {
defaultValue: false,
control: 'boolean',
Expand Down