Skip to content
Closed
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
6 changes: 6 additions & 0 deletions packages/react-core/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface CardProps extends React.HTMLProps<HTMLElement>, 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 {
Expand Down Expand Up @@ -65,6 +67,7 @@ export const Card: React.FunctionComponent<CardProps> = ({
isLarge = false,
isFullHeight = false,
isPlain = false,
isOption = false,
ouiaId,
ouiaSafe = true,
...props
Expand All @@ -77,6 +80,8 @@ export const Card: React.FunctionComponent<CardProps> = ({
isLarge = false;
}

const ariaProps = isOption ? { role: 'option', 'aria-selected': isSelected } : {};

const getSelectableModifiers = () => {
if (isDisabledRaised) {
return css(styles.modifiers.nonSelectableRaised);
Expand Down Expand Up @@ -112,6 +117,7 @@ export const Card: React.FunctionComponent<CardProps> = ({
className
)}
tabIndex={isSelectable || isSelectableRaised ? '0' : undefined}
{...ariaProps}
{...props}
{...ouiaProps}
>
Expand Down
4 changes: 4 additions & 0 deletions packages/react-core/src/components/Card/examples/Card.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ export const CardLegacySelectable: React.FunctionComponent = () => {
];

return (
<>
<div role="listbox" aria-label="Selectable cards">
<Card
id="legacy-first-card"
onKeyDown={onKeyDown}
onClick={onClick}
isOption
isSelectable
isSelected={selected === 'legacy-first-card'}
>
Expand All @@ -88,12 +89,13 @@ export const CardLegacySelectable: React.FunctionComponent = () => {
id="legacy-second-card"
onKeyDown={onKeyDown}
onClick={onClick}
isOption
isSelectable
isSelected={selected === 'legacy-second-card'}
>
<CardTitle>Second card</CardTitle>
<CardBody>This is a selectable card. Click me to select me. Click again to deselect me.</CardBody>
</Card>
</>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ export const CardSelectable: React.FunctionComponent = () => {
];

return (
<React.Fragment>
<div role="listbox" aria-label="Selectable cards">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the example should include one sentence above it that describes why you needed to add the role 'listbox' to the div surrounding it.

<Card
id="selectable-first-card"
onKeyDown={onKeyDown}
onClick={onClick}
isOption
isSelectableRaised
isSelected={selected === 'selectable-first-card'}
>
Expand All @@ -91,17 +92,18 @@ export const CardSelectable: React.FunctionComponent = () => {
id="selectable-second-card"
onKeyDown={onKeyDown}
onClick={onClick}
isOption
isSelectableRaised
isSelected={selected === 'selectable-second-card'}
>
<CardTitle>Second card</CardTitle>
<CardBody>This is a selectable card. Click me to select me. Click again to deselect me.</CardBody>
</Card>
<br />
<Card id="selectable-third-card" isSelectableRaised isDisabledRaised>
<Card id="selectable-third-card" isOption isSelectableRaised isDisabledRaised aria-disabled>
<CardTitle>Third card</CardTitle>
<CardBody>This is a raised but disabled card.</CardBody>
</Card>
</React.Fragment>
</div>
);
};
20 changes: 16 additions & 4 deletions packages/react-core/src/demos/Card/Card.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ class CardViewBasic extends React.Component {
splitButtonDropdownIsOpen: false,
page: 1,
perPage: 10,
totalItemCount: 10
totalItemCount: 10,
focusedItemId: '',
};

this.onToolbarDropdownToggle = isLowerToolbarDropdownOpen => {
Expand Down Expand Up @@ -230,6 +231,10 @@ class CardViewBasic extends React.Component {
};
});
};

this.onFocus = id => {
this.setState({focusedItemId: id})
}
}

selectedItems(e) {
Expand Down Expand Up @@ -559,16 +564,20 @@ class CardViewBasic extends React.Component {
</Toolbar>
</PageSection>
<PageSection isFilled>
<Gallery hasGutter>
<Card
<Gallery hasGutter role="listbox" aria-multiselectable="true" aria-activedescendant={this.state.focusedItemId} aria-label="card container">
<Card
isOption
isSelectable
selectableVariant="raised"
isCompact
id="add-card"
onFocus={() => this.onFocus("add-card")}
aria-describedby="#add-card-title"
>
<Bullseye>
<EmptyState variant={EmptyStateVariant.xs}>
<EmptyStateIcon icon={PlusCircleIcon} />
<Title headingLevel="h2" size="md">
<Title headingLevel="h2" size="md" id="add-card-title">
Add a new card to your page
</Title>
<EmptyStateSecondaryActions>
Expand All @@ -579,10 +588,13 @@ class CardViewBasic extends React.Component {
</Card>
{filtered.map((product, key) => (
<Card
isOption
isSelectable
selectableVariant="raised"
isCompact
key={product.name}
onFocus={() => this.onFocus(product.name)}
id={product.name}
onKeyDown={(e) => this.onKeyDown(e, product.id)}
onClick={() => this.onClick(product.id)}
isSelected={selectedItems.includes(product.id)}
Expand Down
4 changes: 3 additions & 1 deletion packages/react-core/src/demos/PrimaryDetail.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1262,7 +1263,7 @@ class PrimaryDetailCardView extends React.Component {
};

const drawerContent = (
<Gallery hasGutter>
<Gallery hasGutter role="listbox" aria-label="Selectable cards">
{filtered.map((product, key) => (
<React.Fragment>
<Card
Expand All @@ -1271,6 +1272,7 @@ class PrimaryDetailCardView extends React.Component {
id={'card-view-' + key}
onKeyDown={this.onKeyDown}
onClick={this.onCardClick}
isOption
isSelectable
isSelected={activeCard === key}
>
Expand Down
3 changes: 2 additions & 1 deletion packages/react-core/src/demos/examples/Tabs/ModalTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ export const ModalTabs: React.FunctionComponent = () => {
</TextContent>
</PageSection>
<PageSection isFilled>
<Gallery hasGutter>
<Gallery hasGutter role="listbox" aria-label="Selectable cards">
{products.map(product => (
<Card
isOption
isSelectable
isSelectableRaised
isCompact
Expand Down