Skip to content

Commit 3b41786

Browse files
fix(Card): indicate card selectivity and status if using a screen reader
1 parent 51ec4fd commit 3b41786

File tree

5 files changed

+77
-23
lines changed

5 files changed

+77
-23
lines changed

packages/react-core/src/components/Card/Card.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export interface CardProps extends React.HTMLProps<HTMLElement>, OUIAProps {
3636
isPlain?: boolean;
3737
/** Flag indicating if a card is expanded. Modifies the card to be expandable. */
3838
isExpanded?: boolean;
39+
/** Flag indicating that the card should render a hidden input for a11y reasons */
40+
hasHiddenInput?: boolean;
41+
/** Callback that executes when the hidden input is changed */
42+
onHiddenInputChange?: (labelledBy: string, event: React.FormEvent<HTMLInputElement>) => void;
3943
}
4044

4145
interface CardContextProps {
@@ -67,6 +71,8 @@ export const Card: React.FunctionComponent<CardProps> = ({
6771
isPlain = false,
6872
ouiaId,
6973
ouiaSafe = true,
74+
hasHiddenInput = false,
75+
onHiddenInputChange = () => {},
7076
...props
7177
}: CardProps) => {
7278
const Component = component as any;
@@ -90,33 +96,51 @@ export const Card: React.FunctionComponent<CardProps> = ({
9096
return '';
9197
};
9298

99+
const CardComponent = (
100+
<Component
101+
id={id}
102+
className={css(
103+
styles.card,
104+
isCompact && styles.modifiers.compact,
105+
isExpanded && styles.modifiers.expanded,
106+
isFlat && styles.modifiers.flat,
107+
isRounded && styles.modifiers.rounded,
108+
isLarge && styles.modifiers.displayLg,
109+
isFullHeight && styles.modifiers.fullHeight,
110+
isPlain && styles.modifiers.plain,
111+
getSelectableModifiers(),
112+
className
113+
)}
114+
tabIndex={isSelectable || isSelectableRaised ? '0' : undefined}
115+
{...props}
116+
{...ouiaProps}
117+
>
118+
{children}
119+
</Component>
120+
);
121+
122+
const CardWithHiddenInput = (
123+
<label htmlFor={id}>
124+
<input
125+
className="pf-screen-reader"
126+
type="checkbox"
127+
checked={isSelected}
128+
onChange={event => onHiddenInputChange(id, event)}
129+
aria-labelledby={id}
130+
disabled={isDisabledRaised}
131+
/>
132+
{CardComponent}
133+
</label>
134+
);
135+
93136
return (
94137
<CardContext.Provider
95138
value={{
96139
cardId: id,
97140
isExpanded
98141
}}
99142
>
100-
<Component
101-
id={id}
102-
className={css(
103-
styles.card,
104-
isCompact && styles.modifiers.compact,
105-
isExpanded && styles.modifiers.expanded,
106-
isFlat && styles.modifiers.flat,
107-
isRounded && styles.modifiers.rounded,
108-
isLarge && styles.modifiers.displayLg,
109-
isFullHeight && styles.modifiers.fullHeight,
110-
isPlain && styles.modifiers.plain,
111-
getSelectableModifiers(),
112-
className
113-
)}
114-
tabIndex={isSelectable || isSelectableRaised ? '0' : undefined}
115-
{...props}
116-
{...ouiaProps}
117-
>
118-
{children}
119-
</Component>
143+
{hasHiddenInput ? CardWithHiddenInput : CardComponent}
120144
</CardContext.Provider>
121145
);
122146
};

packages/react-core/src/components/Card/examples/CardSelectable.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export const CardSelectable: React.FunctionComponent = () => {
3131
setSelected(newSelected);
3232
};
3333

34+
const onChange = (labelledById: string, _event: React.FormEvent<HTMLInputElement>) => {
35+
const newSelected = labelledById === selected ? null : labelledById;
36+
setSelected(newSelected);
37+
};
38+
3439
const onToggle = (
3540
isOpen: boolean,
3641
event: MouseEvent | TouchEvent | KeyboardEvent | React.KeyboardEvent<any> | React.MouseEvent<HTMLButtonElement>
@@ -68,6 +73,8 @@ export const CardSelectable: React.FunctionComponent = () => {
6873
id="selectable-first-card"
6974
onKeyDown={onKeyDown}
7075
onClick={onClick}
76+
hasHiddenInput
77+
onHiddenInputChange={onChange}
7178
isSelectableRaised
7279
isSelected={selected === 'selectable-first-card'}
7380
>
@@ -91,14 +98,16 @@ export const CardSelectable: React.FunctionComponent = () => {
9198
id="selectable-second-card"
9299
onKeyDown={onKeyDown}
93100
onClick={onClick}
101+
hasHiddenInput
102+
onHiddenInputChange={onChange}
94103
isSelectableRaised
95104
isSelected={selected === 'selectable-second-card'}
96105
>
97106
<CardTitle>Second card</CardTitle>
98107
<CardBody>This is a selectable card. Click me to select me. Click again to deselect me.</CardBody>
99108
</Card>
100109
<br />
101-
<Card id="selectable-third-card" isSelectableRaised isDisabledRaised>
110+
<Card id="selectable-third-card" isSelectableRaised isDisabledRaised aria-disabled hasHiddenInput>
102111
<CardTitle>Third card</CardTitle>
103112
<CardBody>This is a raised but disabled card.</CardBody>
104113
</Card>

packages/react-core/src/demos/Card/Card.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ class CardViewBasic extends React.Component {
103103
splitButtonDropdownIsOpen: false,
104104
page: 1,
105105
perPage: 10,
106-
totalItemCount: 10
106+
totalItemCount: 10,
107+
focusedItemId: '',
107108
};
108109

109110
this.onToolbarDropdownToggle = isLowerToolbarDropdownOpen => {
@@ -581,10 +582,13 @@ class CardViewBasic extends React.Component {
581582
<Card
582583
isSelectable
583584
selectableVariant="raised"
585+
hasHiddenInput
584586
isCompact
585587
key={product.name}
588+
id={product.name}
586589
onKeyDown={(e) => this.onKeyDown(e, product.id)}
587590
onClick={() => this.onClick(product.id)}
591+
onHiddenInputChange={() => this.onClick(product.id)}
588592
isSelected={selectedItems.includes(product.id)}
589593
>
590594
<CardHeader>

packages/react-core/src/demos/PrimaryDetail.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,7 @@ class PrimaryDetailCardView extends React.Component {
11001100
return;
11011101
}
11021102
if ([13, 32].includes(event.keyCode)) {
1103+
event.preventDefault();
11031104
const newSelected = event.currentTarget.id;
11041105
this.setState({
11051106
activeCard: newSelected,
@@ -1121,6 +1122,17 @@ class PrimaryDetailCardView extends React.Component {
11211122
});
11221123
};
11231124

1125+
this.onChange = (labelledById, _event) => {
1126+
if (labelledById === this.state.activeCard) {
1127+
return;
1128+
}
1129+
1130+
this.setState({
1131+
activeCard: labelledById,
1132+
isDrawerExpanded: true
1133+
});
1134+
};
1135+
11241136
this.onPerPageSelect = (_evt, perPage) => {
11251137
this.setState({ page: 1, perPage });
11261138
};
@@ -1271,8 +1283,10 @@ class PrimaryDetailCardView extends React.Component {
12711283
id={'card-view-' + key}
12721284
onKeyDown={this.onKeyDown}
12731285
onClick={this.onCardClick}
1286+
onHiddenInputChange={this.onChange}
12741287
isSelectable
1275-
isSelected={activeCard === key}
1288+
isSelected={activeCard === 'card-view-' + key}
1289+
hasHiddenInput
12761290
>
12771291
<CardHeader>
12781292
<img src={icons[product.icon]} alt={`${product.name} icon`} style={{ height: '50px' }} />

packages/react-core/src/demos/examples/Tabs/ModalTabs.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,12 @@ export const ModalTabs: React.FunctionComponent = () => {
9494
<Card
9595
isSelectable
9696
isSelectableRaised
97+
hasHiddenInput
9798
isCompact
9899
key={product.id}
100+
id={product.name}
99101
onClick={onCardClick(product)}
102+
onHiddenInputChange={() => onCardClick(product)()}
100103
onKeyPress={onCardKeyPress(product)}
101104
>
102105
<CardTitle>{product.name}</CardTitle>

0 commit comments

Comments
 (0)