@patternfly/react-core 4.237.11 → 4.238.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-core",
3
- "version": "4.237.11",
3
+ "version": "4.238.0",
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",
@@ -44,9 +44,9 @@
44
44
  "generate": "node scripts/copyStyles.js"
45
45
  },
46
46
  "dependencies": {
47
- "@patternfly/react-icons": "^4.88.11",
48
- "@patternfly/react-styles": "^4.87.11",
49
- "@patternfly/react-tokens": "^4.89.11",
47
+ "@patternfly/react-icons": "^4.89.0",
48
+ "@patternfly/react-styles": "^4.88.0",
49
+ "@patternfly/react-tokens": "^4.90.0",
50
50
  "focus-trap": "6.9.2",
51
51
  "react-dropzone": "9.0.0",
52
52
  "tippy.js": "5.1.2",
@@ -70,5 +70,5 @@
70
70
  "react": "^16.8.0 || ^17.0.0",
71
71
  "react-dom": "^16.8.0 || ^17.0.0"
72
72
  },
73
- "gitHead": "d5c28cc90817bb41bf6062071daaa1e435f93b87"
73
+ "gitHead": "844856ede5d24e9c60242140404ef95c4acc50b1"
74
74
  }
@@ -21,6 +21,15 @@ export interface SearchAttribute {
21
21
  display: React.ReactNode;
22
22
  }
23
23
 
24
+ export interface ExpandableInput {
25
+ /** Flag to indicate if the search input is expanded */
26
+ isExpanded: boolean;
27
+ /** Callback function to toggle the expandable search input */
28
+ onToggleExpand: (isExpanded: boolean, event: React.SyntheticEvent<HTMLButtonElement>) => void;
29
+ /** An accessible label for the expandable search input toggle */
30
+ toggleAriaLabel: string;
31
+ }
32
+
24
33
  export interface SearchInputProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onChange' | 'results' | 'ref'> {
25
34
  /** Additional classes added to the banner */
26
35
  className?: string;
@@ -38,7 +47,8 @@ export interface SearchInputProps extends Omit<React.HTMLProps<HTMLDivElement>,
38
47
  onChange?: (value: string, event: React.FormEvent<HTMLInputElement>) => void;
39
48
  /** A suggestion for autocompleting */
40
49
  hint?: string;
41
-
50
+ /** Object that makes the search input expandable/collapsible */
51
+ expandableInput?: ExpandableInput;
42
52
  /** A callback for when the search button clicked changes */
43
53
  onSearch?: (
44
54
  value: string,
@@ -72,7 +82,6 @@ export interface SearchInputProps extends Omit<React.HTMLProps<HTMLDivElement>,
72
82
  /** The number of search results returned. Either a total number of results,
73
83
  * or a string representing the current result over the total number of results. i.e. "1 / 5" */
74
84
  resultsCount?: number | string;
75
-
76
85
  /** Array of attribute values used for dynamically generated advanced search */
77
86
  attributes?: string[] | SearchAttribute[];
78
87
  /* Additional elements added after the attributes in the form.
@@ -112,6 +121,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
112
121
  onNextClick,
113
122
  onPreviousClick,
114
123
  innerRef,
124
+ expandableInput,
115
125
  'aria-label': ariaLabel = 'Search input',
116
126
  resetButtonLabel = 'Reset',
117
127
  openMenuButtonAriaLabel = 'Open advanced search',
@@ -130,6 +140,23 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
130
140
  const searchInputRef = React.useRef(null);
131
141
  const ref = React.useRef(null);
132
142
  const searchInputInputRef = innerRef || ref;
143
+ const searchInputExpandableToggleRef = React.useRef(null);
144
+ const [focusAfterExpandChange, setFocusAfterExpandChange] = React.useState(false);
145
+
146
+ const { isExpanded, onToggleExpand, toggleAriaLabel } = expandableInput || {};
147
+
148
+ React.useEffect(() => {
149
+ // this effect and the focusAfterExpandChange variable are needed to focus the input/toggle as needed when the
150
+ // expansion toggle is fired without focusing on mount
151
+ if (!focusAfterExpandChange) {
152
+ return;
153
+ } else if (isExpanded) {
154
+ searchInputInputRef?.current?.focus();
155
+ } else {
156
+ searchInputExpandableToggleRef?.current?.focus();
157
+ }
158
+ setFocusAfterExpandChange(false);
159
+ }, [focusAfterExpandChange, isExpanded, searchInputInputRef, searchInputExpandableToggleRef]);
133
160
 
134
161
  React.useEffect(() => {
135
162
  setSearchValue(value);
@@ -202,7 +229,16 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
202
229
  }
203
230
  };
204
231
 
205
- const buildSearchTextInputGroup = ({ ...searchInputProps } = {}) => (
232
+ const onExpandHandler = (event: React.SyntheticEvent<HTMLButtonElement>) => {
233
+ setSearchValue('');
234
+ onToggleExpand(isExpanded, event);
235
+ setFocusAfterExpandChange(true);
236
+ };
237
+
238
+ const renderUtilities =
239
+ value && (resultsCount || (!!onNextClick && !!onPreviousClick) || (!!onClear && !expandableInput));
240
+
241
+ const buildTextInputGroup = ({ ...searchInputProps } = {}) => (
206
242
  <TextInputGroup isDisabled={isDisabled} {...searchInputProps}>
207
243
  <TextInputGroupMain
208
244
  hint={hint}
@@ -214,7 +250,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
214
250
  onKeyDown={onEnter}
215
251
  onChange={onChangeHandler}
216
252
  />
217
- {value && (
253
+ {renderUtilities && (
218
254
  <TextInputGroupUtilities>
219
255
  {resultsCount && <Badge isRead>{resultsCount}</Badge>}
220
256
  {!!onNextClick && !!onPreviousClick && (
@@ -237,7 +273,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
237
273
  </Button>
238
274
  </div>
239
275
  )}
240
- {!!onClear && (
276
+ {!!onClear && !expandableInput && (
241
277
  <Button
242
278
  variant={ButtonVariant.plain}
243
279
  isDisabled={isDisabled}
@@ -252,9 +288,35 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
252
288
  </TextInputGroup>
253
289
  );
254
290
 
291
+ const expandableToggle = (
292
+ <Button
293
+ variant={ButtonVariant.plain}
294
+ aria-label={toggleAriaLabel}
295
+ aria-expanded={isExpanded}
296
+ icon={isExpanded ? <TimesIcon /> : <SearchIcon />}
297
+ onClick={onExpandHandler}
298
+ ref={searchInputExpandableToggleRef}
299
+ />
300
+ );
301
+
302
+ const buildExpandableSearchInput = ({ ...searchInputProps } = {}) => (
303
+ <InputGroup {...searchInputProps}>
304
+ {buildTextInputGroup()}
305
+ {expandableToggle}
306
+ </InputGroup>
307
+ );
308
+
309
+ const buildSearchTextInputGroup = ({ ...searchInputProps } = {}) => {
310
+ if (expandableInput) {
311
+ return buildExpandableSearchInput({ ...searchInputProps });
312
+ }
313
+
314
+ return buildTextInputGroup({ ...searchInputProps });
315
+ };
316
+
255
317
  const buildSearchTextInputGroupWithExtraButtons = ({ ...searchInputProps } = {}) => (
256
318
  <InputGroup {...searchInputProps}>
257
- {buildSearchTextInputGroup()}
319
+ {buildTextInputGroup()}
258
320
  {(attributes.length > 0 || onToggleAdvancedSearch) && (
259
321
  <Button
260
322
  className={isSearchMenuOpen && 'pf-m-expanded'}
@@ -278,6 +340,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
278
340
  <ArrowRightIcon />
279
341
  </Button>
280
342
  )}
343
+ {expandableInput && expandableToggle}
281
344
  </InputGroup>
282
345
  );
283
346
 
@@ -287,6 +350,10 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
287
350
  innerRef: searchInputRef
288
351
  };
289
352
 
353
+ if (!!expandableInput && !isExpanded) {
354
+ return <InputGroup {...searchInputProps}>{expandableToggle}</InputGroup>;
355
+ }
356
+
290
357
  if (!!onSearch || attributes.length > 0 || !!onToggleAdvancedSearch) {
291
358
  if (attributes.length > 0) {
292
359
  const AdvancedSearch = (
@@ -333,7 +400,6 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
333
400
 
334
401
  return appendTo !== 'inline' ? AdvancedSearchWithPopper : AdvancedSearchInline;
335
402
  }
336
-
337
403
  return buildSearchTextInputGroupWithExtraButtons({ ...searchInputProps });
338
404
  }
339
405
  return buildSearchTextInputGroup(searchInputProps);
@@ -167,3 +167,85 @@ describe('SearchInput', () => {
167
167
  expect(document.body).toContainElement(screen.getByText('test'));
168
168
  });
169
169
  });
170
+
171
+ test('expandable toggle does not render by default', () => {
172
+ render(<SearchInput />);
173
+
174
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
175
+ });
176
+
177
+ test('expandable toggle renders when a value is passed for expandableProps', () => {
178
+ render(
179
+ <SearchInput expandableInput={{ isExpanded: false, onToggleExpand: () => {}, toggleAriaLabel: 'Test label' }} />
180
+ );
181
+
182
+ expect(screen.getByRole('button')).toBeVisible();
183
+ });
184
+
185
+ test('expandable toggle is not aria-expanded when isExpanded is not true', () => {
186
+ render(
187
+ <SearchInput expandableInput={{ isExpanded: false, onToggleExpand: () => {}, toggleAriaLabel: 'Test label' }} />
188
+ );
189
+
190
+ expect(screen.getByRole('button', { expanded: false })).toBeVisible();
191
+ });
192
+
193
+ test('expandable toggle is aria-expanded when isExpanded is true', () => {
194
+ render(
195
+ <SearchInput expandableInput={{ isExpanded: true, onToggleExpand: () => {}, toggleAriaLabel: 'Test label' }} />
196
+ );
197
+
198
+ expect(screen.getByRole('button', { expanded: true })).toBeVisible();
199
+ });
200
+
201
+ test('text input is not rendered when isExpanded is not true', () => {
202
+ render(
203
+ <SearchInput expandableInput={{ isExpanded: false, onToggleExpand: () => {}, toggleAriaLabel: 'Test label' }} />
204
+ );
205
+
206
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
207
+ });
208
+
209
+ test('text input is rendered when isExpanded is true', () => {
210
+ render(
211
+ <SearchInput expandableInput={{ isExpanded: true, onToggleExpand: () => {}, toggleAriaLabel: 'Test label' }} />
212
+ );
213
+
214
+ expect(screen.getByRole('textbox')).toBeVisible();
215
+ });
216
+
217
+ test('onToggleExpand is not called if the expandable toggle is not clicked', () => {
218
+ const mockOnToggleExpand = jest.fn();
219
+
220
+ render(
221
+ <SearchInput
222
+ expandableInput={{ isExpanded: true, onToggleExpand: mockOnToggleExpand, toggleAriaLabel: 'Test label' }}
223
+ />
224
+ );
225
+
226
+ expect(mockOnToggleExpand).not.toHaveBeenCalled();
227
+ });
228
+
229
+ test('onToggleExpand is called if the expandable toggle is clicked', async () => {
230
+ const mockOnToggleExpand = jest.fn();
231
+ const user = userEvent.setup();
232
+
233
+ render(
234
+ <SearchInput
235
+ expandableInput={{ isExpanded: true, onToggleExpand: mockOnToggleExpand, toggleAriaLabel: 'Test label' }}
236
+ />
237
+ );
238
+
239
+ await user.click(screen.getByRole('button'));
240
+
241
+ expect(mockOnToggleExpand).toHaveBeenCalledTimes(1);
242
+ expect(mockOnToggleExpand).toHaveBeenCalledWith(true, expect.objectContaining({ type: 'click' }));
243
+ });
244
+
245
+ test('toggleAriaLabel is applied to the expandable toggle', () => {
246
+ render(
247
+ <SearchInput expandableInput={{ isExpanded: true, onToggleExpand: () => {}, toggleAriaLabel: 'Test label' }} />
248
+ );
249
+
250
+ expect(screen.getByRole('button')).toHaveAccessibleName('Test label');
251
+ });
@@ -2,7 +2,7 @@
2
2
  id: 'Search input'
3
3
  section: components
4
4
  cssPrefix: 'pf-c-search-input'
5
- propComponents: ['SearchInput', 'SearchAttribute']
5
+ propComponents: ['SearchInput', 'SearchAttribute', 'ExpandableInput']
6
6
  beta: true
7
7
  ---
8
8
 
@@ -35,6 +35,11 @@ import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
35
35
  ```ts file='./SearchInputFocusSearch.tsx'
36
36
  ```
37
37
 
38
+ ### With expandable button
39
+
40
+ ```ts file='./SearchInputWithExpandable.tsx'
41
+ ```
42
+
38
43
  ### Advanced
39
44
 
40
45
  The search input component can be used to dynamically build a one to one attribute-value advanced search.
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { SearchInput } from '@patternfly/react-core';
3
+
4
+ export const SearchInputWithExpandable: React.FunctionComponent = () => {
5
+ const [value, setValue] = React.useState('');
6
+ const [isExpanded, setIsExpanded] = React.useState(false);
7
+
8
+ const onChange = (value: string) => {
9
+ setValue(value);
10
+ };
11
+
12
+ const onToggleExpand = (isExpanded: boolean) => {
13
+ setIsExpanded(!isExpanded);
14
+ };
15
+
16
+ return (
17
+ <SearchInput
18
+ placeholder="Find by name"
19
+ value={value}
20
+ onChange={onChange}
21
+ onClear={() => onChange('')}
22
+ expandableInput={{ isExpanded, onToggleExpand, toggleAriaLabel: 'Expandable search input toggle' }}
23
+ />
24
+ );
25
+ };