@patternfly/react-core 4.213.2 → 4.213.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/esm/components/Card/Card.d.ts +7 -0
  3. package/dist/esm/components/Card/Card.d.ts.map +1 -1
  4. package/dist/esm/components/Card/Card.js +24 -1
  5. package/dist/esm/components/Card/Card.js.map +1 -1
  6. package/dist/esm/components/Card/CardTitle.d.ts.map +1 -1
  7. package/dist/esm/components/Card/CardTitle.js +8 -1
  8. package/dist/esm/components/Card/CardTitle.js.map +1 -1
  9. package/dist/js/components/Card/Card.d.ts +7 -0
  10. package/dist/js/components/Card/Card.d.ts.map +1 -1
  11. package/dist/js/components/Card/Card.js +24 -1
  12. package/dist/js/components/Card/Card.js.map +1 -1
  13. package/dist/js/components/Card/CardTitle.d.ts.map +1 -1
  14. package/dist/js/components/Card/CardTitle.js +8 -1
  15. package/dist/js/components/Card/CardTitle.js.map +1 -1
  16. package/dist/umd/react-core.css +475 -475
  17. package/dist/umd/react-core.js +31 -2
  18. package/dist/umd/react-core.min.css +142 -142
  19. package/dist/umd/react-core.min.js +1 -1
  20. package/package.json +5 -5
  21. package/src/components/Card/Card.tsx +53 -0
  22. package/src/components/Card/CardTitle.tsx +11 -1
  23. package/src/components/Card/__tests__/Card.test.tsx +90 -1
  24. package/src/components/Card/__tests__/CardTitle.test.tsx +13 -0
  25. package/src/components/Card/examples/Card.md +17 -0
  26. package/src/components/Card/examples/CardLegacySelectable.tsx +11 -2
  27. package/src/components/Card/examples/CardSelectable.tsx +10 -1
  28. package/src/components/Card/examples/CardSelectableA11yHighlight.tsx +61 -0
  29. package/src/demos/Card/Card.md +29 -29
  30. package/src/demos/PrimaryDetail.md +16 -2
  31. package/src/demos/examples/Tabs/ModalTabs.tsx +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-core",
3
- "version": "4.213.2",
3
+ "version": "4.213.3",
4
4
  "description": "This library provides a set of common React components for use with the PatternFly reference implementation.",
5
5
  "main": "dist/js/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -34,9 +34,9 @@
34
34
  "generate": "node scripts/copyStyles.js"
35
35
  },
36
36
  "dependencies": {
37
- "@patternfly/react-icons": "^4.64.2",
38
- "@patternfly/react-styles": "^4.63.2",
39
- "@patternfly/react-tokens": "^4.65.2",
37
+ "@patternfly/react-icons": "^4.64.3",
38
+ "@patternfly/react-styles": "^4.63.3",
39
+ "@patternfly/react-tokens": "^4.65.3",
40
40
  "focus-trap": "6.2.2",
41
41
  "react-dropzone": "9.0.0",
42
42
  "tippy.js": "5.1.2",
@@ -60,5 +60,5 @@
60
60
  "react": "^16.8.0 || ^17.0.0",
61
61
  "react-dom": "^16.8.0 || ^17.0.0"
62
62
  },
63
- "gitHead": "42579815df2b03fd9efa0082f41dd349fa04bfe4"
63
+ "gitHead": "ac145c98fef36357752cd28ffd81f3fcd4ba2cb5"
64
64
  }
@@ -36,15 +36,28 @@ export interface CardProps extends React.HTMLProps<HTMLElement>, OUIAProps {
36
36
  isPlain?: boolean;
37
37
  /** Flag indicating if a card is expanded. Modifies the card to be expandable. */
38
38
  isExpanded?: boolean;
39
+ /** Flag indicating that the card should render a hidden input to make it selectable */
40
+ hasSelectableInput?: boolean;
41
+ /** Aria label to apply to the selectable input if one is rendered */
42
+ selectableInputAriaLabel?: string;
43
+ /** Callback that executes when the selectable input is changed */
44
+ onSelectableInputChange?: (labelledBy: string, event: React.FormEvent<HTMLInputElement>) => void;
39
45
  }
40
46
 
41
47
  interface CardContextProps {
42
48
  cardId: string;
49
+ registerTitleId: (id: string) => void;
43
50
  isExpanded: boolean;
44
51
  }
45
52
 
53
+ interface AriaProps {
54
+ 'aria-label'?: string;
55
+ 'aria-labelledby'?: string;
56
+ }
57
+
46
58
  export const CardContext = React.createContext<Partial<CardContextProps>>({
47
59
  cardId: '',
60
+ registerTitleId: () => {},
48
61
  isExpanded: false
49
62
  });
50
63
 
@@ -67,10 +80,16 @@ export const Card: React.FunctionComponent<CardProps> = ({
67
80
  isPlain = false,
68
81
  ouiaId,
69
82
  ouiaSafe = true,
83
+ hasSelectableInput = false,
84
+ selectableInputAriaLabel,
85
+ onSelectableInputChange = () => {},
70
86
  ...props
71
87
  }: CardProps) => {
72
88
  const Component = component as any;
73
89
  const ouiaProps = useOUIAProps(Card.displayName, ouiaId, ouiaSafe);
90
+ const [titleId, setTitleId] = React.useState('');
91
+ const [ariaProps, setAriaProps] = React.useState<AriaProps>();
92
+
74
93
  if (isCompact && isLarge) {
75
94
  // eslint-disable-next-line no-console
76
95
  console.warn('Card: Cannot use isCompact with isLarge. Defaulting to isCompact');
@@ -90,13 +109,47 @@ export const Card: React.FunctionComponent<CardProps> = ({
90
109
  return '';
91
110
  };
92
111
 
112
+ const containsCardTitleChildRef = React.useRef(false);
113
+
114
+ const registerTitleId = (id: string) => {
115
+ setTitleId(id);
116
+ containsCardTitleChildRef.current = !!id;
117
+ };
118
+
119
+ React.useEffect(() => {
120
+ if (selectableInputAriaLabel) {
121
+ setAriaProps({ 'aria-label': selectableInputAriaLabel });
122
+ } else if (titleId) {
123
+ setAriaProps({ 'aria-labelledby': titleId });
124
+ } else if (hasSelectableInput && !containsCardTitleChildRef.current) {
125
+ setAriaProps({});
126
+ // eslint-disable-next-line no-console
127
+ console.warn(
128
+ 'If no CardTitle component is passed as a child of Card the selectableInputAriaLabel prop must be passed'
129
+ );
130
+ }
131
+ }, [hasSelectableInput, selectableInputAriaLabel, titleId]);
132
+
93
133
  return (
94
134
  <CardContext.Provider
95
135
  value={{
96
136
  cardId: id,
137
+ registerTitleId,
97
138
  isExpanded
98
139
  }}
99
140
  >
141
+ {hasSelectableInput && (
142
+ <input
143
+ className="pf-screen-reader"
144
+ id={`${id}-input`}
145
+ {...ariaProps}
146
+ type="checkbox"
147
+ checked={isSelected}
148
+ onChange={event => onSelectableInputChange(id, event)}
149
+ disabled={isDisabledRaised}
150
+ tabIndex={-1}
151
+ />
152
+ )}
100
153
  <Component
101
154
  id={id}
102
155
  className={css(
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { css } from '@patternfly/react-styles';
3
3
  import styles from '@patternfly/react-styles/css/components/Card/card';
4
+ import { CardContext } from './Card';
4
5
 
5
6
  export interface CardTitleProps extends React.HTMLProps<HTMLDivElement> {
6
7
  /** Content rendered inside the CardTitle */
@@ -17,9 +18,18 @@ export const CardTitle: React.FunctionComponent<CardTitleProps> = ({
17
18
  component = 'div',
18
19
  ...props
19
20
  }: CardTitleProps) => {
21
+ const { cardId, registerTitleId } = React.useContext(CardContext);
20
22
  const Component = component as any;
23
+ const titleId = cardId ? `${cardId}-title` : '';
24
+
25
+ React.useEffect(() => {
26
+ registerTitleId(titleId);
27
+
28
+ return () => registerTitleId('');
29
+ }, [registerTitleId, titleId]);
30
+
21
31
  return (
22
- <Component className={css(styles.cardTitle, className)} {...props}>
32
+ <Component className={css(styles.cardTitle, className)} id={titleId || undefined} {...props}>
23
33
  {children}
24
34
  </Component>
25
35
  );
@@ -1,6 +1,9 @@
1
1
  import React from 'react';
2
+
2
3
  import { render, screen } from '@testing-library/react';
3
- import { Card } from '../Card';
4
+ import '@testing-library/jest-dom';
5
+
6
+ import { Card, CardContext } from '../Card';
4
7
 
5
8
  describe('Card', () => {
6
9
  test('renders with PatternFly Core styles', () => {
@@ -118,4 +121,90 @@ describe('Card', () => {
118
121
  render(<Card isLarge isCompact />);
119
122
  expect(consoleWarnMock).toHaveBeenCalled();
120
123
  });
124
+
125
+ test('card renders with a hidden input to improve a11y when hasSelectableInput is passed', () => {
126
+ render(<Card isSelectable hasSelectableInput />);
127
+
128
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
129
+
130
+ expect(selectableInput).toBeInTheDocument();
131
+ });
132
+
133
+ test('card does not render the hidden input when hasSelectableInput is not passed', () => {
134
+ render(<Card isSelectable />);
135
+
136
+ const selectableInput = screen.queryByRole('checkbox', { hidden: true });
137
+
138
+ expect(selectableInput).not.toBeInTheDocument();
139
+ });
140
+
141
+ test('card warns when hasSelectableInput is passed without selectableInputAriaLabel or a card title', () => {
142
+ const consoleWarnMock = jest.fn();
143
+ global.console = { warn: consoleWarnMock } as any;
144
+
145
+ render(<Card isSelectable hasSelectableInput />);
146
+
147
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
148
+
149
+ expect(consoleWarnMock).toBeCalled();
150
+ expect(selectableInput).toHaveAccessibleName('');
151
+ });
152
+
153
+ test('card applies selectableInputAriaLabel to the hidden input', () => {
154
+ render(<Card isSelectable hasSelectableInput selectableInputAriaLabel="Input label test" />);
155
+
156
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
157
+
158
+ expect(selectableInput).toHaveAccessibleName('Input label test');
159
+ });
160
+
161
+ test('card applies the supplied card title as the aria label of the hidden input', () => {
162
+
163
+ // this component is used to mock the CardTitle's title registry behavior to keep this a pure unit test
164
+ const MockCardTitle = ({ children }) => {
165
+ const { registerTitleId } = React.useContext(CardContext);
166
+ const id = 'card-title-id';
167
+
168
+ React.useEffect(() => {
169
+ registerTitleId(id);
170
+ });
171
+
172
+ return <div id={id}>{children}</div>;
173
+ };
174
+
175
+ render(
176
+ <Card id="card" isSelectable hasSelectableInput>
177
+ <MockCardTitle>Card title from title component</MockCardTitle>
178
+ </Card>
179
+ );
180
+
181
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
182
+
183
+ expect(selectableInput).toHaveAccessibleName('Card title from title component');
184
+ });
185
+
186
+ test('card prioritizes selectableInputAriaLabel over card title labelling via card title', () => {
187
+
188
+ // this component is used to mock the CardTitle's title registry behavior to keep this a pure unit test
189
+ const MockCardTitle = ({ children }) => {
190
+ const { registerTitleId } = React.useContext(CardContext);
191
+ const id = 'card-title-id';
192
+
193
+ React.useEffect(() => {
194
+ registerTitleId(id);
195
+ });
196
+
197
+ return <div id={id}>{children}</div>;
198
+ };
199
+
200
+ render(
201
+ <Card id="card" isSelectable hasSelectableInput selectableInputAriaLabel="Input label test">
202
+ <MockCardTitle>Card title from title component</MockCardTitle>
203
+ </Card>
204
+ );
205
+
206
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
207
+
208
+ expect(selectableInput).toHaveAccessibleName('Input label test');
209
+ });
121
210
  });
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import { CardTitle } from '../CardTitle';
4
+ import { CardContext } from '../Card';
4
5
 
5
6
  describe('CardTitle', () => {
6
7
  test('renders with PatternFly Core styles', () => {
@@ -19,4 +20,16 @@ describe('CardTitle', () => {
19
20
  render(<CardTitle data-testid={testId} />);
20
21
  expect(screen.getByTestId(testId)).toBeInTheDocument();
21
22
  });
23
+
24
+ test('calls the registerTitleId function provided by the CardContext with the generated title id', () => {
25
+ const mockRegisterTitleId = jest.fn();
26
+
27
+ render(
28
+ <CardContext.Provider value={{ cardId: 'card', registerTitleId: mockRegisterTitleId }}>
29
+ <CardTitle>text</CardTitle>
30
+ </CardContext.Provider>
31
+ );
32
+
33
+ expect(mockRegisterTitleId).toHaveBeenCalledWith('card-title');
34
+ });
22
35
  });
@@ -71,6 +71,23 @@ import pfLogoSmall from './pf-logo-small.svg';
71
71
  ```ts file='./CardSelectable.tsx'
72
72
  ```
73
73
 
74
+ ### Selectable accessibility highlight
75
+
76
+ This example demonstrates how the `hasSelectableInput` and `onSelectableInputChange` props improve accessibility for selectable cards.
77
+
78
+ The first card sets `hasSelectableInput` to true, which renders a checkbox input that is only visible to, and navigable by, screen readers. This input communicates to assistive technology users that a card is selectable, and if so, it communicates the current selection state as well.
79
+
80
+ By default this input will have an aria-label that corresponds to the title given to the card if using the card title component. If you don't use the card title component in your selectable card, you must pass a custom aria-label for this input using the `selectableInputAriaLabel` prop.
81
+
82
+ The first card also (by passing an onchange callback to `onSelectableInputChange`) enables the selection/deselection of the associated card by checking/unchecking the checkbox input.
83
+
84
+ The second card does not set `hasSelectableInput` to true, so the input is not rendered. It does not communicate to screen reader users that it is selectable or if it is currently selected.
85
+
86
+ To best understand this example it is encouraged that you navigate both of these cards using a screen reader.
87
+
88
+ ```ts file='./CardSelectableA11yHighlight.tsx'
89
+ ```
90
+
74
91
  ### With heading element
75
92
 
76
93
  ```ts file='./CardWithHeadingElement.tsx'
@@ -31,6 +31,11 @@ export const CardLegacySelectable: React.FunctionComponent = () => {
31
31
  setSelected(newSelected);
32
32
  };
33
33
 
34
+ const onChange = (labelledById: string, _event: React.FormEvent<HTMLInputElement>) => {
35
+ const newSelected = labelledById === selected ? null : labelledById;
36
+ setSelected(newSelected);
37
+ };
38
+
34
39
  const onToggle = (isOpen: boolean, event: any) => {
35
40
  event.stopPropagation();
36
41
  setIsKebabOpen(isOpen);
@@ -65,8 +70,10 @@ export const CardLegacySelectable: React.FunctionComponent = () => {
65
70
  id="legacy-first-card"
66
71
  onKeyDown={onKeyDown}
67
72
  onClick={onClick}
73
+ onSelectableInputChange={onChange}
68
74
  isSelectable
69
75
  isSelected={selected === 'legacy-first-card'}
76
+ hasSelectableInput
70
77
  >
71
78
  <CardHeader>
72
79
  <CardActions>
@@ -80,7 +87,7 @@ export const CardLegacySelectable: React.FunctionComponent = () => {
80
87
  />
81
88
  </CardActions>
82
89
  </CardHeader>
83
- <CardTitle>First card</CardTitle>
90
+ <CardTitle>First legacy selectable card</CardTitle>
84
91
  <CardBody>This is a selectable card. Click me to select me. Click again to deselect me.</CardBody>
85
92
  </Card>
86
93
  <br />
@@ -88,10 +95,12 @@ export const CardLegacySelectable: React.FunctionComponent = () => {
88
95
  id="legacy-second-card"
89
96
  onKeyDown={onKeyDown}
90
97
  onClick={onClick}
98
+ onSelectableInputChange={onChange}
91
99
  isSelectable
92
100
  isSelected={selected === 'legacy-second-card'}
101
+ hasSelectableInput
93
102
  >
94
- <CardTitle>Second card</CardTitle>
103
+ <CardTitle>Second legacy selectable card</CardTitle>
95
104
  <CardBody>This is a selectable card. Click me to select me. Click again to deselect me.</CardBody>
96
105
  </Card>
97
106
  </>
@@ -31,6 +31,11 @@ export const CardSelectable: React.FunctionComponent = () => {
31
31
  setSelected(newSelected);
32
32
  };
33
33
 
34
+ const onChange = (labelledById: string, _event: React.FormEvent<HTMLInputElement>) => {
35
+ const newSelected = labelledById === selected ? null : labelledById;
36
+ setSelected(newSelected);
37
+ };
38
+
34
39
  const onToggle = (
35
40
  isOpen: boolean,
36
41
  event: MouseEvent | TouchEvent | KeyboardEvent | React.KeyboardEvent<any> | React.MouseEvent<HTMLButtonElement>
@@ -68,6 +73,8 @@ export const CardSelectable: React.FunctionComponent = () => {
68
73
  id="selectable-first-card"
69
74
  onKeyDown={onKeyDown}
70
75
  onClick={onClick}
76
+ hasSelectableInput
77
+ onSelectableInputChange={onChange}
71
78
  isSelectableRaised
72
79
  isSelected={selected === 'selectable-first-card'}
73
80
  >
@@ -91,6 +98,8 @@ export const CardSelectable: React.FunctionComponent = () => {
91
98
  id="selectable-second-card"
92
99
  onKeyDown={onKeyDown}
93
100
  onClick={onClick}
101
+ hasSelectableInput
102
+ onSelectableInputChange={onChange}
94
103
  isSelectableRaised
95
104
  isSelected={selected === 'selectable-second-card'}
96
105
  >
@@ -98,7 +107,7 @@ export const CardSelectable: React.FunctionComponent = () => {
98
107
  <CardBody>This is a selectable card. Click me to select me. Click again to deselect me.</CardBody>
99
108
  </Card>
100
109
  <br />
101
- <Card id="selectable-third-card" isSelectableRaised isDisabledRaised>
110
+ <Card id="selectable-third-card" isSelectableRaised isDisabledRaised hasSelectableInput>
102
111
  <CardTitle>Third card</CardTitle>
103
112
  <CardBody>This is a raised but disabled card.</CardBody>
104
113
  </Card>
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { Card, CardTitle, CardBody } from '@patternfly/react-core';
3
+
4
+ export const CardSelectableA11yHighlight: React.FunctionComponent = () => {
5
+ const [selected, setSelected] = React.useState<string>('');
6
+
7
+ const onKeyDown = (event: React.KeyboardEvent) => {
8
+ if (event.target !== event.currentTarget) {
9
+ return;
10
+ }
11
+ if ([' ', 'Enter'].includes(event.key)) {
12
+ event.preventDefault();
13
+ const newSelected = event.currentTarget.id === selected ? null : event.currentTarget.id;
14
+ setSelected(newSelected);
15
+ }
16
+ };
17
+
18
+ const onClick = (event: React.MouseEvent) => {
19
+ const newSelected = event.currentTarget.id === selected ? null : event.currentTarget.id;
20
+ setSelected(newSelected);
21
+ };
22
+
23
+ const onChange = (labelledById: string, _event: React.FormEvent<HTMLInputElement>) => {
24
+ const newSelected = labelledById === selected ? null : labelledById;
25
+ setSelected(newSelected);
26
+ };
27
+
28
+ return (
29
+ <React.Fragment>
30
+ <Card
31
+ id="selectable-first-card"
32
+ onKeyDown={onKeyDown}
33
+ onClick={onClick}
34
+ hasSelectableInput
35
+ onSelectableInputChange={onChange}
36
+ isSelectableRaised
37
+ isSelected={selected === 'selectable-first-card'}
38
+ >
39
+ <CardTitle>Selectable card with proper accessibility considerations</CardTitle>
40
+ <CardBody>
41
+ When using a screen reader a checkbox will become navigable that indicates this card is selectable and
42
+ communicate if it is currently selected.
43
+ </CardBody>
44
+ </Card>
45
+ <br />
46
+ <Card
47
+ id="selectable-second-card"
48
+ onKeyDown={onKeyDown}
49
+ onClick={onClick}
50
+ isSelectableRaised
51
+ isSelected={selected === 'selectable-second-card'}
52
+ >
53
+ <CardTitle>Selectable card without proper accessibility considerations</CardTitle>
54
+ <CardBody>
55
+ When using a screen reader there are no indications that this card is selectable or if it is currently
56
+ selected.
57
+ </CardBody>
58
+ </Card>
59
+ </React.Fragment>
60
+ );
61
+ };
@@ -197,7 +197,7 @@ class CardViewBasic extends React.Component {
197
197
  });
198
198
  }
199
199
  };
200
-
200
+
201
201
  this.onKeyDown = (event, productId) => {
202
202
  console.log(productId);
203
203
  if (event.target !== event.currentTarget) {
@@ -206,28 +206,30 @@ class CardViewBasic extends React.Component {
206
206
  if ([' ', 'Enter'].includes(event.key)) {
207
207
  event.preventDefault();
208
208
  this.setState(prevState => {
209
- return prevState.selectedItems.includes(productId*1) ?
210
- {
211
- selectedItems: [...prevState.selectedItems.filter(id => productId * 1 != id)],
212
- areAllSelected: false
213
- } : {
214
- selectedItems: [...prevState.selectedItems, productId * 1],
215
- areAllSelected: prevState.totalItemCount === prevState.selectedItems.length + 1
216
- };
209
+ return prevState.selectedItems.includes(productId * 1)
210
+ ? {
211
+ selectedItems: [...prevState.selectedItems.filter(id => productId * 1 != id)],
212
+ areAllSelected: false
213
+ }
214
+ : {
215
+ selectedItems: [...prevState.selectedItems, productId * 1],
216
+ areAllSelected: prevState.totalItemCount === prevState.selectedItems.length + 1
217
+ };
217
218
  });
218
219
  }
219
220
  };
220
-
221
+
221
222
  this.onClick = productId => {
222
223
  this.setState(prevState => {
223
- return prevState.selectedItems.includes(productId*1) ?
224
- {
225
- selectedItems: [...prevState.selectedItems.filter(id => productId * 1 != id)],
226
- areAllSelected: false
227
- } : {
228
- selectedItems: [...prevState.selectedItems, productId * 1],
229
- areAllSelected: prevState.totalItemCount === prevState.selectedItems.length + 1
230
- };
224
+ return prevState.selectedItems.includes(productId * 1)
225
+ ? {
226
+ selectedItems: [...prevState.selectedItems.filter(id => productId * 1 != id)],
227
+ areAllSelected: false
228
+ }
229
+ : {
230
+ selectedItems: [...prevState.selectedItems, productId * 1],
231
+ areAllSelected: prevState.totalItemCount === prevState.selectedItems.length + 1
232
+ };
231
233
  });
232
234
  };
233
235
  }
@@ -559,12 +561,8 @@ class CardViewBasic extends React.Component {
559
561
  </Toolbar>
560
562
  </PageSection>
561
563
  <PageSection isFilled>
562
- <Gallery hasGutter>
563
- <Card
564
- isSelectable
565
- selectableVariant="raised"
566
- isCompact
567
- >
564
+ <Gallery hasGutter aria-label="Selectable card container">
565
+ <Card isSelectableRaised isCompact>
568
566
  <Bullseye>
569
567
  <EmptyState variant={EmptyStateVariant.xs}>
570
568
  <EmptyStateIcon icon={PlusCircleIcon} />
@@ -578,13 +576,15 @@ class CardViewBasic extends React.Component {
578
576
  </Bullseye>
579
577
  </Card>
580
578
  {filtered.map((product, key) => (
581
- <Card
582
- isSelectable
583
- selectableVariant="raised"
584
- isCompact
579
+ <Card
580
+ isSelectableRaised
581
+ hasSelectableInput
582
+ isCompact
585
583
  key={product.name}
586
- onKeyDown={(e) => this.onKeyDown(e, product.id)}
584
+ id={product.name.replace(/ /g, '-')}
585
+ onKeyDown={e => this.onKeyDown(e, product.id)}
587
586
  onClick={() => this.onClick(product.id)}
587
+ onSelectableInputChange={() => this.onClick(product.id)}
588
588
  isSelected={selectedItems.includes(product.id)}
589
589
  >
590
590
  <CardHeader>
@@ -1100,6 +1100,7 @@ class PrimaryDetailCardView extends React.Component {
1100
1100
  return;
1101
1101
  }
1102
1102
  if ([13, 32].includes(event.keyCode)) {
1103
+ event.preventDefault();
1103
1104
  const newSelected = event.currentTarget.id;
1104
1105
  this.setState({
1105
1106
  activeCard: newSelected,
@@ -1121,6 +1122,17 @@ class PrimaryDetailCardView extends React.Component {
1121
1122
  });
1122
1123
  };
1123
1124
 
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
+
1124
1136
  this.onPerPageSelect = (_evt, perPage) => {
1125
1137
  this.setState({ page: 1, perPage });
1126
1138
  };
@@ -1262,7 +1274,7 @@ class PrimaryDetailCardView extends React.Component {
1262
1274
  };
1263
1275
 
1264
1276
  const drawerContent = (
1265
- <Gallery hasGutter>
1277
+ <Gallery hasGutter aria-label="Selectable card container">
1266
1278
  {filtered.map((product, key) => (
1267
1279
  <React.Fragment>
1268
1280
  <Card
@@ -1271,8 +1283,10 @@ class PrimaryDetailCardView extends React.Component {
1271
1283
  id={'card-view-' + key}
1272
1284
  onKeyDown={this.onKeyDown}
1273
1285
  onClick={this.onCardClick}
1286
+ onSelectableInputChange={this.onChange}
1274
1287
  isSelectable
1275
- isSelected={activeCard === key}
1288
+ isSelected={activeCard === 'card-view-' + key}
1289
+ hasSelectableInput
1276
1290
  >
1277
1291
  <CardHeader>
1278
1292
  <img src={icons[product.icon]} alt={`${product.name} icon`} style={{ height: '50px' }} />
@@ -89,14 +89,17 @@ export const ModalTabs: React.FunctionComponent = () => {
89
89
  </TextContent>
90
90
  </PageSection>
91
91
  <PageSection isFilled>
92
- <Gallery hasGutter>
92
+ <Gallery hasGutter aria-label="Selectable card container">
93
93
  {products.map(product => (
94
94
  <Card
95
95
  isSelectable
96
96
  isSelectableRaised
97
+ hasSelectableInput
97
98
  isCompact
98
99
  key={product.id}
100
+ id={product.name.replace(/ /g, '-')}
99
101
  onClick={onCardClick(product)}
102
+ onSelectableInputChange={() => onCardClick(product)()}
100
103
  onKeyPress={onCardKeyPress(product)}
101
104
  >
102
105
  <CardTitle>{product.name}</CardTitle>