@dhis2-ui/menu 9.9.0-alpha.1 → 9.9.1

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.
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+
3
+ var _react = require("@testing-library/react");
4
+
5
+ var _userEvent = _interopRequireDefault(require("@testing-library/user-event"));
6
+
7
+ var _enzyme = require("enzyme");
8
+
9
+ var _react2 = _interopRequireDefault(require("react"));
10
+
11
+ var _menuDivider = require("../../menu-divider/menu-divider.js");
12
+
13
+ var _menuItem = require("../../menu-item/menu-item.js");
14
+
15
+ var _menuSectionHeader = require("../../menu-section-header/menu-section-header.js");
16
+
17
+ var _menu = require("../menu.js");
18
+
19
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20
+
21
+ describe('Menu Component', () => {
22
+ const menuDataTest = 'data-test-menu';
23
+ const menuItemDataTest = 'data-test-menu-item';
24
+ const dividerDataTest = 'data-test-menu-divider';
25
+ it('Menu and menu items have aria attributes', () => {
26
+ const wrapper = (0, _enzyme.mount)( /*#__PURE__*/_react2.default.createElement(_menu.Menu, {
27
+ dataTest: menuDataTest,
28
+ dense: false
29
+ }, /*#__PURE__*/_react2.default.createElement(_menuSectionHeader.MenuSectionHeader, {
30
+ label: "Header"
31
+ }), /*#__PURE__*/_react2.default.createElement(_menuDivider.MenuDivider, {
32
+ dataTest: dividerDataTest
33
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
34
+ dataTest: menuItemDataTest,
35
+ label: "Menu item"
36
+ })));
37
+ const menuElement = wrapper.find({
38
+ 'data-test': menuDataTest
39
+ });
40
+ const menuItem = wrapper.find({
41
+ 'data-test': menuItemDataTest
42
+ });
43
+ const menuDivider = wrapper.find({
44
+ 'data-test': dividerDataTest
45
+ });
46
+ expect(menuElement.prop('role')).toBe('menu');
47
+ expect(menuItem.childAt(0).props().role).toBe('menuitem');
48
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Menu item');
49
+ expect(menuDivider.prop('role')).toBe('separator');
50
+ });
51
+ it('Empty menu has role menu', () => {
52
+ const menuDataTest = 'data-test-menu';
53
+ const wrapper = (0, _enzyme.mount)( /*#__PURE__*/_react2.default.createElement(_menu.Menu, {
54
+ dataTest: menuDataTest,
55
+ dense: false
56
+ }));
57
+ const menuElement = wrapper.find({
58
+ 'data-test': menuDataTest
59
+ });
60
+ expect(menuElement.prop('role')).toBe('menu');
61
+ });
62
+ it('can handle focus of first focusable element when tabbed to', () => {
63
+ const {
64
+ getByRole,
65
+ getByText
66
+ } = (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_menu.Menu, {
67
+ dataTest: menuDataTest,
68
+ dense: false
69
+ }, /*#__PURE__*/_react2.default.createElement(_menuSectionHeader.MenuSectionHeader, {
70
+ label: "Header"
71
+ }), /*#__PURE__*/_react2.default.createElement(_menuDivider.MenuDivider, {
72
+ dataTest: dividerDataTest
73
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
74
+ dataTest: menuItemDataTest,
75
+ label: "Menu item 1"
76
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
77
+ dataTest: menuItemDataTest,
78
+ label: "Menu item 2"
79
+ })));
80
+ const menu = getByRole('menu');
81
+ const divider = getByRole('separator');
82
+ const header = getByText(/Header/i);
83
+ const menuItem1 = getByText(/Menu item 1/i);
84
+ const menuItem2 = getByText(/Menu item 2/i);
85
+ expect(menu).not.toHaveFocus();
86
+
87
+ _userEvent.default.tab(); // check if LI parent node has focus or not
88
+ // headers and dividers do not receive focus
89
+
90
+
91
+ expect(header.parentNode.parentNode).not.toHaveFocus();
92
+ expect(divider.parentNode.parentNode).not.toHaveFocus();
93
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus();
94
+ expect(menuItem1.parentNode.parentNode).toHaveFocus();
95
+ });
96
+ it('can handle arrow down key navigation', async () => {
97
+ const {
98
+ getByText
99
+ } = (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_menu.Menu, {
100
+ dataTest: menuDataTest,
101
+ dense: false
102
+ }, /*#__PURE__*/_react2.default.createElement(_menuSectionHeader.MenuSectionHeader, {
103
+ label: "Header"
104
+ }), /*#__PURE__*/_react2.default.createElement(_menuDivider.MenuDivider, {
105
+ dataTest: dividerDataTest
106
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
107
+ dataTest: menuItemDataTest,
108
+ label: "Menu item 1"
109
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
110
+ dataTest: menuItemDataTest,
111
+ label: "Menu item 2"
112
+ })));
113
+ const menuItem1 = getByText(/Menu item 1/i);
114
+ const menuItem2 = getByText(/Menu item 2/i);
115
+
116
+ _userEvent.default.tab();
117
+
118
+ expect(menuItem1.parentNode.parentNode).toHaveFocus(); // simulate arrowDown press
119
+
120
+ _userEvent.default.keyboard('{ArrowDown}');
121
+
122
+ expect(menuItem1.parentNode.parentNode).not.toHaveFocus();
123
+ expect(menuItem2.parentNode.parentNode).toHaveFocus();
124
+
125
+ _userEvent.default.keyboard('{ArrowDown}');
126
+
127
+ expect(menuItem1.parentNode.parentNode).toHaveFocus();
128
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus();
129
+ });
130
+ it('can handle arrow up key navigation', async () => {
131
+ const {
132
+ getByText
133
+ } = (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_menu.Menu, {
134
+ dataTest: menuDataTest,
135
+ dense: false
136
+ }, /*#__PURE__*/_react2.default.createElement(_menuSectionHeader.MenuSectionHeader, {
137
+ label: "Header"
138
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
139
+ dataTest: menuItemDataTest,
140
+ label: "Menu item 1"
141
+ }), /*#__PURE__*/_react2.default.createElement(_menuDivider.MenuDivider, {
142
+ dataTest: dividerDataTest
143
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
144
+ dataTest: menuItemDataTest,
145
+ label: "Menu item 2"
146
+ })));
147
+ const menuItem1 = getByText(/Menu item 1/i);
148
+ const menuItem2 = getByText(/Menu item 2/i);
149
+
150
+ _userEvent.default.tab();
151
+
152
+ expect(menuItem1.parentNode.parentNode).toHaveFocus(); // simulate arrowUp press
153
+
154
+ _userEvent.default.keyboard('{ArrowUp}');
155
+
156
+ expect(menuItem1.parentNode.parentNode).not.toHaveFocus();
157
+ expect(menuItem2.parentNode.parentNode).toHaveFocus();
158
+
159
+ _userEvent.default.keyboard('{ArrowUp}');
160
+
161
+ expect(menuItem1.parentNode.parentNode).toHaveFocus();
162
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus();
163
+ });
164
+ it('can handle space and enter key presses for clickable menu items', async () => {
165
+ const onClick = jest.fn();
166
+ const {
167
+ getByText
168
+ } = (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_menu.Menu, {
169
+ dataTest: menuDataTest,
170
+ dense: false
171
+ }, /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
172
+ onClick: onClick,
173
+ value: "myValue",
174
+ label: "Click menu item"
175
+ })));
176
+ const clickableItem = getByText(/Click menu item/i);
177
+
178
+ _userEvent.default.tab();
179
+
180
+ expect(clickableItem.parentNode.parentNode).toHaveFocus();
181
+
182
+ _userEvent.default.keyboard('[Space]');
183
+
184
+ expect(onClick).toHaveBeenCalledTimes(1);
185
+
186
+ _userEvent.default.keyboard('{Enter}');
187
+
188
+ expect(onClick).toHaveBeenCalledTimes(2);
189
+ });
190
+ it('can handle non MenuItem components', () => {
191
+ const onClick = jest.fn();
192
+ const {
193
+ getByText
194
+ } = (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_menu.Menu, {
195
+ dataTest: menuDataTest,
196
+ dense: false
197
+ }, /*#__PURE__*/_react2.default.createElement("span", {
198
+ role: "menuitem",
199
+ onClick: onClick
200
+ }, "Span 1"), /*#__PURE__*/_react2.default.createElement("li", {
201
+ tabIndex: -1
202
+ }, /*#__PURE__*/_react2.default.createElement("a", {
203
+ href: "#",
204
+ role: "menuitem"
205
+ }, "Link 2")), /*#__PURE__*/_react2.default.createElement("li", null, /*#__PURE__*/_react2.default.createElement("span", {
206
+ onClick: onClick
207
+ }, "Span 2"))));
208
+ const nonListMenuItem = getByText(/span 1/i);
209
+ const listMenuItem = getByText(/link 2/i);
210
+ const plainListItem = getByText(/span 2/i); // all children must be list items
211
+
212
+ expect(nonListMenuItem.parentElement.nodeName).toBe('LI');
213
+
214
+ _userEvent.default.tab();
215
+
216
+ expect(nonListMenuItem.parentElement).toHaveFocus();
217
+ expect(nonListMenuItem.parentElement.tabIndex).toBe(0);
218
+ expect(onClick).toHaveBeenCalledTimes(0);
219
+
220
+ _userEvent.default.keyboard('[Space]');
221
+
222
+ expect(onClick).toHaveBeenCalledTimes(1);
223
+
224
+ _userEvent.default.keyboard('{ArrowDown}');
225
+
226
+ expect(listMenuItem.parentElement).toHaveFocus();
227
+
228
+ _userEvent.default.keyboard('{ArrowDown}');
229
+
230
+ expect(nonListMenuItem.parentElement).toHaveFocus(); // non menu items do not receive focus
231
+
232
+ expect(plainListItem.parentElement).not.toHaveFocus();
233
+ });
234
+ });
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.hasMenuItemRole = exports.getFocusableItemsIndices = void 0;
7
+
8
+ const isMenuItem = role => {
9
+ return ['menuitem', 'menuitemcheckbox', 'menuitemradio'].includes(role);
10
+ };
11
+
12
+ const isValidMenuItemNode = node => {
13
+ if (node.nodeName === 'LI' && node.firstElementChild) {
14
+ return isValidMenuItemNode(node.firstElementChild);
15
+ }
16
+
17
+ const role = node.getAttribute('role');
18
+ return role && isMenuItem(role);
19
+ };
20
+
21
+ const getFocusableItemsIndices = elements => {
22
+ const focusableIndices = [];
23
+ elements.forEach((node, index) => {
24
+ if (isValidMenuItemNode(node)) {
25
+ focusableIndices.push(index);
26
+ }
27
+ });
28
+ return focusableIndices;
29
+ };
30
+
31
+ exports.getFocusableItemsIndices = getFocusableItemsIndices;
32
+
33
+ const hasMenuItemRole = component => {
34
+ var _component$props;
35
+
36
+ return isMenuItem(component === null || component === void 0 ? void 0 : (_component$props = component.props) === null || _component$props === void 0 ? void 0 : _component$props['role']);
37
+ };
38
+
39
+ exports.hasMenuItemRole = hasMenuItemRole;
@@ -11,6 +11,10 @@ var _propTypes = _interopRequireDefault(require("prop-types"));
11
11
 
12
12
  var _react = _interopRequireWildcard(require("react"));
13
13
 
14
+ var _helpers = require("./helpers.js");
15
+
16
+ var _useMenu = require("./use-menu.js");
17
+
14
18
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
15
19
 
16
20
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
@@ -24,15 +28,55 @@ const Menu = _ref => {
24
28
  dataTest,
25
29
  dense
26
30
  } = _ref;
31
+ const {
32
+ menuRef,
33
+ focusedIndex
34
+ } = (0, _useMenu.useMenuNavigation)(children);
35
+
36
+ const childrenToRender = _react.Children.map(children, (child, index) => {
37
+ if (! /*#__PURE__*/(0, _react.isValidElement)(child)) {
38
+ return child;
39
+ }
40
+
41
+ const tabIndex = index === focusedIndex ? 0 : -1;
42
+ const childProps = { ...child.props
43
+ };
44
+
45
+ if (typeof child.type === 'string') {
46
+ // remove non-native props from native HTML elements
47
+ delete childProps.hideDivider;
48
+ delete childProps.dense;
49
+ delete childProps.active; // all ul children must be li elements
50
+ // add tabindex for focus to those elements that are/contain a menuitem
51
+
52
+ if (child.type === 'li') {
53
+ return (0, _helpers.hasMenuItemRole)(child.props.children[0]) ? /*#__PURE__*/(0, _react.cloneElement)(child, { ...childProps,
54
+ tabIndex
55
+ }) : /*#__PURE__*/(0, _react.cloneElement)(child, childProps);
56
+ } else {
57
+ return /*#__PURE__*/_react.default.createElement("li", {
58
+ tabIndex: (0, _helpers.hasMenuItemRole)(child) ? tabIndex : null
59
+ }, /*#__PURE__*/(0, _react.cloneElement)(child, childProps));
60
+ }
61
+ } else {
62
+ // assign non-native props to custom elements
63
+ childProps.dense = typeof child.props.dense === 'boolean' ? child.props.dense : dense;
64
+ childProps.hideDivider = typeof child.props.hideDivider !== 'boolean' && index === 0 ? true : child.props.hideDivider;
65
+ return /*#__PURE__*/(0, _react.cloneElement)(child, { ...childProps,
66
+ tabIndex
67
+ });
68
+ }
69
+ });
70
+
27
71
  return /*#__PURE__*/_react.default.createElement("ul", {
28
72
  "data-test": dataTest,
29
- className: "jsx-3549878755" + " " + (className || "")
30
- }, _react.Children.map(children, (child, index) => /*#__PURE__*/(0, _react.isValidElement)(child) ? /*#__PURE__*/(0, _react.cloneElement)(child, {
31
- dense: typeof child.props.dense === 'boolean' ? child.props.dense : dense,
32
- hideDivider: typeof child.props.hideDivider !== 'boolean' && index === 0 ? true : child.props.hideDivider
33
- }) : child), /*#__PURE__*/_react.default.createElement(_style.default, {
34
- id: "3549878755"
35
- }, ["ul.jsx-3549878755{display:block;position:relative;width:100%;margin:0;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}"]));
73
+ role: "menu",
74
+ ref: menuRef,
75
+ tabIndex: 0,
76
+ className: "jsx-1636612837" + " " + (className || "")
77
+ }, childrenToRender, /*#__PURE__*/_react.default.createElement(_style.default, {
78
+ id: "1636612837"
79
+ }, ["ul.jsx-1636612837{display:block;position:relative;width:100%;margin:0;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}"]));
36
80
  };
37
81
 
38
82
  exports.Menu = Menu;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useMenuNavigation = void 0;
7
+
8
+ var _react = require("react");
9
+
10
+ var _helpers = require("./helpers.js");
11
+
12
+ const useMenuNavigation = children => {
13
+ const menuRef = (0, _react.useRef)(null);
14
+ const [focusableItemsIndices, setFocusableItemsIndices] = (0, _react.useState)(null);
15
+ const [activeItemIndex, setActiveItemIndex] = (0, _react.useState)(-1); // Initializes the indices for focusable items
16
+
17
+ (0, _react.useEffect)(() => {
18
+ if (menuRef) {
19
+ const menuItems = Array.from(menuRef.current.children);
20
+ const itemsIndices = (0, _helpers.getFocusableItemsIndices)(menuItems);
21
+ setFocusableItemsIndices(itemsIndices);
22
+ }
23
+ }, [children]); // Focus the active menu child
24
+
25
+ (0, _react.useEffect)(() => {
26
+ if (menuRef) {
27
+ if (focusableItemsIndices !== null && focusableItemsIndices !== void 0 && focusableItemsIndices.length && activeItemIndex > -1) {
28
+ const currentIndex = focusableItemsIndices[activeItemIndex];
29
+ menuRef.current.children[currentIndex].focus();
30
+ }
31
+ }
32
+ }, [activeItemIndex, focusableItemsIndices]); // Navigate through focusable children using arrow keys
33
+ // Trigger actionable items
34
+
35
+ const handleKeyDown = (0, _react.useCallback)(event => {
36
+ const totalFocusablePositions = focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices.length;
37
+
38
+ if (totalFocusablePositions) {
39
+ const lastIndex = totalFocusablePositions - 1;
40
+
41
+ switch (event.key) {
42
+ case 'ArrowUp':
43
+ event.preventDefault();
44
+ setActiveItemIndex(activeItemIndex > 0 ? activeItemIndex - 1 : lastIndex);
45
+ break;
46
+
47
+ case 'ArrowDown':
48
+ event.preventDefault();
49
+ setActiveItemIndex(activeItemIndex >= lastIndex ? 0 : activeItemIndex + 1);
50
+ break;
51
+
52
+ case 'Enter':
53
+ case ' ':
54
+ event.preventDefault();
55
+
56
+ if (event.target.nodeName === 'LI') {
57
+ event.target.children[0].click();
58
+ }
59
+
60
+ break;
61
+
62
+ default:
63
+ break;
64
+ }
65
+ }
66
+ }, [activeItemIndex, focusableItemsIndices]); // Event listeners for menu focus and key handling
67
+
68
+ (0, _react.useEffect)(() => {
69
+ if (!menuRef) {
70
+ return;
71
+ }
72
+
73
+ const menu = menuRef.current; // Focus the first menu item when the menu receives focus
74
+
75
+ const handleFocus = event => {
76
+ if (event.target === menuRef.current) {
77
+ const firstItemIndex = focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices[0];
78
+ firstItemIndex && menuRef.current.children[firstItemIndex].focus();
79
+ setActiveItemIndex(0);
80
+ }
81
+ };
82
+
83
+ menu.addEventListener('focus', handleFocus);
84
+ menu.addEventListener('keydown', handleKeyDown);
85
+ return () => {
86
+ menu.removeEventListener('focus', handleFocus);
87
+ menu.removeEventListener('keydown', handleKeyDown);
88
+ };
89
+ }, [activeItemIndex, focusableItemsIndices, handleKeyDown]);
90
+ return {
91
+ menuRef,
92
+ focusedIndex: focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices[activeItemIndex]
93
+ };
94
+ };
95
+
96
+ exports.useMenuNavigation = useMenuNavigation;
@@ -25,6 +25,7 @@ const MenuDivider = _ref => {
25
25
  } = _ref;
26
26
  return /*#__PURE__*/_react.default.createElement("li", {
27
27
  "data-test": dataTest,
28
+ role: "separator",
28
29
  className: _style.default.dynamic([["591815244", [_uiConstants.colors.white]]]) + " " + (className || "")
29
30
  }, /*#__PURE__*/_react.default.createElement(_divider.Divider, {
30
31
  dense: dense
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ var _enzyme = require("enzyme");
4
+
5
+ var _react = _interopRequireDefault(require("react"));
6
+
7
+ var _menuItem = require("../menu-item.js");
8
+
9
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
+
11
+ describe('Menu Component', () => {
12
+ it('Default menu item has role', () => {
13
+ const menuItemDataTest = 'data-test-menu-item';
14
+ const wrapper = (0, _enzyme.mount)( /*#__PURE__*/_react.default.createElement(_menuItem.MenuItem, {
15
+ dataTest: menuItemDataTest,
16
+ label: "Menu item"
17
+ }));
18
+ const menuItem = wrapper.find({
19
+ 'data-test': menuItemDataTest
20
+ });
21
+ expect(menuItem.childAt(0).prop('role')).toBe('menuitem');
22
+ expect(menuItem.childAt(0).prop('aria-disabled')).toBe(undefined);
23
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Menu item');
24
+ });
25
+ it('Disabled menu item has aria-disabled attribute', () => {
26
+ const menuItemDataTest = 'data-test-menu-item';
27
+ const wrapper = (0, _enzyme.mount)( /*#__PURE__*/_react.default.createElement(_menuItem.MenuItem, {
28
+ dataTest: menuItemDataTest,
29
+ label: "Disabled menu item",
30
+ disabled: true
31
+ }));
32
+ const menuItem = wrapper.find({
33
+ 'data-test': menuItemDataTest
34
+ });
35
+ expect(menuItem.childAt(0).prop('role')).toBe('menuitem');
36
+ expect(menuItem.childAt(0).prop('aria-disabled')).toBe(true);
37
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Disabled menu item');
38
+ });
39
+ it('Toggle-able menu item has menuitemcheckbox role', () => {
40
+ const menuItemDataTest = 'data-test-menu-item';
41
+ const wrapper = (0, _enzyme.mount)( /*#__PURE__*/_react.default.createElement(_menuItem.MenuItem, {
42
+ dataTest: menuItemDataTest,
43
+ label: "Toggle-able menu item",
44
+ checkbox: true,
45
+ checked: false
46
+ }));
47
+ const menuItem = wrapper.find({
48
+ 'data-test': menuItemDataTest
49
+ });
50
+ expect(menuItem.childAt(0).prop('role')).not.toBe('menuitem');
51
+ expect(menuItem.childAt(0).prop('role')).toBe('menuitemcheckbox');
52
+ expect(menuItem.childAt(0).prop('aria-checked')).toBe(false);
53
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Toggle-able menu item');
54
+ });
55
+ it('Submenu has relevant aria attributes', () => {
56
+ const showSubMenu = false;
57
+ const wrapper = (0, _enzyme.mount)( /*#__PURE__*/_react.default.createElement(_menuItem.MenuItem, {
58
+ showSubMenu: showSubMenu,
59
+ toggleSubMenu: () => jest.fn(),
60
+ label: "Parent of submenus"
61
+ }, /*#__PURE__*/_react.default.createElement(_menuItem.MenuItem, {
62
+ label: "Test submenu child"
63
+ })));
64
+ const menuItem = wrapper.find({
65
+ role: 'menuitem'
66
+ });
67
+ expect(menuItem.prop('aria-haspopup')).toBe('menu');
68
+ expect(menuItem.prop('aria-expanded')).toBe(false);
69
+ expect(menuItem.prop('aria-label')).toBe('Parent of submenus');
70
+ expect(wrapper.find({
71
+ label: 'Test submenu child'
72
+ }).exists()).toBe(false);
73
+ });
74
+ });
@@ -70,17 +70,22 @@ const MenuItem = _ref2 => {
70
70
  label,
71
71
  showSubMenu,
72
72
  toggleSubMenu,
73
- suffix
73
+ suffix,
74
+ checkbox,
75
+ checked,
76
+ tabIndex
74
77
  } = _ref2;
75
78
  const menuItemRef = (0, _react.useRef)();
76
79
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("li", {
77
80
  ref: menuItemRef,
78
81
  "data-test": dataTest,
82
+ role: "presentation",
83
+ tabIndex: tabIndex,
79
84
  className: "jsx-".concat(_menuItemStyles.default.__hash) + " " + ((0, _classnames.default)(className, {
80
85
  destructive,
81
86
  disabled,
82
87
  dense,
83
- active: active || showSubMenu,
88
+ active: active || showSubMenu || tabIndex === 0,
84
89
  'with-chevron': children || chevron
85
90
  }) || "")
86
91
  }, /*#__PURE__*/_react.default.createElement("a", {
@@ -92,6 +97,12 @@ const MenuItem = _ref2 => {
92
97
  isLink: !!href,
93
98
  value
94
99
  }) : undefined,
100
+ role: checkbox ? 'menuitemcheckbox' : 'menuitem',
101
+ "aria-checked": checkbox ? checked : null,
102
+ "aria-disabled": disabled,
103
+ "aria-haspopup": children && 'menu',
104
+ "aria-expanded": showSubMenu,
105
+ "aria-label": label,
95
106
  className: "jsx-".concat(_menuItemStyles.default.__hash)
96
107
  }, icon && /*#__PURE__*/_react.default.createElement("span", {
97
108
  className: "jsx-".concat(_menuItemStyles.default.__hash) + " " + "icon"
@@ -117,6 +128,8 @@ MenuItem.defaultProps = {
117
128
  };
118
129
  MenuItem.propTypes = {
119
130
  active: _propTypes.default.bool,
131
+ checkbox: _propTypes.default.bool,
132
+ checked: _propTypes.default.bool,
120
133
  chevron: _propTypes.default.bool,
121
134
 
122
135
  /**
@@ -144,6 +157,7 @@ MenuItem.propTypes = {
144
157
 
145
158
  /** A supporting element shown at the end of the menu item */
146
159
  suffix: _propTypes.default.node,
160
+ tabIndex: _propTypes.default.number,
147
161
 
148
162
  /** For using menu item as a link */
149
163
  target: _propTypes.default.string,
@@ -168,7 +168,9 @@ const ToggleMenuItem = args => {
168
168
  return /*#__PURE__*/_react.default.createElement(_index.Menu, null, /*#__PURE__*/_react.default.createElement(_menuItem.MenuItem, _extends({}, args, {
169
169
  onClick: toggleOn,
170
170
  icon: icon,
171
- label: "A toggle menu item"
171
+ label: "A toggle menu item",
172
+ checkbox: true,
173
+ checked: on
172
174
  })));
173
175
  };
174
176
 
@@ -0,0 +1,196 @@
1
+ import { render } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { mount } from 'enzyme';
4
+ import React from 'react';
5
+ import { MenuDivider } from '../../menu-divider/menu-divider.js';
6
+ import { MenuItem } from '../../menu-item/menu-item.js';
7
+ import { MenuSectionHeader } from '../../menu-section-header/menu-section-header.js';
8
+ import { Menu } from '../menu.js';
9
+ describe('Menu Component', () => {
10
+ const menuDataTest = 'data-test-menu';
11
+ const menuItemDataTest = 'data-test-menu-item';
12
+ const dividerDataTest = 'data-test-menu-divider';
13
+ it('Menu and menu items have aria attributes', () => {
14
+ const wrapper = mount( /*#__PURE__*/React.createElement(Menu, {
15
+ dataTest: menuDataTest,
16
+ dense: false
17
+ }, /*#__PURE__*/React.createElement(MenuSectionHeader, {
18
+ label: "Header"
19
+ }), /*#__PURE__*/React.createElement(MenuDivider, {
20
+ dataTest: dividerDataTest
21
+ }), /*#__PURE__*/React.createElement(MenuItem, {
22
+ dataTest: menuItemDataTest,
23
+ label: "Menu item"
24
+ })));
25
+ const menuElement = wrapper.find({
26
+ 'data-test': menuDataTest
27
+ });
28
+ const menuItem = wrapper.find({
29
+ 'data-test': menuItemDataTest
30
+ });
31
+ const menuDivider = wrapper.find({
32
+ 'data-test': dividerDataTest
33
+ });
34
+ expect(menuElement.prop('role')).toBe('menu');
35
+ expect(menuItem.childAt(0).props().role).toBe('menuitem');
36
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Menu item');
37
+ expect(menuDivider.prop('role')).toBe('separator');
38
+ });
39
+ it('Empty menu has role menu', () => {
40
+ const menuDataTest = 'data-test-menu';
41
+ const wrapper = mount( /*#__PURE__*/React.createElement(Menu, {
42
+ dataTest: menuDataTest,
43
+ dense: false
44
+ }));
45
+ const menuElement = wrapper.find({
46
+ 'data-test': menuDataTest
47
+ });
48
+ expect(menuElement.prop('role')).toBe('menu');
49
+ });
50
+ it('can handle focus of first focusable element when tabbed to', () => {
51
+ const {
52
+ getByRole,
53
+ getByText
54
+ } = render( /*#__PURE__*/React.createElement(Menu, {
55
+ dataTest: menuDataTest,
56
+ dense: false
57
+ }, /*#__PURE__*/React.createElement(MenuSectionHeader, {
58
+ label: "Header"
59
+ }), /*#__PURE__*/React.createElement(MenuDivider, {
60
+ dataTest: dividerDataTest
61
+ }), /*#__PURE__*/React.createElement(MenuItem, {
62
+ dataTest: menuItemDataTest,
63
+ label: "Menu item 1"
64
+ }), /*#__PURE__*/React.createElement(MenuItem, {
65
+ dataTest: menuItemDataTest,
66
+ label: "Menu item 2"
67
+ })));
68
+ const menu = getByRole('menu');
69
+ const divider = getByRole('separator');
70
+ const header = getByText(/Header/i);
71
+ const menuItem1 = getByText(/Menu item 1/i);
72
+ const menuItem2 = getByText(/Menu item 2/i);
73
+ expect(menu).not.toHaveFocus();
74
+ userEvent.tab(); // check if LI parent node has focus or not
75
+ // headers and dividers do not receive focus
76
+
77
+ expect(header.parentNode.parentNode).not.toHaveFocus();
78
+ expect(divider.parentNode.parentNode).not.toHaveFocus();
79
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus();
80
+ expect(menuItem1.parentNode.parentNode).toHaveFocus();
81
+ });
82
+ it('can handle arrow down key navigation', async () => {
83
+ const {
84
+ getByText
85
+ } = render( /*#__PURE__*/React.createElement(Menu, {
86
+ dataTest: menuDataTest,
87
+ dense: false
88
+ }, /*#__PURE__*/React.createElement(MenuSectionHeader, {
89
+ label: "Header"
90
+ }), /*#__PURE__*/React.createElement(MenuDivider, {
91
+ dataTest: dividerDataTest
92
+ }), /*#__PURE__*/React.createElement(MenuItem, {
93
+ dataTest: menuItemDataTest,
94
+ label: "Menu item 1"
95
+ }), /*#__PURE__*/React.createElement(MenuItem, {
96
+ dataTest: menuItemDataTest,
97
+ label: "Menu item 2"
98
+ })));
99
+ const menuItem1 = getByText(/Menu item 1/i);
100
+ const menuItem2 = getByText(/Menu item 2/i);
101
+ userEvent.tab();
102
+ expect(menuItem1.parentNode.parentNode).toHaveFocus(); // simulate arrowDown press
103
+
104
+ userEvent.keyboard('{ArrowDown}');
105
+ expect(menuItem1.parentNode.parentNode).not.toHaveFocus();
106
+ expect(menuItem2.parentNode.parentNode).toHaveFocus();
107
+ userEvent.keyboard('{ArrowDown}');
108
+ expect(menuItem1.parentNode.parentNode).toHaveFocus();
109
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus();
110
+ });
111
+ it('can handle arrow up key navigation', async () => {
112
+ const {
113
+ getByText
114
+ } = render( /*#__PURE__*/React.createElement(Menu, {
115
+ dataTest: menuDataTest,
116
+ dense: false
117
+ }, /*#__PURE__*/React.createElement(MenuSectionHeader, {
118
+ label: "Header"
119
+ }), /*#__PURE__*/React.createElement(MenuItem, {
120
+ dataTest: menuItemDataTest,
121
+ label: "Menu item 1"
122
+ }), /*#__PURE__*/React.createElement(MenuDivider, {
123
+ dataTest: dividerDataTest
124
+ }), /*#__PURE__*/React.createElement(MenuItem, {
125
+ dataTest: menuItemDataTest,
126
+ label: "Menu item 2"
127
+ })));
128
+ const menuItem1 = getByText(/Menu item 1/i);
129
+ const menuItem2 = getByText(/Menu item 2/i);
130
+ userEvent.tab();
131
+ expect(menuItem1.parentNode.parentNode).toHaveFocus(); // simulate arrowUp press
132
+
133
+ userEvent.keyboard('{ArrowUp}');
134
+ expect(menuItem1.parentNode.parentNode).not.toHaveFocus();
135
+ expect(menuItem2.parentNode.parentNode).toHaveFocus();
136
+ userEvent.keyboard('{ArrowUp}');
137
+ expect(menuItem1.parentNode.parentNode).toHaveFocus();
138
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus();
139
+ });
140
+ it('can handle space and enter key presses for clickable menu items', async () => {
141
+ const onClick = jest.fn();
142
+ const {
143
+ getByText
144
+ } = render( /*#__PURE__*/React.createElement(Menu, {
145
+ dataTest: menuDataTest,
146
+ dense: false
147
+ }, /*#__PURE__*/React.createElement(MenuItem, {
148
+ onClick: onClick,
149
+ value: "myValue",
150
+ label: "Click menu item"
151
+ })));
152
+ const clickableItem = getByText(/Click menu item/i);
153
+ userEvent.tab();
154
+ expect(clickableItem.parentNode.parentNode).toHaveFocus();
155
+ userEvent.keyboard('[Space]');
156
+ expect(onClick).toHaveBeenCalledTimes(1);
157
+ userEvent.keyboard('{Enter}');
158
+ expect(onClick).toHaveBeenCalledTimes(2);
159
+ });
160
+ it('can handle non MenuItem components', () => {
161
+ const onClick = jest.fn();
162
+ const {
163
+ getByText
164
+ } = render( /*#__PURE__*/React.createElement(Menu, {
165
+ dataTest: menuDataTest,
166
+ dense: false
167
+ }, /*#__PURE__*/React.createElement("span", {
168
+ role: "menuitem",
169
+ onClick: onClick
170
+ }, "Span 1"), /*#__PURE__*/React.createElement("li", {
171
+ tabIndex: -1
172
+ }, /*#__PURE__*/React.createElement("a", {
173
+ href: "#",
174
+ role: "menuitem"
175
+ }, "Link 2")), /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement("span", {
176
+ onClick: onClick
177
+ }, "Span 2"))));
178
+ const nonListMenuItem = getByText(/span 1/i);
179
+ const listMenuItem = getByText(/link 2/i);
180
+ const plainListItem = getByText(/span 2/i); // all children must be list items
181
+
182
+ expect(nonListMenuItem.parentElement.nodeName).toBe('LI');
183
+ userEvent.tab();
184
+ expect(nonListMenuItem.parentElement).toHaveFocus();
185
+ expect(nonListMenuItem.parentElement.tabIndex).toBe(0);
186
+ expect(onClick).toHaveBeenCalledTimes(0);
187
+ userEvent.keyboard('[Space]');
188
+ expect(onClick).toHaveBeenCalledTimes(1);
189
+ userEvent.keyboard('{ArrowDown}');
190
+ expect(listMenuItem.parentElement).toHaveFocus();
191
+ userEvent.keyboard('{ArrowDown}');
192
+ expect(nonListMenuItem.parentElement).toHaveFocus(); // non menu items do not receive focus
193
+
194
+ expect(plainListItem.parentElement).not.toHaveFocus();
195
+ });
196
+ });
@@ -0,0 +1,27 @@
1
+ const isMenuItem = role => {
2
+ return ['menuitem', 'menuitemcheckbox', 'menuitemradio'].includes(role);
3
+ };
4
+
5
+ const isValidMenuItemNode = node => {
6
+ if (node.nodeName === 'LI' && node.firstElementChild) {
7
+ return isValidMenuItemNode(node.firstElementChild);
8
+ }
9
+
10
+ const role = node.getAttribute('role');
11
+ return role && isMenuItem(role);
12
+ };
13
+
14
+ export const getFocusableItemsIndices = elements => {
15
+ const focusableIndices = [];
16
+ elements.forEach((node, index) => {
17
+ if (isValidMenuItemNode(node)) {
18
+ focusableIndices.push(index);
19
+ }
20
+ });
21
+ return focusableIndices;
22
+ };
23
+ export const hasMenuItemRole = component => {
24
+ var _component$props;
25
+
26
+ return isMenuItem(component === null || component === void 0 ? void 0 : (_component$props = component.props) === null || _component$props === void 0 ? void 0 : _component$props['role']);
27
+ };
@@ -1,6 +1,8 @@
1
1
  import _JSXStyle from "styled-jsx/style";
2
2
  import PropTypes from 'prop-types';
3
3
  import React, { Children, cloneElement, isValidElement } from 'react';
4
+ import { hasMenuItemRole } from './helpers.js';
5
+ import { useMenuNavigation } from './use-menu.js';
4
6
 
5
7
  const Menu = _ref => {
6
8
  let {
@@ -9,15 +11,53 @@ const Menu = _ref => {
9
11
  dataTest,
10
12
  dense
11
13
  } = _ref;
14
+ const {
15
+ menuRef,
16
+ focusedIndex
17
+ } = useMenuNavigation(children);
18
+ const childrenToRender = Children.map(children, (child, index) => {
19
+ if (! /*#__PURE__*/isValidElement(child)) {
20
+ return child;
21
+ }
22
+
23
+ const tabIndex = index === focusedIndex ? 0 : -1;
24
+ const childProps = { ...child.props
25
+ };
26
+
27
+ if (typeof child.type === 'string') {
28
+ // remove non-native props from native HTML elements
29
+ delete childProps.hideDivider;
30
+ delete childProps.dense;
31
+ delete childProps.active; // all ul children must be li elements
32
+ // add tabindex for focus to those elements that are/contain a menuitem
33
+
34
+ if (child.type === 'li') {
35
+ return hasMenuItemRole(child.props.children[0]) ? /*#__PURE__*/cloneElement(child, { ...childProps,
36
+ tabIndex
37
+ }) : /*#__PURE__*/cloneElement(child, childProps);
38
+ } else {
39
+ return /*#__PURE__*/React.createElement("li", {
40
+ tabIndex: hasMenuItemRole(child) ? tabIndex : null
41
+ }, /*#__PURE__*/cloneElement(child, childProps));
42
+ }
43
+ } else {
44
+ // assign non-native props to custom elements
45
+ childProps.dense = typeof child.props.dense === 'boolean' ? child.props.dense : dense;
46
+ childProps.hideDivider = typeof child.props.hideDivider !== 'boolean' && index === 0 ? true : child.props.hideDivider;
47
+ return /*#__PURE__*/cloneElement(child, { ...childProps,
48
+ tabIndex
49
+ });
50
+ }
51
+ });
12
52
  return /*#__PURE__*/React.createElement("ul", {
13
53
  "data-test": dataTest,
14
- className: "jsx-3549878755" + " " + (className || "")
15
- }, Children.map(children, (child, index) => /*#__PURE__*/isValidElement(child) ? /*#__PURE__*/cloneElement(child, {
16
- dense: typeof child.props.dense === 'boolean' ? child.props.dense : dense,
17
- hideDivider: typeof child.props.hideDivider !== 'boolean' && index === 0 ? true : child.props.hideDivider
18
- }) : child), /*#__PURE__*/React.createElement(_JSXStyle, {
19
- id: "3549878755"
20
- }, ["ul.jsx-3549878755{display:block;position:relative;width:100%;margin:0;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}"]));
54
+ role: "menu",
55
+ ref: menuRef,
56
+ tabIndex: 0,
57
+ className: "jsx-1636612837" + " " + (className || "")
58
+ }, childrenToRender, /*#__PURE__*/React.createElement(_JSXStyle, {
59
+ id: "1636612837"
60
+ }, ["ul.jsx-1636612837{display:block;position:relative;width:100%;margin:0;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}"]));
21
61
  };
22
62
 
23
63
  Menu.defaultProps = {
@@ -0,0 +1,85 @@
1
+ import { useRef, useState, useEffect, useCallback } from 'react';
2
+ import { getFocusableItemsIndices } from './helpers.js';
3
+ export const useMenuNavigation = children => {
4
+ const menuRef = useRef(null);
5
+ const [focusableItemsIndices, setFocusableItemsIndices] = useState(null);
6
+ const [activeItemIndex, setActiveItemIndex] = useState(-1); // Initializes the indices for focusable items
7
+
8
+ useEffect(() => {
9
+ if (menuRef) {
10
+ const menuItems = Array.from(menuRef.current.children);
11
+ const itemsIndices = getFocusableItemsIndices(menuItems);
12
+ setFocusableItemsIndices(itemsIndices);
13
+ }
14
+ }, [children]); // Focus the active menu child
15
+
16
+ useEffect(() => {
17
+ if (menuRef) {
18
+ if (focusableItemsIndices !== null && focusableItemsIndices !== void 0 && focusableItemsIndices.length && activeItemIndex > -1) {
19
+ const currentIndex = focusableItemsIndices[activeItemIndex];
20
+ menuRef.current.children[currentIndex].focus();
21
+ }
22
+ }
23
+ }, [activeItemIndex, focusableItemsIndices]); // Navigate through focusable children using arrow keys
24
+ // Trigger actionable items
25
+
26
+ const handleKeyDown = useCallback(event => {
27
+ const totalFocusablePositions = focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices.length;
28
+
29
+ if (totalFocusablePositions) {
30
+ const lastIndex = totalFocusablePositions - 1;
31
+
32
+ switch (event.key) {
33
+ case 'ArrowUp':
34
+ event.preventDefault();
35
+ setActiveItemIndex(activeItemIndex > 0 ? activeItemIndex - 1 : lastIndex);
36
+ break;
37
+
38
+ case 'ArrowDown':
39
+ event.preventDefault();
40
+ setActiveItemIndex(activeItemIndex >= lastIndex ? 0 : activeItemIndex + 1);
41
+ break;
42
+
43
+ case 'Enter':
44
+ case ' ':
45
+ event.preventDefault();
46
+
47
+ if (event.target.nodeName === 'LI') {
48
+ event.target.children[0].click();
49
+ }
50
+
51
+ break;
52
+
53
+ default:
54
+ break;
55
+ }
56
+ }
57
+ }, [activeItemIndex, focusableItemsIndices]); // Event listeners for menu focus and key handling
58
+
59
+ useEffect(() => {
60
+ if (!menuRef) {
61
+ return;
62
+ }
63
+
64
+ const menu = menuRef.current; // Focus the first menu item when the menu receives focus
65
+
66
+ const handleFocus = event => {
67
+ if (event.target === menuRef.current) {
68
+ const firstItemIndex = focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices[0];
69
+ firstItemIndex && menuRef.current.children[firstItemIndex].focus();
70
+ setActiveItemIndex(0);
71
+ }
72
+ };
73
+
74
+ menu.addEventListener('focus', handleFocus);
75
+ menu.addEventListener('keydown', handleKeyDown);
76
+ return () => {
77
+ menu.removeEventListener('focus', handleFocus);
78
+ menu.removeEventListener('keydown', handleKeyDown);
79
+ };
80
+ }, [activeItemIndex, focusableItemsIndices, handleKeyDown]);
81
+ return {
82
+ menuRef,
83
+ focusedIndex: focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices[activeItemIndex]
84
+ };
85
+ };
@@ -12,6 +12,7 @@ const MenuDivider = _ref => {
12
12
  } = _ref;
13
13
  return /*#__PURE__*/React.createElement("li", {
14
14
  "data-test": dataTest,
15
+ role: "separator",
15
16
  className: _JSXStyle.dynamic([["591815244", [colors.white]]]) + " " + (className || "")
16
17
  }, /*#__PURE__*/React.createElement(Divider, {
17
18
  dense: dense
@@ -0,0 +1,67 @@
1
+ import { mount } from 'enzyme';
2
+ import React from 'react';
3
+ import { MenuItem } from '../menu-item.js';
4
+ describe('Menu Component', () => {
5
+ it('Default menu item has role', () => {
6
+ const menuItemDataTest = 'data-test-menu-item';
7
+ const wrapper = mount( /*#__PURE__*/React.createElement(MenuItem, {
8
+ dataTest: menuItemDataTest,
9
+ label: "Menu item"
10
+ }));
11
+ const menuItem = wrapper.find({
12
+ 'data-test': menuItemDataTest
13
+ });
14
+ expect(menuItem.childAt(0).prop('role')).toBe('menuitem');
15
+ expect(menuItem.childAt(0).prop('aria-disabled')).toBe(undefined);
16
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Menu item');
17
+ });
18
+ it('Disabled menu item has aria-disabled attribute', () => {
19
+ const menuItemDataTest = 'data-test-menu-item';
20
+ const wrapper = mount( /*#__PURE__*/React.createElement(MenuItem, {
21
+ dataTest: menuItemDataTest,
22
+ label: "Disabled menu item",
23
+ disabled: true
24
+ }));
25
+ const menuItem = wrapper.find({
26
+ 'data-test': menuItemDataTest
27
+ });
28
+ expect(menuItem.childAt(0).prop('role')).toBe('menuitem');
29
+ expect(menuItem.childAt(0).prop('aria-disabled')).toBe(true);
30
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Disabled menu item');
31
+ });
32
+ it('Toggle-able menu item has menuitemcheckbox role', () => {
33
+ const menuItemDataTest = 'data-test-menu-item';
34
+ const wrapper = mount( /*#__PURE__*/React.createElement(MenuItem, {
35
+ dataTest: menuItemDataTest,
36
+ label: "Toggle-able menu item",
37
+ checkbox: true,
38
+ checked: false
39
+ }));
40
+ const menuItem = wrapper.find({
41
+ 'data-test': menuItemDataTest
42
+ });
43
+ expect(menuItem.childAt(0).prop('role')).not.toBe('menuitem');
44
+ expect(menuItem.childAt(0).prop('role')).toBe('menuitemcheckbox');
45
+ expect(menuItem.childAt(0).prop('aria-checked')).toBe(false);
46
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Toggle-able menu item');
47
+ });
48
+ it('Submenu has relevant aria attributes', () => {
49
+ const showSubMenu = false;
50
+ const wrapper = mount( /*#__PURE__*/React.createElement(MenuItem, {
51
+ showSubMenu: showSubMenu,
52
+ toggleSubMenu: () => jest.fn(),
53
+ label: "Parent of submenus"
54
+ }, /*#__PURE__*/React.createElement(MenuItem, {
55
+ label: "Test submenu child"
56
+ })));
57
+ const menuItem = wrapper.find({
58
+ role: 'menuitem'
59
+ });
60
+ expect(menuItem.prop('aria-haspopup')).toBe('menu');
61
+ expect(menuItem.prop('aria-expanded')).toBe(false);
62
+ expect(menuItem.prop('aria-label')).toBe('Parent of submenus');
63
+ expect(wrapper.find({
64
+ label: 'Test submenu child'
65
+ }).exists()).toBe(false);
66
+ });
67
+ });
@@ -49,17 +49,22 @@ const MenuItem = _ref2 => {
49
49
  label,
50
50
  showSubMenu,
51
51
  toggleSubMenu,
52
- suffix
52
+ suffix,
53
+ checkbox,
54
+ checked,
55
+ tabIndex
53
56
  } = _ref2;
54
57
  const menuItemRef = useRef();
55
58
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("li", {
56
59
  ref: menuItemRef,
57
60
  "data-test": dataTest,
61
+ role: "presentation",
62
+ tabIndex: tabIndex,
58
63
  className: "jsx-".concat(styles.__hash) + " " + (cx(className, {
59
64
  destructive,
60
65
  disabled,
61
66
  dense,
62
- active: active || showSubMenu,
67
+ active: active || showSubMenu || tabIndex === 0,
63
68
  'with-chevron': children || chevron
64
69
  }) || "")
65
70
  }, /*#__PURE__*/React.createElement("a", {
@@ -71,6 +76,12 @@ const MenuItem = _ref2 => {
71
76
  isLink: !!href,
72
77
  value
73
78
  }) : undefined,
79
+ role: checkbox ? 'menuitemcheckbox' : 'menuitem',
80
+ "aria-checked": checkbox ? checked : null,
81
+ "aria-disabled": disabled,
82
+ "aria-haspopup": children && 'menu',
83
+ "aria-expanded": showSubMenu,
84
+ "aria-label": label,
74
85
  className: "jsx-".concat(styles.__hash)
75
86
  }, icon && /*#__PURE__*/React.createElement("span", {
76
87
  className: "jsx-".concat(styles.__hash) + " " + "icon"
@@ -95,6 +106,8 @@ MenuItem.defaultProps = {
95
106
  };
96
107
  MenuItem.propTypes = {
97
108
  active: PropTypes.bool,
109
+ checkbox: PropTypes.bool,
110
+ checked: PropTypes.bool,
98
111
  chevron: PropTypes.bool,
99
112
 
100
113
  /**
@@ -122,6 +135,7 @@ MenuItem.propTypes = {
122
135
 
123
136
  /** A supporting element shown at the end of the menu item */
124
137
  suffix: PropTypes.node,
138
+ tabIndex: PropTypes.number,
125
139
 
126
140
  /** For using menu item as a link */
127
141
  target: PropTypes.string,
@@ -134,7 +134,9 @@ export const ToggleMenuItem = args => {
134
134
  return /*#__PURE__*/React.createElement(Menu, null, /*#__PURE__*/React.createElement(MenuItem, _extends({}, args, {
135
135
  onClick: toggleOn,
136
136
  icon: icon,
137
- label: "A toggle menu item"
137
+ label: "A toggle menu item",
138
+ checkbox: true,
139
+ checked: on
138
140
  })));
139
141
  };
140
142
  ToggleMenuItem.parameters = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2-ui/menu",
3
- "version": "9.9.0-alpha.1",
3
+ "version": "9.9.1",
4
4
  "description": "UI Menu",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,13 +33,13 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@dhis2/prop-types": "^3.1.2",
36
- "@dhis2-ui/card": "9.9.0-alpha.1",
37
- "@dhis2-ui/divider": "9.9.0-alpha.1",
38
- "@dhis2-ui/layer": "9.9.0-alpha.1",
39
- "@dhis2-ui/popper": "9.9.0-alpha.1",
40
- "@dhis2-ui/portal": "9.9.0-alpha.1",
41
- "@dhis2/ui-constants": "9.9.0-alpha.1",
42
- "@dhis2/ui-icons": "9.9.0-alpha.1",
36
+ "@dhis2-ui/card": "9.9.1",
37
+ "@dhis2-ui/divider": "9.9.1",
38
+ "@dhis2-ui/layer": "9.9.1",
39
+ "@dhis2-ui/popper": "9.9.1",
40
+ "@dhis2-ui/portal": "9.9.1",
41
+ "@dhis2/ui-constants": "9.9.1",
42
+ "@dhis2/ui-icons": "9.9.1",
43
43
  "classnames": "^2.3.1",
44
44
  "prop-types": "^15.7.2"
45
45
  },
package/types/index.d.ts CHANGED
@@ -42,6 +42,14 @@ export const MenuDivider: React.FC<MenuDividerProps>
42
42
 
43
43
  export interface MenuItemProps {
44
44
  active?: boolean
45
+ /**
46
+ * Specifies if menu item is a checkbox
47
+ */
48
+ checkbox?: boolean
49
+ /**
50
+ * checkbox state for toggleable menu items
51
+ */
52
+ checked?: boolean
45
53
  chevron?: boolean
46
54
  /**
47
55
  * Nested menu items can become submenus.
@@ -69,6 +77,11 @@ export interface MenuItemProps {
69
77
  * When true, nested menu items are shown in a Popper
70
78
  */
71
79
  showSubMenu?: boolean
80
+ /**
81
+ * A supporting element shown at the end of the menu item
82
+ */
83
+ suffix: React.ReactNode
84
+ tabIndex?: number
72
85
  /**
73
86
  * For using menu item as a link
74
87
  */