@patternfly/react-core 4.237.11 → 4.239.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/CHANGELOG.md +22 -0
- package/dist/esm/components/SearchInput/SearchInput.d.ts +10 -0
- package/dist/esm/components/SearchInput/SearchInput.d.ts.map +1 -1
- package/dist/esm/components/SearchInput/SearchInput.js +48 -6
- package/dist/esm/components/SearchInput/SearchInput.js.map +1 -1
- package/dist/esm/next/components/Dropdown/Dropdown.d.ts +25 -0
- package/dist/esm/next/components/Dropdown/Dropdown.d.ts.map +1 -0
- package/dist/esm/next/components/Dropdown/Dropdown.js +59 -0
- package/dist/esm/next/components/Dropdown/Dropdown.js.map +1 -0
- package/dist/esm/next/components/Dropdown/DropdownGroup.d.ts +12 -0
- package/dist/esm/next/components/Dropdown/DropdownGroup.d.ts.map +1 -0
- package/dist/esm/next/components/Dropdown/DropdownGroup.js +10 -0
- package/dist/esm/next/components/Dropdown/DropdownGroup.js.map +1 -0
- package/dist/esm/next/components/Dropdown/DropdownItem.d.ts +12 -0
- package/dist/esm/next/components/Dropdown/DropdownItem.d.ts.map +1 -0
- package/dist/esm/next/components/Dropdown/DropdownItem.js +10 -0
- package/dist/esm/next/components/Dropdown/DropdownItem.js.map +1 -0
- package/dist/esm/next/components/Dropdown/DropdownList.d.ts +10 -0
- package/dist/esm/next/components/Dropdown/DropdownList.d.ts.map +1 -0
- package/dist/esm/next/components/Dropdown/DropdownList.js +10 -0
- package/dist/esm/next/components/Dropdown/DropdownList.js.map +1 -0
- package/dist/esm/next/components/Dropdown/index.d.ts +5 -0
- package/dist/esm/next/components/Dropdown/index.d.ts.map +1 -0
- package/dist/esm/next/components/Dropdown/index.js +5 -0
- package/dist/esm/next/components/Dropdown/index.js.map +1 -0
- package/dist/esm/next/components/index.d.ts +1 -0
- package/dist/esm/next/components/index.d.ts.map +1 -1
- package/dist/esm/next/components/index.js +1 -0
- package/dist/esm/next/components/index.js.map +1 -1
- package/dist/js/components/SearchInput/SearchInput.d.ts +10 -0
- package/dist/js/components/SearchInput/SearchInput.d.ts.map +1 -1
- package/dist/js/components/SearchInput/SearchInput.js +48 -6
- package/dist/js/components/SearchInput/SearchInput.js.map +1 -1
- package/dist/js/next/components/Dropdown/Dropdown.d.ts +25 -0
- package/dist/js/next/components/Dropdown/Dropdown.d.ts.map +1 -0
- package/dist/js/next/components/Dropdown/Dropdown.js +63 -0
- package/dist/js/next/components/Dropdown/Dropdown.js.map +1 -0
- package/dist/js/next/components/Dropdown/DropdownGroup.d.ts +12 -0
- package/dist/js/next/components/Dropdown/DropdownGroup.d.ts.map +1 -0
- package/dist/js/next/components/Dropdown/DropdownGroup.js +14 -0
- package/dist/js/next/components/Dropdown/DropdownGroup.js.map +1 -0
- package/dist/js/next/components/Dropdown/DropdownItem.d.ts +12 -0
- package/dist/js/next/components/Dropdown/DropdownItem.d.ts.map +1 -0
- package/dist/js/next/components/Dropdown/DropdownItem.js +14 -0
- package/dist/js/next/components/Dropdown/DropdownItem.js.map +1 -0
- package/dist/js/next/components/Dropdown/DropdownList.d.ts +10 -0
- package/dist/js/next/components/Dropdown/DropdownList.d.ts.map +1 -0
- package/dist/js/next/components/Dropdown/DropdownList.js +14 -0
- package/dist/js/next/components/Dropdown/DropdownList.js.map +1 -0
- package/dist/js/next/components/Dropdown/index.d.ts +5 -0
- package/dist/js/next/components/Dropdown/index.d.ts.map +1 -0
- package/dist/js/next/components/Dropdown/index.js +8 -0
- package/dist/js/next/components/Dropdown/index.js.map +1 -0
- package/dist/js/next/components/index.d.ts +1 -0
- package/dist/js/next/components/index.d.ts.map +1 -1
- package/dist/js/next/components/index.js +1 -0
- package/dist/js/next/components/index.js.map +1 -1
- package/dist/umd/react-core.min.css +1262 -1262
- package/dist/umd/react-core.min.js +1 -1
- package/package.json +5 -5
- package/src/components/SearchInput/SearchInput.tsx +73 -7
- package/src/components/SearchInput/__tests__/SearchInput.test.tsx +82 -0
- package/src/components/SearchInput/examples/SearchInput.md +6 -1
- package/src/components/SearchInput/examples/SearchInputWithExpandable.tsx +25 -0
- package/src/next/components/Dropdown/Dropdown.tsx +115 -0
- package/src/next/components/Dropdown/DropdownGroup.tsx +25 -0
- package/src/next/components/Dropdown/DropdownItem.tsx +24 -0
- package/src/next/components/Dropdown/DropdownList.tsx +21 -0
- package/src/next/components/Dropdown/examples/Dropdown.md +31 -0
- package/src/next/components/Dropdown/examples/DropdownBasic.tsx +54 -0
- package/src/next/components/Dropdown/examples/DropdownWithDescriptions.tsx +60 -0
- package/src/next/components/Dropdown/examples/DropdownWithGroups.tsx +65 -0
- package/src/next/components/Dropdown/examples/DropdownWithKebabToggle.tsx +62 -0
- package/src/next/components/Dropdown/index.ts +4 -0
- package/src/next/components/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/react-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.239.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.
|
|
48
|
-
"@patternfly/react-styles": "^4.
|
|
49
|
-
"@patternfly/react-tokens": "^4.
|
|
47
|
+
"@patternfly/react-icons": "^4.90.0",
|
|
48
|
+
"@patternfly/react-styles": "^4.89.0",
|
|
49
|
+
"@patternfly/react-tokens": "^4.91.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": "
|
|
73
|
+
"gitHead": "d5b77ee797de7d6babd7f2add1fb238812282d64"
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { css } from '@patternfly/react-styles';
|
|
3
|
+
import { Menu, MenuContent, MenuProps } from '../../../components/Menu';
|
|
4
|
+
import { Popper } from '../../../helpers/Popper/Popper';
|
|
5
|
+
|
|
6
|
+
export interface DropdownProps extends MenuProps {
|
|
7
|
+
/** Anything which can be rendered in a dropdown. */
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
/** Classes applied to root element of dropdown. */
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Renderer for a custom dropdown toggle. Forwards a ref to the toggle. */
|
|
12
|
+
toggle: (toggleRef: React.RefObject<any>) => React.ReactNode;
|
|
13
|
+
/** Flag to indicate if menu is opened.*/
|
|
14
|
+
isOpen?: boolean;
|
|
15
|
+
/** Function callback called when user selects item. */
|
|
16
|
+
onSelect?: (event?: React.MouseEvent<Element, MouseEvent>, itemId?: string | number) => void;
|
|
17
|
+
/** Callback to allow the dropdown component to change the open state of the menu.
|
|
18
|
+
* Triggered by clicking outside of the menu, or by pressing either tab or escape. */
|
|
19
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
20
|
+
/** Indicates if the menu should be without the outer box-shadow. */
|
|
21
|
+
isPlain?: boolean;
|
|
22
|
+
/** Indicates if the menu should be scrollable. */
|
|
23
|
+
isScrollable?: boolean;
|
|
24
|
+
/** Min width of the menu. */
|
|
25
|
+
minWidth?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Dropdown: React.FunctionComponent<DropdownProps> = ({
|
|
29
|
+
children,
|
|
30
|
+
className,
|
|
31
|
+
onSelect,
|
|
32
|
+
isOpen,
|
|
33
|
+
toggle,
|
|
34
|
+
onOpenChange,
|
|
35
|
+
isPlain,
|
|
36
|
+
isScrollable,
|
|
37
|
+
minWidth,
|
|
38
|
+
...props
|
|
39
|
+
}: DropdownProps) => {
|
|
40
|
+
const localMenuRef = React.useRef<HTMLDivElement>();
|
|
41
|
+
const toggleRef = React.useRef<HTMLButtonElement>();
|
|
42
|
+
const containerRef = React.useRef<HTMLDivElement>();
|
|
43
|
+
|
|
44
|
+
const menuRef = (props.innerRef as React.RefObject<HTMLDivElement>) || localMenuRef;
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const handleMenuKeys = (event: KeyboardEvent) => {
|
|
47
|
+
if (!isOpen && toggleRef.current?.contains(event.target as Node)) {
|
|
48
|
+
// toggle was clicked open, focus on first menu item
|
|
49
|
+
if (event.key === 'Enter') {
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
|
|
52
|
+
firstElement && (firstElement as HTMLElement).focus();
|
|
53
|
+
}, 0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Close the menu on tab or escape if onOpenChange is provided
|
|
57
|
+
if (
|
|
58
|
+
(isOpen && onOpenChange && menuRef.current?.contains(event.target as Node)) ||
|
|
59
|
+
toggleRef.current?.contains(event.target as Node)
|
|
60
|
+
) {
|
|
61
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
62
|
+
onOpenChange(!isOpen);
|
|
63
|
+
toggleRef.current?.focus();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
69
|
+
// If the event is not on the toggle and onOpenChange callback is provided, close the menu
|
|
70
|
+
if (isOpen && onOpenChange && !toggleRef?.current?.contains(event.target as Node)) {
|
|
71
|
+
if (isOpen && !menuRef.current?.contains(event.target as Node)) {
|
|
72
|
+
onOpenChange(false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
window.addEventListener('keydown', handleMenuKeys);
|
|
78
|
+
window.addEventListener('click', handleClickOutside);
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
window.removeEventListener('keydown', handleMenuKeys);
|
|
82
|
+
window.removeEventListener('click', handleClickOutside);
|
|
83
|
+
};
|
|
84
|
+
}, [isOpen, menuRef, onOpenChange]);
|
|
85
|
+
|
|
86
|
+
const menu = (
|
|
87
|
+
<Menu
|
|
88
|
+
className={css(className)}
|
|
89
|
+
ref={menuRef}
|
|
90
|
+
onSelect={(event, itemId) => onSelect(event, itemId)}
|
|
91
|
+
isPlain={isPlain}
|
|
92
|
+
isScrollable={isScrollable}
|
|
93
|
+
{...(minWidth && {
|
|
94
|
+
style: {
|
|
95
|
+
'--pf-c-menu--MinWidth': minWidth
|
|
96
|
+
} as React.CSSProperties
|
|
97
|
+
})}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
<MenuContent>{children}</MenuContent>
|
|
101
|
+
</Menu>
|
|
102
|
+
);
|
|
103
|
+
return (
|
|
104
|
+
<div ref={containerRef}>
|
|
105
|
+
<Popper
|
|
106
|
+
trigger={toggle(toggleRef)}
|
|
107
|
+
removeFindDomNode
|
|
108
|
+
popper={menu}
|
|
109
|
+
appendTo={containerRef.current || undefined}
|
|
110
|
+
isVisible={isOpen}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
Dropdown.displayName = 'Dropdown';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { css } from '@patternfly/react-styles';
|
|
3
|
+
import { MenuGroupProps, MenuGroup } from '../../../components/Menu';
|
|
4
|
+
|
|
5
|
+
export interface DropdownGroupProps extends Omit<MenuGroupProps, 'ref'> {
|
|
6
|
+
/** Anything which can be rendered in a dropdown group. */
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
/** Classes applied to root element of dropdown group */
|
|
9
|
+
className?: string;
|
|
10
|
+
/** Label of the dropdown group */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DropdownGroup: React.FunctionComponent<DropdownGroupProps> = ({
|
|
15
|
+
children,
|
|
16
|
+
className,
|
|
17
|
+
label,
|
|
18
|
+
labelHeadingLevel = 'h1',
|
|
19
|
+
...props
|
|
20
|
+
}: DropdownGroupProps) => (
|
|
21
|
+
<MenuGroup className={css(className)} label={label} labelHeadingLevel={labelHeadingLevel} {...props}>
|
|
22
|
+
{children}
|
|
23
|
+
</MenuGroup>
|
|
24
|
+
);
|
|
25
|
+
DropdownGroup.displayName = 'DropdownGroup';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { css } from '@patternfly/react-styles';
|
|
3
|
+
import { MenuItemProps, MenuItem } from '../../../components/Menu';
|
|
4
|
+
|
|
5
|
+
export interface DropdownItemProps extends Omit<MenuItemProps, 'ref'> {
|
|
6
|
+
/** Anything which can be rendered in a dropdown item */
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
/** Classes applied to root element of dropdown item */
|
|
9
|
+
className?: string;
|
|
10
|
+
/** Description of the dropdown item */
|
|
11
|
+
description?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DropdownItem: React.FunctionComponent<MenuItemProps> = ({
|
|
15
|
+
children,
|
|
16
|
+
className,
|
|
17
|
+
description,
|
|
18
|
+
...props
|
|
19
|
+
}: DropdownItemProps) => (
|
|
20
|
+
<MenuItem className={css(className)} description={description} {...props}>
|
|
21
|
+
{children}
|
|
22
|
+
</MenuItem>
|
|
23
|
+
);
|
|
24
|
+
DropdownItem.displayName = 'DropdownItem';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { css } from '@patternfly/react-styles';
|
|
3
|
+
import { MenuListProps, MenuList } from '../../../components/Menu';
|
|
4
|
+
|
|
5
|
+
export interface DropdownListProps extends MenuListProps {
|
|
6
|
+
/** Anything which can be rendered in a dropdown list */
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
/** Classes applied to root element of dropdown list */
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DropdownList: React.FunctionComponent<MenuListProps> = ({
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}: DropdownListProps) => (
|
|
17
|
+
<MenuList className={css(className)} {...props}>
|
|
18
|
+
{children}
|
|
19
|
+
</MenuList>
|
|
20
|
+
);
|
|
21
|
+
DropdownList.displayName = 'DropdownList';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: Dropdown
|
|
3
|
+
section: components
|
|
4
|
+
cssPrefix: pf-c-dropdown
|
|
5
|
+
propComponents: ['Dropdown', DropdownGroup, 'DropdownItem', 'DropdownList']
|
|
6
|
+
beta: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
### Basic
|
|
14
|
+
|
|
15
|
+
```ts file="./DropdownBasic.tsx"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### With kebab toggle
|
|
19
|
+
|
|
20
|
+
```ts file="./DropdownWithKebabToggle.tsx"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### With groups
|
|
24
|
+
|
|
25
|
+
```ts file="./DropdownWithGroups.tsx"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### With descriptions
|
|
29
|
+
|
|
30
|
+
```ts file="./DropdownWithDescriptions.tsx"
|
|
31
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Dropdown, DropdownItem, DropdownList } from '@patternfly/react-core/next';
|
|
3
|
+
import { Divider, MenuToggle } from '@patternfly/react-core';
|
|
4
|
+
|
|
5
|
+
export const DropdownBasic: React.FunctionComponent = () => {
|
|
6
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
7
|
+
const menuRef = React.useRef<HTMLDivElement>(null);
|
|
8
|
+
|
|
9
|
+
const onToggleClick = () => {
|
|
10
|
+
setIsOpen(!isOpen);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string | number | undefined) => {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log('selected', itemId);
|
|
16
|
+
setIsOpen(false);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Dropdown
|
|
21
|
+
innerRef={menuRef}
|
|
22
|
+
isOpen={isOpen}
|
|
23
|
+
onSelect={onSelect}
|
|
24
|
+
onOpenChange={isOpen => setIsOpen(isOpen)}
|
|
25
|
+
toggle={toggleRef => (
|
|
26
|
+
<MenuToggle ref={toggleRef} onClick={onToggleClick} isExpanded={isOpen}>
|
|
27
|
+
Dropdown
|
|
28
|
+
</MenuToggle>
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
<DropdownList>
|
|
32
|
+
<DropdownItem itemId={0} key="link">
|
|
33
|
+
Link
|
|
34
|
+
</DropdownItem>
|
|
35
|
+
<DropdownItem itemId={1} key="action" to="#default-link2" onClick={ev => ev.preventDefault()}>
|
|
36
|
+
Action
|
|
37
|
+
</DropdownItem>
|
|
38
|
+
<DropdownItem itemId={2} isDisabled key="disabled link">
|
|
39
|
+
Disabled link
|
|
40
|
+
</DropdownItem>
|
|
41
|
+
<DropdownItem itemId={3} isDisabled key="disabled action" to="#default-link4">
|
|
42
|
+
Disabled action
|
|
43
|
+
</DropdownItem>
|
|
44
|
+
<Divider key="separator" />
|
|
45
|
+
<DropdownItem itemId={4} key="separated link">
|
|
46
|
+
Separated link
|
|
47
|
+
</DropdownItem>
|
|
48
|
+
<DropdownItem itemId={5} key="separated action">
|
|
49
|
+
Separated action
|
|
50
|
+
</DropdownItem>
|
|
51
|
+
</DropdownList>
|
|
52
|
+
</Dropdown>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Dropdown, DropdownItem, DropdownList } from '@patternfly/react-core/next';
|
|
3
|
+
import { MenuToggle } from '@patternfly/react-core';
|
|
4
|
+
|
|
5
|
+
export const DropdownWithDescriptions: React.FunctionComponent = () => {
|
|
6
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
7
|
+
const menuRef = React.useRef<HTMLDivElement>(null);
|
|
8
|
+
|
|
9
|
+
const onToggleClick = () => {
|
|
10
|
+
setIsOpen(!isOpen);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string | number | undefined) => {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log('selected', itemId);
|
|
16
|
+
setIsOpen(false);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Dropdown
|
|
21
|
+
innerRef={menuRef}
|
|
22
|
+
isOpen={isOpen}
|
|
23
|
+
onSelect={onSelect}
|
|
24
|
+
minWidth="150px"
|
|
25
|
+
onOpenChange={isOpen => setIsOpen(isOpen)}
|
|
26
|
+
toggle={toggleRef => (
|
|
27
|
+
<MenuToggle ref={toggleRef} isFullWidth onClick={onToggleClick} isExpanded={isOpen}>
|
|
28
|
+
Dropdown
|
|
29
|
+
</MenuToggle>
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<DropdownList>
|
|
33
|
+
<DropdownItem itemId={0} key="link" description="This is a description">
|
|
34
|
+
Link
|
|
35
|
+
</DropdownItem>
|
|
36
|
+
<DropdownItem
|
|
37
|
+
itemId={1}
|
|
38
|
+
key="action"
|
|
39
|
+
description="This is a very long description that describes the menu item"
|
|
40
|
+
to="#default-link2"
|
|
41
|
+
onClick={ev => ev.preventDefault()}
|
|
42
|
+
>
|
|
43
|
+
Action
|
|
44
|
+
</DropdownItem>
|
|
45
|
+
<DropdownItem itemId={2} isDisabled description="Disabled link description" key="disabled link">
|
|
46
|
+
Disabled link
|
|
47
|
+
</DropdownItem>
|
|
48
|
+
<DropdownItem
|
|
49
|
+
itemId={3}
|
|
50
|
+
isDisabled
|
|
51
|
+
description="This is a description"
|
|
52
|
+
key="disabled action"
|
|
53
|
+
to="#default-link4"
|
|
54
|
+
>
|
|
55
|
+
Disabled action
|
|
56
|
+
</DropdownItem>
|
|
57
|
+
</DropdownList>
|
|
58
|
+
</Dropdown>
|
|
59
|
+
);
|
|
60
|
+
};
|