@patternfly/react-core 4.221.1 → 4.221.2

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 (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/esm/components/DataList/DataList.d.ts +9 -0
  3. package/dist/esm/components/DataList/DataList.d.ts.map +1 -1
  4. package/dist/esm/components/DataList/DataList.js +3 -2
  5. package/dist/esm/components/DataList/DataList.js.map +1 -1
  6. package/dist/esm/components/DataList/DataListItem.d.ts +2 -0
  7. package/dist/esm/components/DataList/DataListItem.d.ts.map +1 -1
  8. package/dist/esm/components/DataList/DataListItem.js +13 -6
  9. package/dist/esm/components/DataList/DataListItem.js.map +1 -1
  10. package/dist/esm/components/Select/Select.d.ts +2 -0
  11. package/dist/esm/components/Select/Select.d.ts.map +1 -1
  12. package/dist/esm/components/Select/Select.js +8 -7
  13. package/dist/esm/components/Select/Select.js.map +1 -1
  14. package/dist/js/components/DataList/DataList.d.ts +9 -0
  15. package/dist/js/components/DataList/DataList.d.ts.map +1 -1
  16. package/dist/js/components/DataList/DataList.js +3 -2
  17. package/dist/js/components/DataList/DataList.js.map +1 -1
  18. package/dist/js/components/DataList/DataListItem.d.ts +2 -0
  19. package/dist/js/components/DataList/DataListItem.d.ts.map +1 -1
  20. package/dist/js/components/DataList/DataListItem.js +13 -6
  21. package/dist/js/components/DataList/DataListItem.js.map +1 -1
  22. package/dist/js/components/Select/Select.d.ts +2 -0
  23. package/dist/js/components/Select/Select.d.ts.map +1 -1
  24. package/dist/js/components/Select/Select.js +8 -7
  25. package/dist/js/components/Select/Select.js.map +1 -1
  26. package/dist/umd/react-core.css +83 -83
  27. package/dist/umd/react-core.js +24 -15
  28. package/dist/umd/react-core.min.js +4 -4
  29. package/package.json +5 -5
  30. package/src/components/DataList/DataList.tsx +12 -0
  31. package/src/components/DataList/DataListItem.tsx +32 -3
  32. package/src/components/DataList/__tests__/DataList.test.tsx +130 -0
  33. package/src/components/DataList/examples/DataList.md +1 -0
  34. package/src/components/DataList/examples/DataListSelectableRows.tsx +5 -0
  35. package/src/components/Select/Select.tsx +8 -2
  36. package/src/demos/PrimaryDetail.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-core",
3
- "version": "4.221.1",
3
+ "version": "4.221.2",
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.72.1",
38
- "@patternfly/react-styles": "^4.71.1",
39
- "@patternfly/react-tokens": "^4.73.1",
37
+ "@patternfly/react-icons": "^4.72.2",
38
+ "@patternfly/react-styles": "^4.71.2",
39
+ "@patternfly/react-tokens": "^4.73.2",
40
40
  "focus-trap": "6.9.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": "185f5001db15c2ef7690bb60e9876dfbd758c4d9"
63
+ "gitHead": "eeeefa74fca5d5c4739cc6113028a2cca51f0fb7"
64
64
  }
@@ -20,6 +20,13 @@ export enum DataListWrapModifier {
20
20
  breakWord = 'breakWord'
21
21
  }
22
22
 
23
+ export interface SelectableRowObject {
24
+ /** Determines if only one of the selectable rows should be selectable at a time */
25
+ type: 'multiple' | 'single';
26
+ /** Callback that executes when the screen reader accessible element receives a change event */
27
+ onChange: (id: string, event: React.FormEvent<HTMLInputElement>) => void;
28
+ }
29
+
23
30
  export interface DataListProps extends Omit<React.HTMLProps<HTMLUListElement>, 'onDragStart' | 'ref'> {
24
31
  /** Content rendered inside the DataList list */
25
32
  children?: React.ReactNode;
@@ -47,6 +54,8 @@ export interface DataListProps extends Omit<React.HTMLProps<HTMLUListElement>, '
47
54
  wrapModifier?: DataListWrapModifier | 'nowrap' | 'truncate' | 'breakWord';
48
55
  /** @deprecated Order of items in a draggable DataList */
49
56
  itemOrder?: string[];
57
+ /** @beta Object that causes the data list to render hidden inputs which improve selectable item a11y */
58
+ selectableRow?: SelectableRowObject;
50
59
  }
51
60
 
52
61
  interface DataListState {
@@ -60,6 +69,7 @@ interface DataListContextProps {
60
69
  isSelectable: boolean;
61
70
  selectedDataListItemId: string;
62
71
  updateSelectedDataListItem: (id: string) => void;
72
+ selectableRow?: SelectableRowObject;
63
73
  isDraggable: boolean;
64
74
  dragStart: (e: React.DragEvent) => void;
65
75
  dragEnd: (e: React.DragEvent) => void;
@@ -308,6 +318,7 @@ export class DataList extends React.Component<DataListProps, DataListState> {
308
318
  onDragFinish,
309
319
  gridBreakpoint,
310
320
  itemOrder,
321
+ selectableRow,
311
322
  /* eslint-enable @typescript-eslint/no-unused-vars */
312
323
  ...props
313
324
  } = this.props;
@@ -330,6 +341,7 @@ export class DataList extends React.Component<DataListProps, DataListState> {
330
341
  isSelectable,
331
342
  selectedDataListItemId,
332
343
  updateSelectedDataListItem,
344
+ selectableRow,
333
345
  isDraggable: this.html5DragDrop,
334
346
  dragStart: this.dragStart,
335
347
  dragEnd: this.dragEnd,
@@ -16,6 +16,8 @@ export interface DataListItemProps extends Omit<React.HTMLProps<HTMLLIElement>,
16
16
  'aria-labelledby': string;
17
17
  /** Unique id for the DataList item */
18
18
  id?: string;
19
+ /** @beta Aria label to apply to the selectable input if one is rendered */
20
+ selectableInputAriaLabel?: string;
19
21
  }
20
22
 
21
23
  export interface DataListItemChildProps {
@@ -52,13 +54,22 @@ export class DataListItem extends React.Component<DataListItemProps> {
52
54
  'aria-labelledby': ''
53
55
  };
54
56
  render() {
55
- const { children, isExpanded, className, id, 'aria-labelledby': ariaLabelledBy, ...props } = this.props;
57
+ const {
58
+ children,
59
+ isExpanded,
60
+ className,
61
+ id,
62
+ 'aria-labelledby': ariaLabelledBy,
63
+ selectableInputAriaLabel,
64
+ ...props
65
+ } = this.props;
56
66
  return (
57
67
  <DataListContext.Consumer>
58
68
  {({
59
69
  isSelectable,
60
70
  selectedDataListItemId,
61
71
  updateSelectedDataListItem,
72
+ selectableRow,
62
73
  isDraggable,
63
74
  dragStart,
64
75
  dragEnd,
@@ -98,6 +109,14 @@ export class DataListItem extends React.Component<DataListItemProps> {
98
109
  onDragStart: dragStart
99
110
  };
100
111
 
112
+ const isSelected = selectedDataListItemId === id;
113
+
114
+ const selectableInputAriaProps = selectableInputAriaLabel
115
+ ? { 'aria-label': selectableInputAriaLabel }
116
+ : { 'aria-labelledby': ariaLabelledBy };
117
+
118
+ const selectableInputType = selectableRow?.type === 'multiple' ? 'checkbox' : 'radio';
119
+
101
120
  return (
102
121
  <li
103
122
  id={id}
@@ -105,15 +124,25 @@ export class DataListItem extends React.Component<DataListItemProps> {
105
124
  styles.dataListItem,
106
125
  isExpanded && styles.modifiers.expanded,
107
126
  isSelectable && styles.modifiers.selectable,
108
- selectedDataListItemId && selectedDataListItemId === id && styles.modifiers.selected,
127
+ selectedDataListItemId && isSelected && styles.modifiers.selected,
109
128
  className
110
129
  )}
111
130
  aria-labelledby={ariaLabelledBy}
112
131
  {...(isSelectable && { tabIndex: 0, onClick: selectDataListItem, onKeyDown })}
113
- {...(isSelectable && selectedDataListItemId === id && { 'aria-selected': true })}
132
+ {...(isSelectable && isSelected && { 'aria-selected': true })}
114
133
  {...props}
115
134
  {...dragProps}
116
135
  >
136
+ {selectableRow && (
137
+ <input
138
+ className="pf-screen-reader"
139
+ type={selectableInputType}
140
+ checked={isSelected}
141
+ onChange={event => selectableRow.onChange(id, event)}
142
+ tabIndex={-1}
143
+ {...selectableInputAriaProps}
144
+ />
145
+ )}
117
146
  {React.Children.map(
118
147
  children,
119
148
  child =>
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
4
5
 
5
6
  import { DataList } from '../DataList';
6
7
  import { DataListItem } from '../DataListItem';
@@ -47,6 +48,135 @@ describe('DataList', () => {
47
48
  expect(asFragment()).toMatchSnapshot();
48
49
  });
49
50
 
51
+ test('List renders with a hidden input to improve a11y when selectableRow is passed', () => {
52
+ render(
53
+ <DataList aria-label="this is a simple list" selectableRow={{ type: 'multiple', onChange: () => {} }}>
54
+ <DataListItem>
55
+ <DataListItemRow aria-labelledby="test-id">
56
+ <p id="test-id">Test</p>
57
+ </DataListItemRow>
58
+ </DataListItem>
59
+ </DataList>
60
+ );
61
+
62
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
63
+
64
+ expect(selectableInput).toBeInTheDocument();
65
+ });
66
+
67
+ test('List does not render with a hidden input to improve a11y when selectableRow is not passed', () => {
68
+ render(
69
+ <DataList aria-label="this is a simple list">
70
+ <DataListItem>
71
+ <DataListItemRow aria-labelledby="test-id">
72
+ <p id="test-id">Test</p>
73
+ </DataListItemRow>
74
+ </DataListItem>
75
+ </DataList>
76
+ );
77
+
78
+ const selectableInput = screen.queryByRole('checkbox', { hidden: true });
79
+
80
+ expect(selectableInput).not.toBeInTheDocument();
81
+ });
82
+
83
+ test('List hidden input renders as a radio when selectableRow.type is radio', () => {
84
+ render(
85
+ <DataList aria-label="this is a simple list" selectableRow={{ type: 'single', onChange: () => {} }}>
86
+ <DataListItem>
87
+ <DataListItemRow aria-labelledby="test-id">
88
+ <p id="test-id">Test</p>
89
+ </DataListItemRow>
90
+ </DataListItem>
91
+ </DataList>
92
+ );
93
+
94
+ const selectableRadioInput = screen.getByRole('radio', { hidden: true });
95
+ const selectableCheckboxInput = screen.queryByRole('checkbox', { hidden: true });
96
+
97
+ expect(selectableRadioInput).toBeInTheDocument();
98
+ expect(selectableCheckboxInput).not.toBeInTheDocument();
99
+ });
100
+
101
+ test('List calls selectableRow.onChange when the selectable input changes', () => {
102
+ const mock = jest.fn();
103
+
104
+ render(
105
+ <DataList aria-label="this is a simple list" selectableRow={{ type: 'multiple', onChange: mock }}>
106
+ <DataListItem>
107
+ <DataListItemRow aria-labelledby="test-id">
108
+ <p id="test-id">Test</p>
109
+ </DataListItemRow>
110
+ </DataListItem>
111
+ </DataList>
112
+ );
113
+
114
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
115
+ userEvent.click(selectableInput);
116
+
117
+ expect(mock).toHaveBeenCalled();
118
+ });
119
+
120
+ test('List does not call selectableRow.onChange when the selectable input is not changed', () => {
121
+ const mock = jest.fn();
122
+
123
+ render(
124
+ <DataList aria-label="this is a simple list" selectableRow={{ type: 'multiple', onChange: mock }}>
125
+ <DataListItem>
126
+ <DataListItemRow aria-labelledby="test-id">
127
+ <p id="test-id">Test</p>
128
+ </DataListItemRow>
129
+ </DataListItem>
130
+ </DataList>
131
+ );
132
+
133
+ expect(mock).not.toHaveBeenCalled();
134
+ });
135
+
136
+ test('Item applies selectableInputAriaLabel to the hidden input', () => {
137
+ render(
138
+ <DataList aria-label="this is a simple list" selectableRow={{ type: 'multiple', onChange: () => {} }}>
139
+ <DataListItem selectableInputAriaLabel="Data list item label test">
140
+ <DataListItemRow aria-labelledby="test-id">
141
+ <p id="test-id">Test</p>
142
+ </DataListItemRow>
143
+ </DataListItem>
144
+ </DataList>
145
+ );
146
+
147
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
148
+
149
+ expect(selectableInput).toHaveAccessibleName('Data list item label test');
150
+ });
151
+
152
+ test('Item defaults to labelling its input using its aria-labelledby prop', () => {
153
+ render(
154
+ <DataList aria-label="this is a simple list" selectableRow={{ type: 'multiple', onChange: () => {} }}>
155
+ <DataListItem aria-labelledby="test-id">
156
+ <p id="test-id">Test cell content</p>
157
+ </DataListItem>
158
+ </DataList>
159
+ );
160
+
161
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
162
+
163
+ expect(selectableInput).toHaveAccessibleName('Test cell content');
164
+ });
165
+
166
+ test('Item prioritizes selectableInputAriaLabel over aria-labelledby prop', () => {
167
+ render(
168
+ <DataList aria-label="this is a simple list" selectableRow={{ type: 'multiple', onChange: () => {} }}>
169
+ <DataListItem aria-labelledby="test-id" selectableInputAriaLabel="Data list item label test">
170
+ <p id="test-id">Test cell content</p>
171
+ </DataListItem>
172
+ </DataList>
173
+ );
174
+
175
+ const selectableInput = screen.getByRole('checkbox', { hidden: true });
176
+
177
+ expect(selectableInput).toHaveAccessibleName('Data list item label test');
178
+ });
179
+
50
180
  test('Item default', () => {
51
181
  const { asFragment } = render(
52
182
  <DataListItem key="item-id-1" aria-labelledby="item-1">
@@ -5,6 +5,7 @@ cssPrefix: pf-c-data-list
5
5
  propComponents:
6
6
  [
7
7
  'DataList',
8
+ 'SelectableRowObject',
8
9
  'DataListAction',
9
10
  'DataListCell',
10
11
  'DataListCheck',
@@ -37,12 +37,17 @@ export const DataListSelectableRows: React.FunctionComponent = () => {
37
37
  setSelectedDataListItemId(id);
38
38
  };
39
39
 
40
+ const handleInputChange = (id: string, _event: React.FormEvent<HTMLInputElement>) => {
41
+ setSelectedDataListItemId(id);
42
+ };
43
+
40
44
  return (
41
45
  <React.Fragment>
42
46
  <DataList
43
47
  aria-label="selectable data list example"
44
48
  selectedDataListItemId={selectedDataListItemId}
45
49
  onSelectDataListItem={onSelectDataListItem}
50
+ selectableRow={{ type: 'single', onChange: handleInputChange }}
46
51
  >
47
52
  <DataListItem aria-labelledby="selectable-action-item1" id="item1">
48
53
  <DataListItemRow>
@@ -103,6 +103,8 @@ export interface SelectProps
103
103
  'aria-invalid'?: boolean;
104
104
  /** Label for input field of type ahead select variants */
105
105
  typeAheadAriaLabel?: string;
106
+ /** Id of div for input field of type ahead select variants */
107
+ typeAheadAriaDescribedby?: string;
106
108
  /** Label for clear selection button of type ahead select variants */
107
109
  clearSelectionsAriaLabel?: string;
108
110
  /** Label for toggle of type ahead select variants */
@@ -222,6 +224,7 @@ export class Select extends React.Component<SelectProps & OUIAProps, SelectState
222
224
  'aria-describedby': '',
223
225
  'aria-invalid': false,
224
226
  typeAheadAriaLabel: '',
227
+ typeAheadAriaDescribedby: '',
225
228
  clearSelectionsAriaLabel: 'Clear all',
226
229
  toggleAriaLabel: 'Options menu',
227
230
  removeSelectionAriaLabel: 'Remove',
@@ -943,6 +946,7 @@ export class Select extends React.Component<SelectProps & OUIAProps, SelectState
943
946
  validated,
944
947
  selections: selectionsProp,
945
948
  typeAheadAriaLabel,
949
+ typeAheadAriaDescribedby,
946
950
  clearSelectionsAriaLabel,
947
951
  toggleAriaLabel,
948
952
  removeSelectionAriaLabel,
@@ -1247,7 +1251,7 @@ export class Select extends React.Component<SelectProps & OUIAProps, SelectState
1247
1251
  className
1248
1252
  )}
1249
1253
  {...(width && { style: { width } })}
1250
- {...(validated !== ValidatedOptions.default && { 'aria-describedby': ariaDescribedby })}
1254
+ {...(ariaDescribedby && { 'aria-describedby': ariaDescribedby })}
1251
1255
  {...(validated !== ValidatedOptions.default && { 'aria-invalid': ariaInvalid })}
1252
1256
  >
1253
1257
  {isOpen && menuContainer}
@@ -1268,7 +1272,7 @@ export class Select extends React.Component<SelectProps & OUIAProps, SelectState
1268
1272
  ref={this.parentRef}
1269
1273
  {...getOUIAProps(Select.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId, ouiaSafe)}
1270
1274
  {...(width && { style: { width } })}
1271
- {...(validated !== ValidatedOptions.default && { 'aria-describedby': ariaDescribedby })}
1275
+ {...(ariaDescribedby && { 'aria-describedby': ariaDescribedby })}
1272
1276
  {...(validated !== ValidatedOptions.default && { 'aria-invalid': ariaInvalid })}
1273
1277
  >
1274
1278
  <SelectToggle
@@ -1337,6 +1341,7 @@ export class Select extends React.Component<SelectProps & OUIAProps, SelectState
1337
1341
  aria-activedescendant={typeaheadActiveChild && typeaheadActiveChild.id}
1338
1342
  id={`${selectToggleId}-select-typeahead`}
1339
1343
  aria-label={typeAheadAriaLabel}
1344
+ {...(typeAheadAriaDescribedby && { 'aria-describedby': typeAheadAriaDescribedby })}
1340
1345
  placeholder={placeholderText as string}
1341
1346
  value={
1342
1347
  typeaheadInputValue !== null
@@ -1365,6 +1370,7 @@ export class Select extends React.Component<SelectProps & OUIAProps, SelectState
1365
1370
  id={`${selectToggleId}-select-multi-typeahead-typeahead`}
1366
1371
  aria-label={typeAheadAriaLabel}
1367
1372
  aria-invalid={validated === ValidatedOptions.error}
1373
+ {...(typeAheadAriaDescribedby && { 'aria-describedby': typeAheadAriaDescribedby })}
1368
1374
  placeholder={placeholderText as string}
1369
1375
  value={typeaheadInputValue !== null ? typeaheadInputValue : ''}
1370
1376
  type="text"
@@ -1274,7 +1274,7 @@ class PrimaryDetailCardView extends React.Component {
1274
1274
  };
1275
1275
 
1276
1276
  const drawerContent = (
1277
- <Gallery hasGutter aria-label="Selectable card container">
1277
+ <Gallery hasGutter role='region' aria-label="Selectable card container">
1278
1278
  {filtered.map((product, key) => (
1279
1279
  <React.Fragment>
1280
1280
  <Card