diff --git a/packages/react-core/src/components/Card/Card.tsx b/packages/react-core/src/components/Card/Card.tsx index ac22e2c151b..9cc64d1e58d 100644 --- a/packages/react-core/src/components/Card/Card.tsx +++ b/packages/react-core/src/components/Card/Card.tsx @@ -36,6 +36,8 @@ export interface CardProps extends React.HTMLProps, OUIAProps { isPlain?: boolean; /** Flag indicating if a card is expanded. Modifies the card to be expandable. */ isExpanded?: boolean; + /** Flag indicating that the card is an option in a listbox */ + isOption?: boolean; } interface CardContextProps { @@ -65,6 +67,7 @@ export const Card: React.FunctionComponent = ({ isLarge = false, isFullHeight = false, isPlain = false, + isOption = false, ouiaId, ouiaSafe = true, ...props @@ -77,6 +80,8 @@ export const Card: React.FunctionComponent = ({ isLarge = false; } + const ariaProps = isOption ? { role: 'option', 'aria-selected': isSelected } : {}; + const getSelectableModifiers = () => { if (isDisabledRaised) { return css(styles.modifiers.nonSelectableRaised); @@ -112,6 +117,7 @@ export const Card: React.FunctionComponent = ({ className )} tabIndex={isSelectable || isSelectableRaised ? '0' : undefined} + {...ariaProps} {...props} {...ouiaProps} > diff --git a/packages/react-core/src/components/Card/examples/Card.md b/packages/react-core/src/components/Card/examples/Card.md index 6239b8a7913..b8058d02a69 100644 --- a/packages/react-core/src/components/Card/examples/Card.md +++ b/packages/react-core/src/components/Card/examples/Card.md @@ -68,6 +68,10 @@ import pfLogoSmall from './pf-logo-small.svg'; ### Selectable +Note: The listbox role on the containing div of this examples is needed because the cards have the option role via their `isOption` prop, and options are required to be inside a listbox. See the [option role documentation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role) from MDN for more information. + +The cards need to be options in order to effectively communicate that they're selectable (and what their current selection state is) for those using assistive technologies (such as screen readers). + ```ts file='./CardSelectable.tsx' ``` diff --git a/packages/react-core/src/components/Card/examples/CardLegacySelectable.tsx b/packages/react-core/src/components/Card/examples/CardLegacySelectable.tsx index b8af08215e0..fd7b5da5771 100644 --- a/packages/react-core/src/components/Card/examples/CardLegacySelectable.tsx +++ b/packages/react-core/src/components/Card/examples/CardLegacySelectable.tsx @@ -60,11 +60,12 @@ export const CardLegacySelectable: React.FunctionComponent = () => { ]; return ( - <> +
@@ -88,12 +89,13 @@ export const CardLegacySelectable: React.FunctionComponent = () => { id="legacy-second-card" onKeyDown={onKeyDown} onClick={onClick} + isOption isSelectable isSelected={selected === 'legacy-second-card'} > Second card This is a selectable card. Click me to select me. Click again to deselect me. - +
); }; diff --git a/packages/react-core/src/components/Card/examples/CardSelectable.tsx b/packages/react-core/src/components/Card/examples/CardSelectable.tsx index b8708882d51..bb56bd103f8 100644 --- a/packages/react-core/src/components/Card/examples/CardSelectable.tsx +++ b/packages/react-core/src/components/Card/examples/CardSelectable.tsx @@ -63,11 +63,12 @@ export const CardSelectable: React.FunctionComponent = () => { ]; return ( - +
@@ -91,6 +92,7 @@ export const CardSelectable: React.FunctionComponent = () => { id="selectable-second-card" onKeyDown={onKeyDown} onClick={onClick} + isOption isSelectableRaised isSelected={selected === 'selectable-second-card'} > @@ -98,10 +100,10 @@ export const CardSelectable: React.FunctionComponent = () => { This is a selectable card. Click me to select me. Click again to deselect me.
- + Third card This is a raised but disabled card. - +
); }; diff --git a/packages/react-core/src/demos/Card/Card.md b/packages/react-core/src/demos/Card/Card.md index eab5c78d57b..32b1bf5fbf1 100644 --- a/packages/react-core/src/demos/Card/Card.md +++ b/packages/react-core/src/demos/Card/Card.md @@ -103,7 +103,8 @@ class CardViewBasic extends React.Component { splitButtonDropdownIsOpen: false, page: 1, perPage: 10, - totalItemCount: 10 + totalItemCount: 10, + focusedItemId: '', }; this.onToolbarDropdownToggle = isLowerToolbarDropdownOpen => { @@ -230,6 +231,10 @@ class CardViewBasic extends React.Component { }; }); }; + + this.onFocus = id => { + this.setState({focusedItemId: id}) + } } selectedItems(e) { @@ -559,16 +564,20 @@ class CardViewBasic extends React.Component { - - + this.onFocus("add-card")} + aria-describedby="#add-card-title" > - + <Title headingLevel="h2" size="md" id="add-card-title"> Add a new card to your page @@ -579,10 +588,13 @@ class CardViewBasic extends React.Component { {filtered.map((product, key) => ( this.onFocus(product.name)} + id={product.name} onKeyDown={(e) => this.onKeyDown(e, product.id)} onClick={() => this.onClick(product.id)} isSelected={selectedItems.includes(product.id)} diff --git a/packages/react-core/src/demos/PrimaryDetail.md b/packages/react-core/src/demos/PrimaryDetail.md index 293a448c06a..e1b7cf797d2 100644 --- a/packages/react-core/src/demos/PrimaryDetail.md +++ b/packages/react-core/src/demos/PrimaryDetail.md @@ -1100,6 +1100,7 @@ class PrimaryDetailCardView extends React.Component { return; } if ([13, 32].includes(event.keyCode)) { + event.preventDefault(); const newSelected = event.currentTarget.id; this.setState({ activeCard: newSelected, @@ -1262,7 +1263,7 @@ class PrimaryDetailCardView extends React.Component { }; const drawerContent = ( - + {filtered.map((product, key) => ( diff --git a/packages/react-core/src/demos/examples/Tabs/ModalTabs.tsx b/packages/react-core/src/demos/examples/Tabs/ModalTabs.tsx index 3a0222f73aa..98d11ea400d 100644 --- a/packages/react-core/src/demos/examples/Tabs/ModalTabs.tsx +++ b/packages/react-core/src/demos/examples/Tabs/ModalTabs.tsx @@ -89,9 +89,10 @@ export const ModalTabs: React.FunctionComponent = () => { - + {products.map(product => (