@atlaskit/dropdown-menu 10.1.8 → 11.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/__perf__/default.tsx +1 -1
  3. package/__perf__/dropdown-menu.tsx +216 -0
  4. package/codemods/11.0.0-lite-mode.tsx +39 -0
  5. package/codemods/__tests__/11.0.0-lite-mode.test.tsx +48 -0
  6. package/codemods/__tests__/convert-position.test.tsx +88 -0
  7. package/codemods/__tests__/convert-triggerType.test.tsx +100 -0
  8. package/codemods/__tests__/deprecate-items.test.tsx +108 -0
  9. package/codemods/__tests__/deprecate-onItemActivated.test.tsx +108 -0
  10. package/codemods/__tests__/deprecate-onPositioned.test.tsx +108 -0
  11. package/codemods/__tests__/deprecate-shouldFitContainer.tsx +108 -0
  12. package/codemods/__tests__/rename-imports.tsx +136 -0
  13. package/codemods/__tests__/replace-position-to-placement.test.tsx +84 -0
  14. package/codemods/__tests__/replace-shouldAllowMultipleLine.test.tsx +122 -0
  15. package/codemods/__tests__/update-component-callsites.tsx +66 -0
  16. package/codemods/migrates/convert-trigger-type.tsx +57 -0
  17. package/codemods/migrates/deprecate-items.tsx +9 -0
  18. package/codemods/migrates/deprecate-onItemActivated.tsx +9 -0
  19. package/codemods/migrates/deprecate-onPositioned.tsx +9 -0
  20. package/codemods/migrates/deprecate-shouldFitContainer.tsx +9 -0
  21. package/codemods/migrates/rename-imports.tsx +22 -0
  22. package/codemods/migrates/replace-position-to-placement.tsx +38 -0
  23. package/codemods/migrates/replace-shouldAllowMultiline.tsx +47 -0
  24. package/codemods/migrates/update-component-callsites.tsx +13 -0
  25. package/codemods/utils/convert-position.tsx +24 -0
  26. package/codemods/utils/create-rename-import.tsx +41 -0
  27. package/codemods/utils/create-update-callsite.tsx +32 -0
  28. package/dist/cjs/checkbox/dropdown-item-checkbox-group.js +31 -0
  29. package/dist/cjs/checkbox/dropdown-item-checkbox.js +108 -0
  30. package/dist/cjs/dropdown-menu-item-group.js +22 -0
  31. package/dist/cjs/dropdown-menu-item.js +67 -0
  32. package/dist/cjs/dropdown-menu.js +194 -0
  33. package/dist/cjs/index.js +21 -29
  34. package/dist/cjs/{components/item/DropdownItemRadio.js → internal/components/focus-manager.js} +40 -9
  35. package/dist/cjs/internal/components/menu-wrapper.js +68 -0
  36. package/dist/cjs/internal/context/checkbox-group-context.js +14 -0
  37. package/dist/cjs/internal/context/selection-store.js +76 -0
  38. package/dist/cjs/internal/hooks/use-checkbox-state.js +68 -0
  39. package/dist/cjs/internal/hooks/use-radio-state.js +84 -0
  40. package/dist/cjs/internal/hooks/use-register-item-with-focus-manager.js +29 -0
  41. package/dist/cjs/internal/utils/get-icon-colors.js +25 -0
  42. package/dist/cjs/internal/utils/handle-focus.js +58 -0
  43. package/dist/cjs/internal/utils/is-checkbox-item.js +11 -0
  44. package/dist/cjs/internal/utils/is-radio-item.js +11 -0
  45. package/dist/cjs/internal/utils/is-voice-over-supported.js +23 -0
  46. package/dist/cjs/internal/utils/reset-options-in-group.js +23 -0
  47. package/dist/cjs/radio/dropdown-item-radio-group.js +89 -0
  48. package/dist/cjs/radio/dropdown-item-radio.js +108 -0
  49. package/dist/cjs/version.json +1 -1
  50. package/dist/es2019/checkbox/dropdown-item-checkbox-group.js +21 -0
  51. package/dist/es2019/checkbox/dropdown-item-checkbox.js +67 -0
  52. package/dist/es2019/dropdown-menu-item-group.js +11 -0
  53. package/dist/es2019/dropdown-menu-item.js +49 -0
  54. package/dist/es2019/dropdown-menu.js +151 -0
  55. package/dist/es2019/index.js +7 -11
  56. package/dist/es2019/internal/components/focus-manager.js +40 -0
  57. package/dist/es2019/internal/components/menu-wrapper.js +44 -0
  58. package/dist/es2019/internal/context/checkbox-group-context.js +6 -0
  59. package/dist/es2019/internal/context/selection-store.js +54 -0
  60. package/dist/es2019/internal/hooks/use-checkbox-state.js +45 -0
  61. package/dist/es2019/internal/hooks/use-radio-state.js +56 -0
  62. package/dist/es2019/internal/hooks/use-register-item-with-focus-manager.js +19 -0
  63. package/dist/es2019/internal/utils/get-icon-colors.js +17 -0
  64. package/dist/es2019/internal/utils/handle-focus.js +48 -0
  65. package/dist/es2019/internal/utils/is-checkbox-item.js +4 -0
  66. package/dist/es2019/internal/utils/is-radio-item.js +4 -0
  67. package/dist/es2019/internal/utils/is-voice-over-supported.js +11 -0
  68. package/dist/es2019/internal/utils/reset-options-in-group.js +7 -0
  69. package/dist/es2019/radio/dropdown-item-radio-group.js +56 -0
  70. package/dist/es2019/radio/dropdown-item-radio.js +67 -0
  71. package/dist/es2019/version.json +1 -1
  72. package/dist/esm/checkbox/dropdown-item-checkbox-group.js +19 -0
  73. package/dist/esm/checkbox/dropdown-item-checkbox.js +81 -0
  74. package/dist/esm/dropdown-menu-item-group.js +11 -0
  75. package/dist/esm/dropdown-menu-item.js +52 -0
  76. package/dist/esm/dropdown-menu.js +168 -0
  77. package/dist/esm/index.js +7 -11
  78. package/dist/esm/internal/components/focus-manager.js +39 -0
  79. package/dist/esm/internal/components/menu-wrapper.js +45 -0
  80. package/dist/esm/internal/context/checkbox-group-context.js +6 -0
  81. package/dist/esm/internal/context/selection-store.js +58 -0
  82. package/dist/esm/internal/hooks/use-checkbox-state.js +55 -0
  83. package/dist/esm/internal/hooks/use-radio-state.js +70 -0
  84. package/dist/esm/internal/hooks/use-register-item-with-focus-manager.js +19 -0
  85. package/dist/esm/internal/utils/get-icon-colors.js +17 -0
  86. package/dist/esm/internal/utils/handle-focus.js +47 -0
  87. package/dist/esm/internal/utils/is-checkbox-item.js +4 -0
  88. package/dist/esm/internal/utils/is-radio-item.js +4 -0
  89. package/dist/esm/internal/utils/is-voice-over-supported.js +15 -0
  90. package/dist/esm/internal/utils/reset-options-in-group.js +13 -0
  91. package/dist/esm/radio/dropdown-item-radio-group.js +66 -0
  92. package/dist/esm/radio/dropdown-item-radio.js +81 -0
  93. package/dist/esm/version.json +1 -1
  94. package/dist/types/checkbox/dropdown-item-checkbox-group.d.ts +16 -0
  95. package/dist/types/checkbox/dropdown-item-checkbox.d.ts +13 -0
  96. package/dist/types/dropdown-menu-item-group.d.ts +11 -0
  97. package/dist/types/dropdown-menu-item.d.ts +13 -0
  98. package/dist/types/dropdown-menu.d.ts +13 -0
  99. package/dist/types/index.d.ts +8 -10
  100. package/dist/types/internal/components/focus-manager.d.ts +19 -0
  101. package/dist/types/internal/components/menu-wrapper.d.ts +11 -0
  102. package/dist/types/internal/context/checkbox-group-context.d.ts +5 -0
  103. package/dist/types/internal/context/selection-store.d.ts +27 -0
  104. package/dist/types/internal/hooks/use-checkbox-state.d.ts +14 -0
  105. package/dist/types/internal/hooks/use-radio-state.d.ts +9 -0
  106. package/dist/types/internal/hooks/use-register-item-with-focus-manager.d.ts +4 -0
  107. package/dist/types/internal/utils/get-icon-colors.d.ts +8 -0
  108. package/dist/types/internal/utils/handle-focus.d.ts +2 -0
  109. package/dist/types/internal/utils/is-checkbox-item.d.ts +1 -0
  110. package/dist/types/internal/utils/is-radio-item.d.ts +1 -0
  111. package/dist/types/internal/utils/is-voice-over-supported.d.ts +2 -0
  112. package/dist/types/internal/utils/reset-options-in-group.d.ts +4 -0
  113. package/dist/types/radio/dropdown-item-radio-group.d.ts +25 -0
  114. package/dist/types/radio/dropdown-item-radio.d.ts +13 -0
  115. package/dist/types/types.d.ts +254 -79
  116. package/package.json +39 -23
  117. package/dist/cjs/components/DropdownMenu.js +0 -230
  118. package/dist/cjs/components/DropdownMenuStateless.js +0 -523
  119. package/dist/cjs/components/context/DropdownItemClickManager.js +0 -72
  120. package/dist/cjs/components/context/DropdownItemFocusManager.js +0 -178
  121. package/dist/cjs/components/context/DropdownItemSelectionCache.js +0 -131
  122. package/dist/cjs/components/context/DropdownItemSelectionManager.js +0 -185
  123. package/dist/cjs/components/group/DropdownItemGroup.js +0 -61
  124. package/dist/cjs/components/group/DropdownItemGroupCheckbox.js +0 -16
  125. package/dist/cjs/components/group/DropdownItemGroupRadio.js +0 -16
  126. package/dist/cjs/components/group/ert-group-selection.js +0 -8
  127. package/dist/cjs/components/hoc/withItemSelectionManager.js +0 -66
  128. package/dist/cjs/components/hoc/withToggleInteraction.js +0 -175
  129. package/dist/cjs/components/item/DropdownItem.js +0 -19
  130. package/dist/cjs/components/item/DropdownItemCheckbox.js +0 -28
  131. package/dist/cjs/components/item/ert-item-checkbox.js +0 -8
  132. package/dist/cjs/components/item/ert-item-radio.js +0 -8
  133. package/dist/cjs/components/item/ert-item.js +0 -8
  134. package/dist/cjs/styled/WidthConstrainer.js +0 -21
  135. package/dist/cjs/util/contextNamespace.js +0 -19
  136. package/dist/cjs/util/getDisplayName.js +0 -14
  137. package/dist/cjs/util/keys.js +0 -18
  138. package/dist/cjs/util/safeContextCall.js +0 -27
  139. package/dist/cjs/util/supportsVoiceover.js +0 -17
  140. package/dist/es2019/components/DropdownMenu.js +0 -156
  141. package/dist/es2019/components/DropdownMenuStateless.js +0 -459
  142. package/dist/es2019/components/context/DropdownItemClickManager.js +0 -31
  143. package/dist/es2019/components/context/DropdownItemFocusManager.js +0 -134
  144. package/dist/es2019/components/context/DropdownItemSelectionCache.js +0 -68
  145. package/dist/es2019/components/context/DropdownItemSelectionManager.js +0 -140
  146. package/dist/es2019/components/group/DropdownItemGroup.js +0 -17
  147. package/dist/es2019/components/group/DropdownItemGroupCheckbox.js +0 -3
  148. package/dist/es2019/components/group/DropdownItemGroupRadio.js +0 -3
  149. package/dist/es2019/components/group/ert-group-selection.js +0 -1
  150. package/dist/es2019/components/hoc/withItemSelectionManager.js +0 -20
  151. package/dist/es2019/components/hoc/withToggleInteraction.js +0 -119
  152. package/dist/es2019/components/item/DropdownItem.js +0 -3
  153. package/dist/es2019/components/item/DropdownItemCheckbox.js +0 -5
  154. package/dist/es2019/components/item/DropdownItemRadio.js +0 -5
  155. package/dist/es2019/components/item/ert-item-checkbox.js +0 -1
  156. package/dist/es2019/components/item/ert-item-radio.js +0 -1
  157. package/dist/es2019/components/item/ert-item.js +0 -1
  158. package/dist/es2019/styled/WidthConstrainer.js +0 -6
  159. package/dist/es2019/util/contextNamespace.js +0 -6
  160. package/dist/es2019/util/getDisplayName.js +0 -4
  161. package/dist/es2019/util/keys.js +0 -6
  162. package/dist/es2019/util/safeContextCall.js +0 -10
  163. package/dist/es2019/util/supportsVoiceover.js +0 -5
  164. package/dist/esm/components/DropdownMenu.js +0 -215
  165. package/dist/esm/components/DropdownMenuStateless.js +0 -516
  166. package/dist/esm/components/context/DropdownItemClickManager.js +0 -59
  167. package/dist/esm/components/context/DropdownItemFocusManager.js +0 -164
  168. package/dist/esm/components/context/DropdownItemSelectionCache.js +0 -113
  169. package/dist/esm/components/context/DropdownItemSelectionManager.js +0 -174
  170. package/dist/esm/components/group/DropdownItemGroup.js +0 -43
  171. package/dist/esm/components/group/DropdownItemGroupCheckbox.js +0 -3
  172. package/dist/esm/components/group/DropdownItemGroupRadio.js +0 -3
  173. package/dist/esm/components/group/ert-group-selection.js +0 -1
  174. package/dist/esm/components/hoc/withItemSelectionManager.js +0 -47
  175. package/dist/esm/components/hoc/withToggleInteraction.js +0 -155
  176. package/dist/esm/components/item/DropdownItem.js +0 -3
  177. package/dist/esm/components/item/DropdownItemCheckbox.js +0 -7
  178. package/dist/esm/components/item/DropdownItemRadio.js +0 -7
  179. package/dist/esm/components/item/ert-item-checkbox.js +0 -1
  180. package/dist/esm/components/item/ert-item-radio.js +0 -1
  181. package/dist/esm/components/item/ert-item.js +0 -1
  182. package/dist/esm/styled/WidthConstrainer.js +0 -9
  183. package/dist/esm/util/contextNamespace.js +0 -8
  184. package/dist/esm/util/getDisplayName.js +0 -6
  185. package/dist/esm/util/keys.js +0 -6
  186. package/dist/esm/util/safeContextCall.js +0 -18
  187. package/dist/esm/util/supportsVoiceover.js +0 -9
  188. package/dist/types/components/DropdownMenu.d.ts +0 -36
  189. package/dist/types/components/DropdownMenuStateless.d.ts +0 -82
  190. package/dist/types/components/context/DropdownItemClickManager.d.ts +0 -19
  191. package/dist/types/components/context/DropdownItemFocusManager.d.ts +0 -35
  192. package/dist/types/components/context/DropdownItemSelectionCache.d.ts +0 -31
  193. package/dist/types/components/context/DropdownItemSelectionManager.d.ts +0 -34
  194. package/dist/types/components/group/DropdownItemGroup.d.ts +0 -12
  195. package/dist/types/components/group/DropdownItemGroupCheckbox.d.ts +0 -55
  196. package/dist/types/components/group/DropdownItemGroupRadio.d.ts +0 -55
  197. package/dist/types/components/group/ert-group-selection.d.ts +0 -6
  198. package/dist/types/components/hoc/withItemSelectionManager.d.ts +0 -63
  199. package/dist/types/components/hoc/withToggleInteraction.d.ts +0 -98
  200. package/dist/types/components/item/DropdownItem.d.ts +0 -65
  201. package/dist/types/components/item/DropdownItemCheckbox.d.ts +0 -80
  202. package/dist/types/components/item/DropdownItemRadio.d.ts +0 -80
  203. package/dist/types/components/item/ert-item-checkbox.d.ts +0 -2
  204. package/dist/types/components/item/ert-item-radio.d.ts +0 -2
  205. package/dist/types/components/item/ert-item.d.ts +0 -2
  206. package/dist/types/styled/WidthConstrainer.d.ts +0 -7
  207. package/dist/types/util/contextNamespace.d.ts +0 -4
  208. package/dist/types/util/getDisplayName.d.ts +0 -3
  209. package/dist/types/util/keys.d.ts +0 -6
  210. package/dist/types/util/safeContextCall.d.ts +0 -6
  211. package/dist/types/util/supportsVoiceover.d.ts +0 -2
@@ -0,0 +1,151 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+
3
+ /** @jsx jsx */
4
+ import { useCallback, useState } from 'react';
5
+ import { css, jsx } from '@emotion/core';
6
+ import Button from '@atlaskit/button/standard-button';
7
+ import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes';
8
+ import noop from '@atlaskit/ds-lib/noop';
9
+ import useControlledState from '@atlaskit/ds-lib/use-controlled';
10
+ import useFocus from '@atlaskit/ds-lib/use-focus-event';
11
+ import useKeydownEvent from '@atlaskit/ds-lib/use-keydown-event';
12
+ import ExpandIcon from '@atlaskit/icon/glyph/chevron-down';
13
+ import Popup from '@atlaskit/popup';
14
+ import Spinner from '@atlaskit/spinner';
15
+ import { gridSize as gridSizeFn, layers } from '@atlaskit/theme/constants';
16
+ import VisuallyHidden from '@atlaskit/visually-hidden';
17
+ import FocusManager from './internal/components/focus-manager';
18
+ import MenuWrapper from './internal/components/menu-wrapper';
19
+ import SelectionStore from './internal/context/selection-store';
20
+ const gridSize = gridSizeFn();
21
+ const spinnerContainerStyles = css({
22
+ display: 'flex',
23
+ minWidth: `${gridSize * 20}px`,
24
+ padding: `${gridSize * 2.5}px`,
25
+ justifyContent: 'center'
26
+ });
27
+ const MAX_HEIGHT = `calc(100vh - ${gridSize * 2}px)`;
28
+ /**
29
+ * __Dropdown menu__
30
+ *
31
+ * A dropdown menu displays a list of actions or options to a user.
32
+ *
33
+ * - [Examples](https://atlassian.design/components/dropdown-menu/examples)
34
+ * - [Code](https://atlassian.design/components/dropdown-menu/code)
35
+ * - [Usage](https://atlassian.design/components/dropdown-menu/usage)
36
+ */
37
+
38
+ const DropdownMenu = props => {
39
+ const {
40
+ defaultOpen = false,
41
+ isOpen,
42
+ onOpenChange = noop,
43
+ children,
44
+ placement = 'bottom-start',
45
+ trigger: Trigger,
46
+ shouldFlip = true,
47
+ isLoading = false,
48
+ autoFocus = false,
49
+ testId,
50
+ statusLabel = 'Loading'
51
+ } = props;
52
+ const [isLocalOpen, setLocalIsOpen] = useControlledState(isOpen, () => defaultOpen);
53
+ const [isTriggeredUsingKeyboard, setTriggeredUsingKeyboard] = useState(false);
54
+ const handleTriggerClicked = useCallback(event => {
55
+ const newValue = !isLocalOpen;
56
+ const {
57
+ clientX,
58
+ clientY,
59
+ type
60
+ } = event;
61
+
62
+ if (type === 'keydown') {
63
+ setTriggeredUsingKeyboard(true);
64
+ } else if (clientX === 0 || clientY === 0) {
65
+ // Hitting enter/space is registered as a click
66
+ // with both clientX and clientY === 0
67
+ setTriggeredUsingKeyboard(true);
68
+ }
69
+
70
+ setLocalIsOpen(newValue);
71
+ onOpenChange({
72
+ isOpen: newValue,
73
+ event
74
+ });
75
+ }, [onOpenChange, isLocalOpen, setLocalIsOpen]);
76
+ const handleOnClose = useCallback(() => {
77
+ const newValue = false;
78
+ setLocalIsOpen(newValue);
79
+ onOpenChange({
80
+ isOpen: newValue
81
+ });
82
+ }, [onOpenChange, setLocalIsOpen]);
83
+ const {
84
+ isFocused,
85
+ bindFocus
86
+ } = useFocus();
87
+
88
+ const handleDownArrow = e => {
89
+ if (e.key === KEY_DOWN) {
90
+ // prevent page scroll
91
+ e.preventDefault();
92
+ handleTriggerClicked(e);
93
+ }
94
+ };
95
+
96
+ useKeydownEvent(handleDownArrow, isFocused);
97
+
98
+ const renderTrigger = triggerProps => {
99
+ if (typeof Trigger === 'function') {
100
+ const {
101
+ ref,
102
+ ...providedProps
103
+ } = triggerProps;
104
+ return jsx(Trigger, _extends({}, providedProps, bindFocus, {
105
+ triggerRef: ref,
106
+ isSelected: isLocalOpen,
107
+ onClick: handleTriggerClicked,
108
+ testId: testId && `${testId}--trigger`
109
+ }));
110
+ }
111
+
112
+ return jsx(Button, _extends({}, triggerProps, bindFocus, {
113
+ isSelected: isLocalOpen,
114
+ iconAfter: jsx(ExpandIcon, {
115
+ size: "medium",
116
+ label: "expand"
117
+ }),
118
+ onClick: handleTriggerClicked,
119
+ testId: testId && `${testId}--trigger`
120
+ }), Trigger);
121
+ };
122
+
123
+ const fallbackPlacements = ['bottom', 'bottom-end', 'right-start', 'left-start', 'auto'];
124
+ return jsx(SelectionStore, null, jsx(Popup, {
125
+ shouldFlip: shouldFlip,
126
+ isOpen: isLocalOpen,
127
+ onClose: handleOnClose,
128
+ zIndex: layers.modal(),
129
+ placement: placement,
130
+ fallbackPlacements: fallbackPlacements,
131
+ testId: testId && `${testId}--content`,
132
+ trigger: renderTrigger,
133
+ shouldUseCaptureOnOutsideClick: true,
134
+ content: ({
135
+ setInitialFocusRef
136
+ }) => jsx(FocusManager, null, jsx(MenuWrapper, {
137
+ maxHeight: MAX_HEIGHT,
138
+ maxWidth: 800,
139
+ onClose: handleOnClose,
140
+ setInitialFocusRef: isTriggeredUsingKeyboard || autoFocus ? setInitialFocusRef : undefined
141
+ }, isLoading ? jsx("div", {
142
+ css: spinnerContainerStyles
143
+ }, jsx(Spinner, {
144
+ size: "small"
145
+ }), jsx(VisuallyHidden, {
146
+ role: "status"
147
+ }, statusLabel)) : children))
148
+ }));
149
+ };
150
+
151
+ export default DropdownMenu;
@@ -1,11 +1,7 @@
1
- // menu exports
2
- export { default } from './components/DropdownMenu';
3
- export { default as DropdownMenuStateless } from './components/DropdownMenuStateless'; // Item exports
4
-
5
- export { default as DropdownItem } from './components/item/DropdownItem';
6
- export { default as DropdownItemRadio } from './components/item/DropdownItemRadio';
7
- export { default as DropdownItemCheckbox } from './components/item/DropdownItemCheckbox'; // ItemGroup exports
8
-
9
- export { default as DropdownItemGroup } from './components/group/DropdownItemGroup';
10
- export { default as DropdownItemGroupRadio } from './components/group/DropdownItemGroupRadio';
11
- export { default as DropdownItemGroupCheckbox } from './components/group/DropdownItemGroupCheckbox'; // Types
1
+ export { default } from './dropdown-menu';
2
+ export { default as DropdownItemGroup } from './dropdown-menu-item-group';
3
+ export { default as DropdownItem } from './dropdown-menu-item';
4
+ export { default as DropdownItemCheckbox } from './checkbox/dropdown-item-checkbox';
5
+ export { default as DropdownItemCheckboxGroup } from './checkbox/dropdown-item-checkbox-group';
6
+ export { default as DropdownItemRadio } from './radio/dropdown-item-radio';
7
+ export { default as DropdownItemRadioGroup } from './radio/dropdown-item-radio-group';
@@ -0,0 +1,40 @@
1
+ import React, { createContext, useCallback, useRef } from 'react';
2
+ import useKeydownEvent from '@atlaskit/ds-lib/use-keydown-event';
3
+ import handleFocus from '../utils/handle-focus';
4
+ /**
5
+ *
6
+ *
7
+ * Context provider which maintains the list of focusable elements and a method to
8
+ * register new menu items.
9
+ * This list drives the keyboard navgation of the menu.
10
+ *
11
+ */
12
+
13
+ export const FocusManagerContext = /*#__PURE__*/createContext({
14
+ menuItemRefs: [],
15
+ registerRef: ref => {}
16
+ });
17
+ /**
18
+ * Focus manager logic
19
+ */
20
+
21
+ const FocusManager = ({
22
+ children
23
+ }) => {
24
+ const menuItemRefs = useRef([]);
25
+ const registerRef = useCallback(ref => {
26
+ if (ref && !menuItemRefs.current.includes(ref)) {
27
+ menuItemRefs.current.push(ref);
28
+ }
29
+ }, []);
30
+ useKeydownEvent(handleFocus(menuItemRefs.current));
31
+ const contextValue = {
32
+ menuItemRefs: menuItemRefs.current,
33
+ registerRef
34
+ };
35
+ return /*#__PURE__*/React.createElement(FocusManagerContext.Provider, {
36
+ value: contextValue
37
+ }, children);
38
+ };
39
+
40
+ export default FocusManager;
@@ -0,0 +1,44 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import React, { useContext } from 'react';
3
+ import MenuGroup from '@atlaskit/menu/menu-group';
4
+ import { FocusManagerContext } from '../components/focus-manager';
5
+ import isCheckboxItem from '../utils/is-checkbox-item';
6
+ import isRadioItem from '../utils/is-radio-item';
7
+ /**
8
+ *
9
+ * MenuWrapper wraps all the menu items.
10
+ * It handles the logic to close the menu when a MenuItem is clicked, but leaves it open
11
+ * if a CheckboxItem or RadioItem is clicked.
12
+ * It also sets focus to the first menu item when opened.
13
+ */
14
+
15
+ const MenuWrapper = ({
16
+ onClose,
17
+ setInitialFocusRef,
18
+ ...props
19
+ }) => {
20
+ const {
21
+ menuItemRefs
22
+ } = useContext(FocusManagerContext);
23
+
24
+ const closeOnMenuItemClick = e => {
25
+ const isTargetMenuItemOrDecendant = menuItemRefs.some(menuItem => {
26
+ const isCheckboxOrRadio = isCheckboxItem(menuItem) || isRadioItem(menuItem);
27
+ return menuItem.contains(e.target) && !isCheckboxOrRadio;
28
+ }); // Close menu if the click is triggered from a MenuItem or
29
+ // its decendant. Don't close the menu if the click is triggered
30
+ // from a MenuItemRadio or MenuItemCheckbox so that the user can
31
+ // select multiple items.
32
+
33
+ if (isTargetMenuItemOrDecendant && onClose) {
34
+ onClose();
35
+ }
36
+ };
37
+
38
+ setInitialFocusRef && setInitialFocusRef(menuItemRefs[0]);
39
+ return /*#__PURE__*/React.createElement(MenuGroup, _extends({
40
+ onClick: closeOnMenuItemClick
41
+ }, props));
42
+ };
43
+
44
+ export default MenuWrapper;
@@ -0,0 +1,6 @@
1
+ import { createContext } from 'react';
2
+ /**
3
+ * Holds the unique identifier for the checkbox group.
4
+ */
5
+
6
+ export const CheckboxGroupContext = /*#__PURE__*/createContext('');
@@ -0,0 +1,54 @@
1
+ import React, { createContext, useMemo, useRef } from 'react';
2
+ import noop from '@atlaskit/ds-lib/noop';
3
+
4
+ /**
5
+ *
6
+ * SelectionStoreContext maintains the state of the selected items
7
+ * and getter setters.
8
+ *
9
+ */
10
+ export const SelectionStoreContext = /*#__PURE__*/createContext({
11
+ setItemState: noop,
12
+ getItemState: () => undefined,
13
+ setGroupState: noop,
14
+ getGroupState: () => ({})
15
+ });
16
+
17
+ /**
18
+ * Selection store will persist data as long as it remains mounted.
19
+ * It handles the uncontrolled story for dropdown menu when the menu
20
+ * items can be mounted/unmounted depending if the menu is open or closed.
21
+ */
22
+ const SelectionStore = props => {
23
+ const {
24
+ children
25
+ } = props;
26
+ const store = useRef({});
27
+ const context = useMemo(() => ({
28
+ setItemState: (group, id, value) => {
29
+ if (!store.current[group]) {
30
+ store.current[group] = {};
31
+ }
32
+
33
+ store.current[group][id] = value;
34
+ },
35
+ getItemState: (group, id) => {
36
+ if (!store.current[group]) {
37
+ return undefined;
38
+ }
39
+
40
+ return store.current[group][id];
41
+ },
42
+ setGroupState: (group, value) => {
43
+ store.current[group] = value;
44
+ },
45
+ getGroupState: group => {
46
+ return store.current[group] || {};
47
+ }
48
+ }), []);
49
+ return /*#__PURE__*/React.createElement(SelectionStoreContext.Provider, {
50
+ value: context
51
+ }, children);
52
+ };
53
+
54
+ export default SelectionStore;
@@ -0,0 +1,45 @@
1
+ import { useCallback, useContext, useState } from 'react';
2
+ import { CheckboxGroupContext } from '../context/checkbox-group-context';
3
+ import { SelectionStoreContext } from '../context/selection-store';
4
+
5
+ /**
6
+ * Custom hook to handle checkbox state for dropdown menu.
7
+ * It works in tandem with the selection store context when the
8
+ * component is uncontrolled.
9
+ */
10
+ const useCheckboxState = ({
11
+ isSelected,
12
+ id,
13
+ defaultSelected
14
+ }) => {
15
+ const {
16
+ setItemState,
17
+ getItemState
18
+ } = useContext(SelectionStoreContext);
19
+ const groupId = useContext(CheckboxGroupContext);
20
+ const persistedIsSelected = getItemState(groupId, id);
21
+ const [localIsSelected, setLocalIsSelected] = useState( // Initial state is set depending on value being defined or not.
22
+ // This state is only utilised if the checkbox is uncontrolled.
23
+ () => persistedIsSelected !== undefined ? persistedIsSelected : defaultSelected || false);
24
+ const setLocalState = useCallback(newValue => {
25
+ const nextValue = newValue(persistedIsSelected);
26
+ setLocalIsSelected(nextValue);
27
+ setItemState(groupId, id, nextValue);
28
+ }, [setItemState, persistedIsSelected, groupId, id]); // Checkbox is controlled - do nothing!
29
+
30
+ if (typeof isSelected === 'boolean') {
31
+ return [isSelected, () => false];
32
+ } // Checkbox is going through its first render pass!
33
+
34
+
35
+ if (persistedIsSelected === undefined) {
36
+ // Set the item so we have this state to access next time the checkbox renders (either by mounting or re-rendering!)
37
+ setItemState(groupId, id, defaultSelected || false);
38
+ } // Return the value and setter!
39
+ // Remember this flow is only returned if the checkbox is uncontrolled.
40
+
41
+
42
+ return [localIsSelected, setLocalState];
43
+ };
44
+
45
+ export default useCheckboxState;
@@ -0,0 +1,56 @@
1
+ import { useCallback, useContext, useEffect, useState } from 'react';
2
+ import { RadioGroupContext } from '../../radio/dropdown-item-radio-group';
3
+ import { SelectionStoreContext } from '../context/selection-store';
4
+ import resetOptionsInGroup from '../utils/reset-options-in-group';
5
+
6
+ function useRadioState({
7
+ id,
8
+ isSelected,
9
+ defaultSelected
10
+ }) {
11
+ const {
12
+ setGroupState,
13
+ getGroupState
14
+ } = useContext(SelectionStoreContext);
15
+ const {
16
+ id: group,
17
+ radioGroupState,
18
+ selectRadioItem
19
+ } = useContext(RadioGroupContext);
20
+ const persistedIsSelected = radioGroupState[id];
21
+ const [localIsSelected, setLocalIsSelected] = useState(() => persistedIsSelected !== undefined ? persistedIsSelected : defaultSelected || false);
22
+ const setLocalState = useCallback(newValue => {
23
+ if (!persistedIsSelected) {
24
+ const nextValue = newValue(persistedIsSelected);
25
+ selectRadioItem(id, nextValue);
26
+ setLocalIsSelected(nextValue);
27
+ }
28
+ }, [persistedIsSelected, id, selectRadioItem]);
29
+ /**
30
+ * - radio state changes any time a radio child is changed
31
+ * - when it changes we want all radio buttons to update their local state
32
+ * - it takes the value from radio group state and applies it locally if it exists which it will only exist, if it is selected
33
+ */
34
+
35
+ useEffect(() => {
36
+ setLocalIsSelected(() => {
37
+ const existing = radioGroupState[id];
38
+ return existing !== undefined ? existing : defaultSelected || false;
39
+ });
40
+ }, [radioGroupState, group, id, defaultSelected]);
41
+
42
+ if (typeof isSelected === 'boolean') {
43
+ return [isSelected, () => false];
44
+ }
45
+
46
+ if (persistedIsSelected === undefined) {
47
+ const existing = getGroupState(group);
48
+ setGroupState(group, { ...resetOptionsInGroup(existing || {}),
49
+ [id]: defaultSelected || false
50
+ });
51
+ }
52
+
53
+ return [localIsSelected, setLocalState];
54
+ }
55
+
56
+ export default useRadioState;
@@ -0,0 +1,19 @@
1
+ import { useContext, useEffect, useRef } from 'react';
2
+ import { FocusManagerContext } from '../components/focus-manager'; // This function is called whenever a MenuItem mounts.
3
+ // The refs stored in the context are used to programatically
4
+ // control focus on a user navigates using the keyboard.
5
+
6
+ function useRegisterItemWithFocusManager() {
7
+ const {
8
+ registerRef
9
+ } = useContext(FocusManagerContext);
10
+ const itemRef = useRef(null);
11
+ useEffect(() => {
12
+ if (itemRef.current !== null) {
13
+ registerRef(itemRef.current);
14
+ }
15
+ }, [registerRef]);
16
+ return itemRef;
17
+ }
18
+
19
+ export default useRegisterItemWithFocusManager;
@@ -0,0 +1,17 @@
1
+ import { B400, N40 } from '@atlaskit/theme/colors';
2
+
3
+ const getIconColors = isSelected => {
4
+ if (isSelected) {
5
+ return {
6
+ primary: `var(--ds-background-boldBrand-resting, ${B400})`,
7
+ secondary: `var(--ds-background-default, ${N40})`
8
+ };
9
+ }
10
+
11
+ return {
12
+ primary: `var(--ds-border-neutral, ${N40})`,
13
+ secondary: `var(--ds-UNSAFE_util-transparent, ${N40})`
14
+ };
15
+ };
16
+
17
+ export default getIconColors;
@@ -0,0 +1,48 @@
1
+ import { KEY_DOWN, KEY_END, KEY_HOME, KEY_UP } from '@atlaskit/ds-lib/keycodes';
2
+ const actionMap = {
3
+ [KEY_DOWN]: 'next',
4
+ [KEY_UP]: 'prev',
5
+ [KEY_HOME]: 'first',
6
+ [KEY_END]: 'last'
7
+ };
8
+ export default function handleFocus(refs) {
9
+ return e => {
10
+ const currentFocusedIdx = refs.findIndex(el => {
11
+ var _document$activeEleme;
12
+
13
+ return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.isSameNode(el);
14
+ });
15
+ const action = actionMap[e.key];
16
+
17
+ switch (action) {
18
+ case 'next':
19
+ if (currentFocusedIdx < refs.length - 1) {
20
+ e.preventDefault();
21
+ refs[currentFocusedIdx + 1].focus();
22
+ }
23
+
24
+ break;
25
+
26
+ case 'prev':
27
+ if (currentFocusedIdx > 0) {
28
+ e.preventDefault();
29
+ refs[currentFocusedIdx - 1].focus();
30
+ }
31
+
32
+ break;
33
+
34
+ case 'first':
35
+ e.preventDefault();
36
+ refs[0].focus();
37
+ break;
38
+
39
+ case 'last':
40
+ e.preventDefault();
41
+ refs[refs.length - 1].focus();
42
+ break;
43
+
44
+ default:
45
+ return;
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,4 @@
1
+ export default function isCheckboxItem(element) {
2
+ const role = element.getAttribute('role');
3
+ return role === 'checkbox' || role === 'menuitemcheckbox';
4
+ }
@@ -0,0 +1,4 @@
1
+ export default function isCheckboxItem(element) {
2
+ const role = element.getAttribute('role');
3
+ return role === 'radio' || role === 'menuitemradio';
4
+ }
@@ -0,0 +1,11 @@
1
+ const canUseDOM = () => Boolean(typeof window !== 'undefined' && window.document && window.document.createElement);
2
+ /*
3
+ * Making sure we can fallback to browsers doesn't support voice over -
4
+ * we would using menuitemcheckbox / menuitemradio for these that supports voice over, and
5
+ * will fallback to checkbox / radio for these doesn't
6
+ */
7
+
8
+
9
+ const isVoiceOverSupported = () => /Mac OS X/.test(canUseDOM() ? navigator.userAgent : '');
10
+
11
+ export default isVoiceOverSupported;
@@ -0,0 +1,7 @@
1
+ const resetOptionsInGroup = group => {
2
+ return Object.keys(group || {}).reduce((accumulator, current) => ({ ...accumulator,
3
+ [current]: typeof group[current] === 'undefined' ? undefined : false
4
+ }), {});
5
+ };
6
+
7
+ export default resetOptionsInGroup;
@@ -0,0 +1,56 @@
1
+ import React, { createContext, useContext, useState } from 'react';
2
+ import noop from '@atlaskit/ds-lib/noop';
3
+ import Section from '@atlaskit/menu/section';
4
+ import { SelectionStoreContext } from '../internal/context/selection-store';
5
+ import resetOptionsInGroup from '../internal/utils/reset-options-in-group';
6
+
7
+ /**
8
+ * __Radio group context__
9
+ * Context provider that wraps each DropdownItemRadioGroup
10
+ */
11
+ export const RadioGroupContext = /*#__PURE__*/createContext({
12
+ id: '',
13
+ radioGroupState: {},
14
+ selectRadioItem: noop
15
+ });
16
+ /**
17
+ * __Dropdown item radio group__
18
+ * Store which manages the selection state for each DropdownItemRadio
19
+ * across mount and unmounts.
20
+ *
21
+ */
22
+
23
+ const DropdownItemRadioGroup = props => {
24
+ const {
25
+ children,
26
+ id
27
+ } = props;
28
+ const {
29
+ setGroupState,
30
+ getGroupState
31
+ } = useContext(SelectionStoreContext);
32
+ /**
33
+ * - initially `radioGroupState` is from selection store, so it's safe to update without re-rendering
34
+ * - we flush a render by updating this local radio group state
35
+ */
36
+
37
+ const [radioGroupState, setRadioGroupState] = useState(() => getGroupState(id));
38
+
39
+ const selectRadioItem = (childId, value) => {
40
+ const newValue = { ...resetOptionsInGroup(getGroupState(id)),
41
+ [childId]: value
42
+ };
43
+ setRadioGroupState(newValue);
44
+ setGroupState(id, newValue);
45
+ };
46
+
47
+ return /*#__PURE__*/React.createElement(RadioGroupContext.Provider, {
48
+ value: {
49
+ id,
50
+ radioGroupState,
51
+ selectRadioItem
52
+ }
53
+ }, /*#__PURE__*/React.createElement(Section, props, children));
54
+ };
55
+
56
+ export default DropdownItemRadioGroup;
@@ -0,0 +1,67 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import React, { useCallback, useEffect, useState } from 'react';
3
+ import noop from '@atlaskit/ds-lib/noop';
4
+ import RadioIcon from '@atlaskit/icon/glyph/radio';
5
+ import ButtonItem from '@atlaskit/menu/button-item';
6
+ import useRadioState from '../internal/hooks/use-radio-state';
7
+ import useRegisterItemWithFocusManager from '../internal/hooks/use-register-item-with-focus-manager';
8
+ import getIconColors from '../internal/utils/get-icon-colors';
9
+ import isVoiceOverSupported from '../internal/utils/is-voice-over-supported';
10
+
11
+ /**
12
+ * __Dropdown item radio__
13
+ *
14
+ * A dropdown item radio displays groups that have a single selection.
15
+ *
16
+ * - [Examples](https://atlassian.design/components/dropdown-menu/dropdown-item-radio/examples)
17
+ * - [Code](https://atlassian.design/components/dropdown-menu/dropdown-item-radio/code)
18
+ * - [Usage](https://atlassian.design/components/dropdown-menu/dropdown-item-radio/usage)
19
+ */
20
+ const DropdownItemRadio = props => {
21
+ const {
22
+ id,
23
+ isSelected,
24
+ defaultSelected,
25
+ onClick: providedOnClick = noop,
26
+ shouldTitleWrap = true,
27
+ shouldDescriptionWrap = true,
28
+ ...rest
29
+ } = props;
30
+
31
+ if (process.env.NODE_ENV !== 'production' && typeof isSelected !== 'undefined' && typeof defaultSelected !== 'undefined') {
32
+ // eslint-disable-next-line no-console
33
+ console.warn("[DropdownItemRadio] You've used both `defaultSelected` and `isSelected` props. This is dangerous and can lead to unexpected results. Use one or the other depending if you want to control the components state yourself.");
34
+ }
35
+
36
+ const [selected, setSelected] = useRadioState({
37
+ id,
38
+ isSelected,
39
+ defaultSelected
40
+ });
41
+ const [iconColors, setIconColors] = useState(getIconColors(defaultSelected));
42
+ const onClickHandler = useCallback(event => {
43
+ setSelected(selected => !selected);
44
+ providedOnClick(event);
45
+ }, [providedOnClick, setSelected]);
46
+ useEffect(() => {
47
+ setIconColors(getIconColors(selected));
48
+ }, [selected]);
49
+ const itemRef = useRegisterItemWithFocusManager();
50
+ return /*#__PURE__*/React.createElement(ButtonItem, _extends({
51
+ id: id,
52
+ onClick: onClickHandler,
53
+ role: isVoiceOverSupported() ? 'radio' : 'menuitemradio',
54
+ "aria-checked": selected,
55
+ shouldTitleWrap: shouldTitleWrap,
56
+ shouldDescriptionWrap: shouldDescriptionWrap,
57
+ iconBefore: /*#__PURE__*/React.createElement(RadioIcon, {
58
+ label: "",
59
+ size: "medium",
60
+ primaryColor: iconColors.primary,
61
+ secondaryColor: iconColors.secondary
62
+ }),
63
+ ref: itemRef
64
+ }, rest));
65
+ };
66
+
67
+ export default DropdownItemRadio;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/dropdown-menu",
3
- "version": "10.1.8",
3
+ "version": "11.0.2",
4
4
  "sideEffects": false
5
5
  }