@patternfly/react-core 4.214.3 → 4.215.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 (70) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/esm/components/Checkbox/Checkbox.d.ts +2 -0
  3. package/dist/esm/components/Checkbox/Checkbox.d.ts.map +1 -1
  4. package/dist/esm/components/Checkbox/Checkbox.js +4 -3
  5. package/dist/esm/components/Checkbox/Checkbox.js.map +1 -1
  6. package/dist/esm/components/Menu/MenuItem.d.ts +3 -1
  7. package/dist/esm/components/Menu/MenuItem.d.ts.map +1 -1
  8. package/dist/esm/components/Menu/MenuItem.js +15 -6
  9. package/dist/esm/components/Menu/MenuItem.js.map +1 -1
  10. package/dist/esm/components/Slider/Slider.d.ts +4 -0
  11. package/dist/esm/components/Slider/Slider.d.ts.map +1 -1
  12. package/dist/esm/components/Slider/Slider.js +2 -2
  13. package/dist/esm/components/Slider/Slider.js.map +1 -1
  14. package/dist/esm/components/TextArea/TextArea.d.ts +1 -1
  15. package/dist/esm/components/TextArea/TextArea.d.ts.map +1 -1
  16. package/dist/esm/components/TextArea/TextArea.js.map +1 -1
  17. package/dist/js/components/Checkbox/Checkbox.d.ts +2 -0
  18. package/dist/js/components/Checkbox/Checkbox.d.ts.map +1 -1
  19. package/dist/js/components/Checkbox/Checkbox.js +4 -3
  20. package/dist/js/components/Checkbox/Checkbox.js.map +1 -1
  21. package/dist/js/components/Menu/MenuItem.d.ts +3 -1
  22. package/dist/js/components/Menu/MenuItem.d.ts.map +1 -1
  23. package/dist/js/components/Menu/MenuItem.js +15 -6
  24. package/dist/js/components/Menu/MenuItem.js.map +1 -1
  25. package/dist/js/components/Slider/Slider.d.ts +4 -0
  26. package/dist/js/components/Slider/Slider.d.ts.map +1 -1
  27. package/dist/js/components/Slider/Slider.js +2 -2
  28. package/dist/js/components/Slider/Slider.js.map +1 -1
  29. package/dist/js/components/TextArea/TextArea.d.ts +1 -1
  30. package/dist/js/components/TextArea/TextArea.d.ts.map +1 -1
  31. package/dist/js/components/TextArea/TextArea.js.map +1 -1
  32. package/dist/umd/react-core.js +19 -11
  33. package/dist/umd/react-core.min.js +2 -2
  34. package/package.json +5 -5
  35. package/src/components/Checkbox/Checkbox.tsx +7 -3
  36. package/src/components/Checkbox/__tests__/Checkbox.test.tsx +9 -0
  37. package/src/components/Dropdown/examples/Dropdown.md +25 -1530
  38. package/src/components/Dropdown/examples/DropdownAlignmentOnBreakpoints.tsx +64 -0
  39. package/src/components/Dropdown/examples/DropdownAppendBodyVsParent.tsx +83 -0
  40. package/src/components/Dropdown/examples/DropdownBadge.tsx +46 -0
  41. package/src/components/Dropdown/examples/DropdownBasic.tsx +57 -0
  42. package/src/components/Dropdown/examples/DropdownBasicPanel.tsx +34 -0
  43. package/src/components/Dropdown/examples/DropdownDescriptions.tsx +55 -0
  44. package/src/components/Dropdown/examples/DropdownDirectionUp.tsx +58 -0
  45. package/src/components/Dropdown/examples/DropdownDisabled.tsx +57 -0
  46. package/src/components/Dropdown/examples/DropdownGroups.tsx +55 -0
  47. package/src/components/Dropdown/examples/DropdownIconOnly.tsx +59 -0
  48. package/src/components/Dropdown/examples/DropdownImageAndText.tsx +64 -0
  49. package/src/components/Dropdown/examples/DropdownInitialSelection.tsx +57 -0
  50. package/src/components/Dropdown/examples/DropdownKebab.tsx +54 -0
  51. package/src/components/Dropdown/examples/DropdownPlainTextToggle.tsx +59 -0
  52. package/src/components/Dropdown/examples/DropdownPositionRight.tsx +58 -0
  53. package/src/components/Dropdown/examples/DropdownPrimaryToggle.tsx +57 -0
  54. package/src/components/Dropdown/examples/DropdownRouterLink.tsx +36 -0
  55. package/src/components/Dropdown/examples/DropdownSecondaryToggle.tsx +57 -0
  56. package/src/components/Dropdown/examples/DropdownSplitButton.tsx +67 -0
  57. package/src/components/Dropdown/examples/DropdownSplitButtonAction.tsx +105 -0
  58. package/src/components/Dropdown/examples/DropdownSplitButtonActionPrimary.tsx +107 -0
  59. package/src/components/Dropdown/examples/DropdownSplitButtonDisabled.tsx +73 -0
  60. package/src/components/Dropdown/examples/DropdownSplitButtonIndeterminate.tsx +78 -0
  61. package/src/components/Dropdown/examples/DropdownSplitButtonText.tsx +69 -0
  62. package/src/components/Menu/MenuItem.tsx +69 -43
  63. package/src/components/Menu/__tests__/Menu.test.tsx +23 -0
  64. package/src/components/Menu/examples/Menu.md +5 -0
  65. package/src/components/Menu/examples/MenuWithCheckbox.tsx +34 -0
  66. package/src/components/Slider/Slider.tsx +8 -0
  67. package/src/components/Slider/_tests_/Slider.test.tsx +27 -1
  68. package/src/components/TextArea/TextArea.tsx +1 -1
  69. package/src/demos/ComposableMenu/ComposableMenu.md +5 -0
  70. package/src/demos/ComposableMenu/examples/ComposableSimpleCheckboxSelect.tsx +98 -0
@@ -0,0 +1,78 @@
1
+ import React from 'react';
2
+ import {
3
+ Dropdown,
4
+ DropdownToggle,
5
+ DropdownToggleCheckbox,
6
+ DropdownItem,
7
+ DropdownSeparator
8
+ } from '@patternfly/react-core';
9
+
10
+ export const DropdownSplitButtonIndeterminate: React.FunctionComponent = () => {
11
+ const [isOpen, setIsOpen] = React.useState(false);
12
+ const [isChecked, setIsChecked] = React.useState(null);
13
+
14
+ const onToggle = (isOpen: boolean) => {
15
+ setIsOpen(isOpen);
16
+ };
17
+
18
+ const onFocus = () => {
19
+ const element = document.getElementById('toggle-split-button-indeterminate');
20
+ element.focus();
21
+ };
22
+
23
+ const onSelect = () => {
24
+ setIsOpen(false);
25
+ onFocus();
26
+ };
27
+
28
+ const onChange = (isChecked: boolean) => {
29
+ setIsChecked(isChecked);
30
+ };
31
+
32
+ const dropdownItems = [
33
+ <DropdownItem key="link">Link</DropdownItem>,
34
+ <DropdownItem key="action" component="button">
35
+ Action
36
+ </DropdownItem>,
37
+ <DropdownItem key="disabled link" isDisabled href="www.google.com">
38
+ Disabled link
39
+ </DropdownItem>,
40
+ <DropdownItem
41
+ key="disabled action"
42
+ isAriaDisabled
43
+ component="button"
44
+ tooltip="Tooltip for disabled item"
45
+ tooltipProps={{ position: 'top' }}
46
+ >
47
+ Disabled action
48
+ </DropdownItem>,
49
+ <DropdownSeparator key="separator" />,
50
+ <DropdownItem key="separated link">Separated link</DropdownItem>,
51
+ <DropdownItem key="separated action" component="button">
52
+ Separated action
53
+ </DropdownItem>
54
+ ];
55
+
56
+ return (
57
+ <Dropdown
58
+ onSelect={onSelect}
59
+ toggle={
60
+ <DropdownToggle
61
+ splitButtonItems={[
62
+ <DropdownToggleCheckbox
63
+ id="split-button-indeterminate-checkbox"
64
+ key="split-checkbox"
65
+ aria-label="Select all"
66
+ onChange={checked => onChange(checked)}
67
+ isChecked={isChecked}
68
+ />
69
+ ]}
70
+ onToggle={onToggle}
71
+ id="toggle-split-button-indeterminate"
72
+ />
73
+ }
74
+ isOpen={isOpen}
75
+ dropdownItems={dropdownItems}
76
+ />
77
+ );
78
+ };
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import {
3
+ Dropdown,
4
+ DropdownToggle,
5
+ DropdownToggleCheckbox,
6
+ DropdownItem,
7
+ DropdownSeparator
8
+ } from '@patternfly/react-core';
9
+
10
+ export const DropdownSplitButtonText: React.FunctionComponent = () => {
11
+ const [isOpen, setIsOpen] = React.useState(false);
12
+
13
+ const onToggle = (isOpen: boolean) => {
14
+ setIsOpen(isOpen);
15
+ };
16
+
17
+ const onFocus = () => {
18
+ const element = document.getElementById('toggle-split-button-text');
19
+ element.focus();
20
+ };
21
+
22
+ const onSelect = () => {
23
+ setIsOpen(false);
24
+ onFocus();
25
+ };
26
+
27
+ const dropdownItems = [
28
+ <DropdownItem key="link">Link</DropdownItem>,
29
+ <DropdownItem key="action" component="button">
30
+ Action
31
+ </DropdownItem>,
32
+ <DropdownItem key="disabled link" isDisabled href="www.google.com">
33
+ Disabled link
34
+ </DropdownItem>,
35
+ <DropdownItem
36
+ key="disabled action"
37
+ isAriaDisabled
38
+ component="button"
39
+ tooltip="Tooltip for disabled item"
40
+ tooltipProps={{ position: 'top' }}
41
+ >
42
+ Disabled action
43
+ </DropdownItem>,
44
+ <DropdownSeparator key="separator" />,
45
+ <DropdownItem key="separated link">Separated link</DropdownItem>,
46
+ <DropdownItem key="separated action" component="button">
47
+ Separated action
48
+ </DropdownItem>
49
+ ];
50
+
51
+ return (
52
+ <Dropdown
53
+ onSelect={onSelect}
54
+ toggle={
55
+ <DropdownToggle
56
+ splitButtonItems={[
57
+ <DropdownToggleCheckbox id="split-button-text-checkbox" key="split-checkbox" aria-label="Select all">
58
+ 10 selected
59
+ </DropdownToggleCheckbox>
60
+ ]}
61
+ onToggle={onToggle}
62
+ id="toggle-split-button-text"
63
+ />
64
+ }
65
+ isOpen={isOpen}
66
+ dropdownItems={dropdownItems}
67
+ />
68
+ );
69
+ };
@@ -8,10 +8,12 @@ import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external
8
8
  import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
9
9
  import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
10
10
  import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon';
11
+ import { Checkbox } from '../Checkbox';
11
12
  import { MenuContext, MenuItemContext } from './MenuContext';
12
13
  import { MenuItemAction } from './MenuItemAction';
13
14
  import { canUseDOM } from '../../helpers/util';
14
15
  import { useIsomorphicLayoutEffect } from '../../helpers/useIsomorphicLayout';
16
+ import { GenerateId } from '../../helpers/GenerateId/GenerateId';
15
17
 
16
18
  export interface MenuItemProps extends Omit<React.HTMLProps<HTMLLIElement>, 'onClick'> {
17
19
  /** Content rendered inside the menu list item. */
@@ -22,6 +24,8 @@ export interface MenuItemProps extends Omit<React.HTMLProps<HTMLLIElement>, 'onC
22
24
  itemId?: any;
23
25
  /** Target navigation link */
24
26
  to?: string;
27
+ /** @beta Flag indicating the item has a checkbox */
28
+ hasCheck?: boolean;
25
29
  /** Flag indicating whether the item is active */
26
30
  isActive?: boolean;
27
31
  /** Flag indicating if the item is favorited */
@@ -71,6 +75,7 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
71
75
  className,
72
76
  itemId = null,
73
77
  to,
78
+ hasCheck = false,
74
79
  isActive = null,
75
80
  isFavorited = null,
76
81
  isLoadButton = false,
@@ -105,7 +110,10 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
105
110
  setFlyoutRef,
106
111
  disableHover
107
112
  } = React.useContext(MenuContext);
108
- const Component = (to ? 'a' : component) as any;
113
+ let Component = (to ? 'a' : component) as any;
114
+ if (hasCheck && !to) {
115
+ Component = 'label' as any;
116
+ }
109
117
  const [flyoutTarget, setFlyoutTarget] = React.useState(null);
110
118
  const flyoutContext = React.useContext(FlyoutContext);
111
119
  const [flyoutXDirection, setFlyoutXDirection] = React.useState(flyoutContext.direction);
@@ -287,52 +295,70 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
287
295
  onMouseOver={onMouseOver}
288
296
  {...(flyoutMenu && { onKeyDown: handleFlyout })}
289
297
  ref={ref}
290
- role="none"
298
+ role={!hasCheck ? 'none' : 'menuitem'}
291
299
  {...props}
292
300
  >
293
- <Component
294
- tabIndex={-1}
295
- className={css(styles.menuItem, getIsSelected() && styles.modifiers.selected, className)}
296
- aria-current={getAriaCurrent()}
297
- disabled={isDisabled}
298
- role="menuitem"
299
- ref={innerRef}
300
- onClick={(event: any) => {
301
- onItemSelect(event, onSelect);
302
- _drill && _drill();
303
- }}
304
- {...additionalProps}
305
- >
306
- <span className={css(styles.menuItemMain)}>
307
- {direction === 'up' && (
308
- <span className={css(styles.menuItemToggleIcon)}>
309
- <AngleLeftIcon aria-hidden />
310
- </span>
311
- )}
312
- {icon && <span className={css(styles.menuItemIcon)}>{icon}</span>}
313
- <span className={css(styles.menuItemText)}>{children}</span>
314
- {isExternalLink && (
315
- <span className={css(styles.menuItemExternalIcon)}>
316
- <ExternalLinkAltIcon aria-hidden />
317
- </span>
318
- )}
319
- {(flyoutMenu || direction === 'down') && (
320
- <span className={css(styles.menuItemToggleIcon)}>
321
- <AngleRightIcon aria-hidden />
322
- </span>
323
- )}
324
- {getIsSelected() && (
325
- <span className={css(styles.menuItemSelectIcon)}>
326
- <CheckIcon aria-hidden />
301
+ <GenerateId>
302
+ {randomId => (
303
+ <Component
304
+ tabIndex={-1}
305
+ className={css(styles.menuItem, getIsSelected() && !hasCheck && styles.modifiers.selected, className)}
306
+ aria-current={getAriaCurrent()}
307
+ {...(!hasCheck && { disabled: isDisabled })}
308
+ {...(!hasCheck && { role: 'menuitem' })}
309
+ ref={innerRef}
310
+ {...(!hasCheck && {
311
+ onClick: (event: any) => {
312
+ onItemSelect(event, onSelect);
313
+ _drill && _drill();
314
+ }
315
+ })}
316
+ {...(hasCheck && { htmlFor: randomId })}
317
+ {...additionalProps}
318
+ >
319
+ <span className={css(styles.menuItemMain)}>
320
+ {direction === 'up' && (
321
+ <span className={css(styles.menuItemToggleIcon)}>
322
+ <AngleLeftIcon aria-hidden />
323
+ </span>
324
+ )}
325
+ {icon && <span className={css(styles.menuItemIcon)}>{icon}</span>}
326
+ {hasCheck && (
327
+ <span className={css('pf-c-menu__item-check')}>
328
+ <Checkbox
329
+ id={randomId}
330
+ component="span"
331
+ isChecked={isSelected || false}
332
+ onChange={event => onItemSelect(event, onSelect)}
333
+ isDisabled={isDisabled}
334
+ />
335
+ </span>
336
+ )}
337
+ <span className={css(styles.menuItemText)}>{children}</span>
338
+ {isExternalLink && (
339
+ <span className={css(styles.menuItemExternalIcon)}>
340
+ <ExternalLinkAltIcon aria-hidden />
341
+ </span>
342
+ )}
343
+ {(flyoutMenu || direction === 'down') && (
344
+ <span className={css(styles.menuItemToggleIcon)}>
345
+ <AngleRightIcon aria-hidden />
346
+ </span>
347
+ )}
348
+ {getIsSelected() && (
349
+ <span className={css(styles.menuItemSelectIcon)}>
350
+ <CheckIcon aria-hidden />
351
+ </span>
352
+ )}
327
353
  </span>
328
- )}
329
- </span>
330
- {description && direction !== 'up' && (
331
- <span className={css(styles.menuItemDescription)}>
332
- <span>{description}</span>
333
- </span>
354
+ {description && direction !== 'up' && (
355
+ <span className={css(styles.menuItemDescription)}>
356
+ <span>{description}</span>
357
+ </span>
358
+ )}
359
+ </Component>
334
360
  )}
335
- </Component>
361
+ </GenerateId>
336
362
  {flyoutVisible && (
337
363
  <MenuContext.Provider value={{ disableHover }}>
338
364
  <FlyoutContext.Provider value={{ direction: flyoutXDirection }}>{flyoutMenu}</FlyoutContext.Provider>
@@ -2,8 +2,12 @@ import React from 'react';
2
2
 
3
3
  import { render, screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
+ import userEvent from '@testing-library/user-event';
5
6
 
6
7
  import { Menu } from '../Menu';
8
+ import { MenuItem } from '../MenuItem';
9
+ import { MenuList } from '../MenuList';
10
+ import { MenuContent } from '../MenuContent';
7
11
 
8
12
  describe('Menu', () => {
9
13
  test('should render Menu successfully', () => {
@@ -47,4 +51,23 @@ describe('Menu', () => {
47
51
  expect(screen.getByText('content')).toHaveClass('pf-m-nav');
48
52
  });
49
53
  });
54
+
55
+ describe('with hasCheck', () => {
56
+ test('should render Menu with checkbox items', () => {
57
+ const { asFragment } = render(
58
+ <Menu>
59
+ <MenuContent>
60
+ <MenuList>
61
+ <MenuItem hasCheck itemId={0}>
62
+ Checkbox 1
63
+ </MenuItem>
64
+ </MenuList>
65
+ </MenuContent>
66
+ </Menu>
67
+ );
68
+ const checkbox1 = screen.getAllByRole('checkbox')[0];
69
+ expect(checkbox1).not.toBeChecked();
70
+ expect(screen.getByText("Checkbox 1")).toBeInTheDocument();
71
+ });
72
+ });
50
73
  });
@@ -128,6 +128,11 @@ class MenuIconsList extends React.Component {
128
128
  }
129
129
  ```
130
130
 
131
+ ### With checkbox
132
+
133
+ ```ts file="./MenuWithCheckbox.tsx" isBeta
134
+ ```
135
+
131
136
  ### Filtering with text input
132
137
 
133
138
  ```js
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Menu, MenuContent, MenuList, MenuItem } from '@patternfly/react-core';
3
+
4
+ export const MenuCheckboxList: React.FunctionComponent = () => {
5
+ const [selectedItems, setSelectedItems] = React.useState<number[]>([]);
6
+
7
+ /* eslint no-unused-vars: ["error", {"args": "after-used"}] */
8
+ const onSelect = (event: React.MouseEvent<Element, MouseEvent>, itemId: number | string) => {
9
+ const item = itemId as number;
10
+ if (selectedItems.includes(item)) {
11
+ setSelectedItems(selectedItems.filter(id => id !== item));
12
+ } else {
13
+ setSelectedItems([...selectedItems, item]);
14
+ }
15
+ };
16
+
17
+ return (
18
+ <Menu onSelect={onSelect} selected={selectedItems}>
19
+ <MenuContent>
20
+ <MenuList>
21
+ <MenuItem hasCheck itemId={0} isSelected={selectedItems.includes(0)}>
22
+ Checkbox 1
23
+ </MenuItem>
24
+ <MenuItem hasCheck itemId={1} isSelected={selectedItems.includes(1)}>
25
+ Checkbox 2
26
+ </MenuItem>
27
+ <MenuItem hasCheck itemId={2} isDisabled>
28
+ Checkbox 3
29
+ </MenuItem>
30
+ </MenuList>
31
+ </MenuContent>
32
+ </Menu>
33
+ );
34
+ };
@@ -61,6 +61,10 @@ export interface SliderProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onCh
61
61
  leftActions?: React.ReactNode;
62
62
  /** Actions placed to the right of the slider */
63
63
  rightActions?: React.ReactNode;
64
+ /** One or more id's to use for the slider thumb description */
65
+ 'aria-describedby'?: string;
66
+ /** One or more id's to use for the slider thumb label */
67
+ 'aria-labelledby'?: string;
64
68
  }
65
69
 
66
70
  const getPercentage = (current: number, max: number) => (100 * current) / max;
@@ -86,6 +90,8 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
86
90
  max = 100,
87
91
  showTicks = false,
88
92
  showBoundaries = true,
93
+ 'aria-describedby': ariaDescribedby,
94
+ 'aria-labelledby': ariaLabelledby,
89
95
  ...props
90
96
  }: SliderProps) => {
91
97
  const sliderRailRef = React.useRef<HTMLDivElement>();
@@ -362,6 +368,8 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
362
368
  aria-valuetext={findAriaTextValue()}
363
369
  aria-label={thumbAriaLabel}
364
370
  aria-disabled={isDisabled}
371
+ aria-describedby={ariaDescribedby}
372
+ aria-labelledby={ariaLabelledby}
365
373
  onMouseDown={!isDisabled ? handleMouseDown : null}
366
374
  onTouchStart={!isDisabled ? handleTouchStart : null}
367
375
  onKeyDown={!isDisabled ? handleThumbKeys : null}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { render } from '@testing-library/react';
2
+ import { render, screen } from '@testing-library/react';
3
3
  import { Slider } from '../Slider';
4
4
  import { Button } from '../../Button';
5
5
 
@@ -79,3 +79,29 @@ describe('slider', () => {
79
79
  expect(asFragment()).toMatchSnapshot();
80
80
  });
81
81
  });
82
+
83
+ test('renders slider with aria-labelledby', () => {
84
+ render(
85
+ <>
86
+ <p id="test">label text</p>
87
+ <Slider value={50} aria-labelledby="test" />
88
+ </>
89
+ );
90
+
91
+ const slider = screen.getByRole('slider', { name: 'label text' });
92
+
93
+ expect(slider).toBeVisible();
94
+ });
95
+
96
+ test('renders slider with aria-describedby', () => {
97
+ render(
98
+ <>
99
+ <p id="test">descriptive text about the slider</p>
100
+ <Slider value={50} aria-describedby="test" />
101
+ </>
102
+ );
103
+
104
+ const slider = screen.getByRole('slider', { description: 'descriptive text about the slider' });
105
+
106
+ expect(slider).toBeVisible();
107
+ });
@@ -125,7 +125,7 @@ export class TextAreaBase extends React.Component<TextAreaProps> {
125
125
  }
126
126
  }
127
127
 
128
- export const TextArea = React.forwardRef((props: TextAreaProps, ref: React.Ref<HTMLInputElement>) => (
128
+ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>((props, ref) => (
129
129
  <TextAreaBase {...props} innerRef={ref as React.MutableRefObject<any>} />
130
130
  ));
131
131
  TextArea.displayName = 'TextArea';
@@ -40,6 +40,11 @@ Composable menus currently require consumer keyboard handling and use of our und
40
40
  ```ts file="./examples/ComposableSimpleSelect.tsx"
41
41
  ```
42
42
 
43
+ ### Composable simple checkbox select
44
+
45
+ ```ts isBeta file="./examples/ComposableSimpleCheckboxSelect.tsx"
46
+ ```
47
+
43
48
  ### Composable drilldown menu
44
49
 
45
50
  ```ts isBeta file="./examples/ComposableDrilldownMenu.tsx"
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import { MenuToggle, Menu, MenuContent, MenuList, MenuItem, Popper, Badge } from '@patternfly/react-core';
3
+
4
+ export const ComposableSimpleCheckboxSelect: React.FunctionComponent = () => {
5
+ const [isOpen, setIsOpen] = React.useState<boolean>(false);
6
+ const [selectedItems, setSelectedItems] = React.useState<number[]>([]);
7
+ const toggleRef = React.useRef<HTMLButtonElement>();
8
+ const menuRef = React.useRef<HTMLDivElement>();
9
+
10
+ const handleMenuKeys = React.useCallback(
11
+ event => {
12
+ if (menuRef.current) {
13
+ if (isOpen && menuRef.current.contains(event.target as Node)) {
14
+ if (event.key === 'Escape' || event.key === 'Tab') {
15
+ setIsOpen(!isOpen);
16
+ toggleRef.current.focus();
17
+ }
18
+ }
19
+ }
20
+ },
21
+ [isOpen]
22
+ );
23
+
24
+ const handleClickOutside = React.useCallback(
25
+ event => {
26
+ if (isOpen && !menuRef.current.contains(event.target as Node)) {
27
+ setIsOpen(false);
28
+ }
29
+ },
30
+ [isOpen]
31
+ );
32
+
33
+ React.useEffect(() => {
34
+ window.addEventListener('keydown', handleMenuKeys);
35
+ window.addEventListener('click', handleClickOutside);
36
+ return () => {
37
+ window.removeEventListener('keydown', handleMenuKeys);
38
+ window.removeEventListener('click', handleClickOutside);
39
+ };
40
+ }, [handleClickOutside, handleMenuKeys]);
41
+
42
+ const onToggleClick = (ev: React.MouseEvent) => {
43
+ ev.stopPropagation(); // Stop handleClickOutside from handling
44
+ setTimeout(() => {
45
+ if (menuRef.current) {
46
+ const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
47
+ firstElement && (firstElement as HTMLElement).focus();
48
+ }
49
+ }, 0);
50
+ setIsOpen(!isOpen);
51
+ };
52
+
53
+ const onSelect = (ev: React.MouseEvent<Element, MouseEvent>, itemId: number | string) => {
54
+ const item = itemId as number;
55
+ if (selectedItems.includes(item)) {
56
+ setSelectedItems(selectedItems.filter(id => id !== item));
57
+ } else {
58
+ setSelectedItems([...selectedItems, item]);
59
+ }
60
+ };
61
+
62
+ const toggle = (
63
+ <MenuToggle
64
+ ref={toggleRef}
65
+ {...(selectedItems.length > 0 && { badge: <Badge isRead>{selectedItems.length}</Badge> })}
66
+ onClick={onToggleClick}
67
+ isExpanded={isOpen}
68
+ style={
69
+ {
70
+ width: '220px'
71
+ } as React.CSSProperties
72
+ }
73
+ >
74
+ Filter by status
75
+ </MenuToggle>
76
+ );
77
+ const menu = (
78
+ <Menu ref={menuRef} id="select-menu" onSelect={onSelect} selected={selectedItems}>
79
+ <MenuContent>
80
+ <MenuList>
81
+ <MenuItem hasCheck itemId={0} isSelected={selectedItems.includes(0)}>
82
+ Debug
83
+ </MenuItem>
84
+ <MenuItem hasCheck itemId={1} isSelected={selectedItems.includes(1)}>
85
+ Info
86
+ </MenuItem>
87
+ <MenuItem hasCheck itemId={2} isSelected={selectedItems.includes(2)}>
88
+ Warn
89
+ </MenuItem>
90
+ <MenuItem hasCheck isDisabled itemId={4} isSelected={selectedItems.includes(4)}>
91
+ Error
92
+ </MenuItem>
93
+ </MenuList>
94
+ </MenuContent>
95
+ </Menu>
96
+ );
97
+ return <Popper trigger={toggle} popper={menu} isVisible={isOpen} />;
98
+ };