@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/esm/components/SearchInput/SearchInput.d.ts +10 -0
  3. package/dist/esm/components/SearchInput/SearchInput.d.ts.map +1 -1
  4. package/dist/esm/components/SearchInput/SearchInput.js +48 -6
  5. package/dist/esm/components/SearchInput/SearchInput.js.map +1 -1
  6. package/dist/esm/next/components/Dropdown/Dropdown.d.ts +25 -0
  7. package/dist/esm/next/components/Dropdown/Dropdown.d.ts.map +1 -0
  8. package/dist/esm/next/components/Dropdown/Dropdown.js +59 -0
  9. package/dist/esm/next/components/Dropdown/Dropdown.js.map +1 -0
  10. package/dist/esm/next/components/Dropdown/DropdownGroup.d.ts +12 -0
  11. package/dist/esm/next/components/Dropdown/DropdownGroup.d.ts.map +1 -0
  12. package/dist/esm/next/components/Dropdown/DropdownGroup.js +10 -0
  13. package/dist/esm/next/components/Dropdown/DropdownGroup.js.map +1 -0
  14. package/dist/esm/next/components/Dropdown/DropdownItem.d.ts +12 -0
  15. package/dist/esm/next/components/Dropdown/DropdownItem.d.ts.map +1 -0
  16. package/dist/esm/next/components/Dropdown/DropdownItem.js +10 -0
  17. package/dist/esm/next/components/Dropdown/DropdownItem.js.map +1 -0
  18. package/dist/esm/next/components/Dropdown/DropdownList.d.ts +10 -0
  19. package/dist/esm/next/components/Dropdown/DropdownList.d.ts.map +1 -0
  20. package/dist/esm/next/components/Dropdown/DropdownList.js +10 -0
  21. package/dist/esm/next/components/Dropdown/DropdownList.js.map +1 -0
  22. package/dist/esm/next/components/Dropdown/index.d.ts +5 -0
  23. package/dist/esm/next/components/Dropdown/index.d.ts.map +1 -0
  24. package/dist/esm/next/components/Dropdown/index.js +5 -0
  25. package/dist/esm/next/components/Dropdown/index.js.map +1 -0
  26. package/dist/esm/next/components/index.d.ts +1 -0
  27. package/dist/esm/next/components/index.d.ts.map +1 -1
  28. package/dist/esm/next/components/index.js +1 -0
  29. package/dist/esm/next/components/index.js.map +1 -1
  30. package/dist/js/components/SearchInput/SearchInput.d.ts +10 -0
  31. package/dist/js/components/SearchInput/SearchInput.d.ts.map +1 -1
  32. package/dist/js/components/SearchInput/SearchInput.js +48 -6
  33. package/dist/js/components/SearchInput/SearchInput.js.map +1 -1
  34. package/dist/js/next/components/Dropdown/Dropdown.d.ts +25 -0
  35. package/dist/js/next/components/Dropdown/Dropdown.d.ts.map +1 -0
  36. package/dist/js/next/components/Dropdown/Dropdown.js +63 -0
  37. package/dist/js/next/components/Dropdown/Dropdown.js.map +1 -0
  38. package/dist/js/next/components/Dropdown/DropdownGroup.d.ts +12 -0
  39. package/dist/js/next/components/Dropdown/DropdownGroup.d.ts.map +1 -0
  40. package/dist/js/next/components/Dropdown/DropdownGroup.js +14 -0
  41. package/dist/js/next/components/Dropdown/DropdownGroup.js.map +1 -0
  42. package/dist/js/next/components/Dropdown/DropdownItem.d.ts +12 -0
  43. package/dist/js/next/components/Dropdown/DropdownItem.d.ts.map +1 -0
  44. package/dist/js/next/components/Dropdown/DropdownItem.js +14 -0
  45. package/dist/js/next/components/Dropdown/DropdownItem.js.map +1 -0
  46. package/dist/js/next/components/Dropdown/DropdownList.d.ts +10 -0
  47. package/dist/js/next/components/Dropdown/DropdownList.d.ts.map +1 -0
  48. package/dist/js/next/components/Dropdown/DropdownList.js +14 -0
  49. package/dist/js/next/components/Dropdown/DropdownList.js.map +1 -0
  50. package/dist/js/next/components/Dropdown/index.d.ts +5 -0
  51. package/dist/js/next/components/Dropdown/index.d.ts.map +1 -0
  52. package/dist/js/next/components/Dropdown/index.js +8 -0
  53. package/dist/js/next/components/Dropdown/index.js.map +1 -0
  54. package/dist/js/next/components/index.d.ts +1 -0
  55. package/dist/js/next/components/index.d.ts.map +1 -1
  56. package/dist/js/next/components/index.js +1 -0
  57. package/dist/js/next/components/index.js.map +1 -1
  58. package/dist/umd/react-core.min.css +1262 -1262
  59. package/dist/umd/react-core.min.js +1 -1
  60. package/package.json +5 -5
  61. package/src/components/SearchInput/SearchInput.tsx +73 -7
  62. package/src/components/SearchInput/__tests__/SearchInput.test.tsx +82 -0
  63. package/src/components/SearchInput/examples/SearchInput.md +6 -1
  64. package/src/components/SearchInput/examples/SearchInputWithExpandable.tsx +25 -0
  65. package/src/next/components/Dropdown/Dropdown.tsx +115 -0
  66. package/src/next/components/Dropdown/DropdownGroup.tsx +25 -0
  67. package/src/next/components/Dropdown/DropdownItem.tsx +24 -0
  68. package/src/next/components/Dropdown/DropdownList.tsx +21 -0
  69. package/src/next/components/Dropdown/examples/Dropdown.md +31 -0
  70. package/src/next/components/Dropdown/examples/DropdownBasic.tsx +54 -0
  71. package/src/next/components/Dropdown/examples/DropdownWithDescriptions.tsx +60 -0
  72. package/src/next/components/Dropdown/examples/DropdownWithGroups.tsx +65 -0
  73. package/src/next/components/Dropdown/examples/DropdownWithKebabToggle.tsx +62 -0
  74. package/src/next/components/Dropdown/index.ts +4 -0
  75. 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.237.11",
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.88.11",
48
- "@patternfly/react-styles": "^4.87.11",
49
- "@patternfly/react-tokens": "^4.89.11",
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": "d5c28cc90817bb41bf6062071daaa1e435f93b87"
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 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
+ };
@@ -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
+ };