@dhis2-ui/menu 9.11.0 → 9.11.1-beta.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.
- package/build/cjs/flyout-menu/__tests__/flyout-menu.test.js +1 -15
- package/build/cjs/flyout-menu/features/accepts_children/index.js +0 -1
- package/build/cjs/flyout-menu/features/position/index.js +4 -4
- package/build/cjs/flyout-menu/features/toggles_submenus/index.js +0 -1
- package/build/cjs/flyout-menu/{flyout-menu.stories.e2e.js → flyout-menu.e2e.stories.js} +2 -20
- package/build/cjs/flyout-menu/flyout-menu.js +5 -23
- package/build/cjs/flyout-menu/{flyout-menu.stories.js → flyout-menu.prod.stories.js} +25 -37
- package/build/cjs/flyout-menu/index.js +0 -1
- package/build/cjs/index.js +0 -5
- package/build/cjs/menu/__tests__/menu.test.js +11 -50
- package/build/cjs/menu/features/accepts_children/index.js +0 -1
- package/build/cjs/menu/helpers.js +2 -10
- package/build/cjs/menu/index.js +0 -1
- package/build/cjs/menu/menu.e2e.stories.js +14 -0
- package/build/cjs/menu/menu.js +12 -20
- package/build/cjs/menu/{menu.stories.js → menu.prod.stories.js} +18 -17
- package/build/cjs/menu/use-menu.js +12 -20
- package/build/cjs/menu-divider/index.js +0 -1
- package/build/cjs/menu-divider/menu-divider.js +3 -11
- package/build/cjs/menu-divider/{menu-divider.stories.js → menu-divider.prod.stories.js} +11 -15
- package/build/cjs/menu-item/__tests__/menu-item.test.js +1 -5
- package/build/cjs/menu-item/features/accepts_href/index.js +1 -2
- package/build/cjs/menu-item/features/accepts_icon/index.js +0 -1
- package/build/cjs/menu-item/features/accepts_label/index.js +1 -2
- package/build/cjs/menu-item/features/accepts_suffix/index.js +0 -1
- package/build/cjs/menu-item/features/accepts_target/index.js +1 -2
- package/build/cjs/menu-item/features/is_clickable/index.js +0 -1
- package/build/cjs/menu-item/index.js +0 -1
- package/build/cjs/menu-item/{menu-item.stories.e2e.js → menu-item.e2e.stories.js} +2 -20
- package/build/cjs/menu-item/menu-item.js +11 -46
- package/build/cjs/menu-item/{menu-item.stories.js → menu-item.prod.stories.js} +22 -46
- package/build/cjs/menu-item/menu-item.styles.js +2 -5
- package/build/cjs/menu-section-header/features/accepts_label/index.js +1 -2
- package/build/cjs/menu-section-header/index.js +0 -1
- package/build/cjs/menu-section-header/{menu-section-header.stories.e2e.js → menu-section-header.e2e.stories.js} +2 -10
- package/build/cjs/menu-section-header/menu-section-header.js +3 -12
- package/build/cjs/menu-section-header/{menu-section-header.stories.js → menu-section-header.prod.stories.js} +11 -19
- package/build/es/flyout-menu/features/position/index.js +4 -3
- package/build/es/flyout-menu/{flyout-menu.stories.e2e.js → flyout-menu.e2e.stories.js} +0 -2
- package/build/es/flyout-menu/flyout-menu.js +2 -12
- package/build/es/flyout-menu/{flyout-menu.stories.js → flyout-menu.prod.stories.js} +25 -12
- package/build/es/menu/__tests__/menu.test.js +10 -8
- package/build/es/menu/helpers.js +2 -6
- package/build/es/menu/menu.e2e.stories.js +6 -0
- package/build/es/menu/menu.js +9 -9
- package/build/es/menu/{menu.stories.js → menu.prod.stories.js} +18 -1
- package/build/es/menu/use-menu.js +12 -16
- package/build/es/menu-divider/menu-divider.js +2 -4
- package/build/es/menu-divider/{menu-divider.stories.js → menu-divider.prod.stories.js} +7 -3
- package/build/es/menu-item/features/accepts_href/index.js +1 -1
- package/build/es/menu-item/features/accepts_label/index.js +1 -1
- package/build/es/menu-item/features/accepts_target/index.js +1 -1
- package/build/es/menu-item/menu-item.js +8 -31
- package/build/es/menu-item/{menu-item.stories.js → menu-item.prod.stories.js} +14 -9
- package/build/es/menu-item/menu-item.styles.js +1 -1
- package/build/es/menu-section-header/features/accepts_label/index.js +1 -1
- package/build/es/menu-section-header/menu-section-header.js +2 -4
- package/build/es/menu-section-header/{menu-section-header.stories.js → menu-section-header.prod.stories.js} +7 -3
- package/package.json +11 -11
- package/build/cjs/menu/menu.stories.e2e.js +0 -11
- package/build/es/menu/menu.stories.e2e.js +0 -4
- /package/build/es/menu-item/{menu-item.stories.e2e.js → menu-item.e2e.stories.js} +0 -0
- /package/build/es/menu-section-header/{menu-section-header.stories.e2e.js → menu-section-header.e2e.stories.js} +0 -0
|
@@ -2,8 +2,7 @@ import _JSXStyle from "styled-jsx/style";
|
|
|
2
2
|
import { colors, elevations, spacers } from '@dhis2/ui-constants';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import React, { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react';
|
|
5
|
-
import { Menu } from '../index.js';
|
|
6
|
-
|
|
5
|
+
import { Menu } from '../menu/index.js';
|
|
7
6
|
const FlyoutMenu = _ref => {
|
|
8
7
|
let {
|
|
9
8
|
children,
|
|
@@ -15,20 +14,16 @@ const FlyoutMenu = _ref => {
|
|
|
15
14
|
closeMenu
|
|
16
15
|
} = _ref;
|
|
17
16
|
const [openedSubMenu, setOpenedSubMenu] = useState(null);
|
|
18
|
-
|
|
19
17
|
const toggleSubMenu = index => {
|
|
20
18
|
const toggleValue = index === openedSubMenu ? null : index;
|
|
21
19
|
setOpenedSubMenu(toggleValue);
|
|
22
20
|
};
|
|
23
|
-
|
|
24
21
|
const divRef = useRef(null);
|
|
25
22
|
useEffect(() => {
|
|
26
23
|
if (!divRef.current) {
|
|
27
24
|
return;
|
|
28
25
|
}
|
|
29
|
-
|
|
30
26
|
const div = divRef.current;
|
|
31
|
-
|
|
32
27
|
const handleFocus = event => {
|
|
33
28
|
if (event.target === div) {
|
|
34
29
|
if (div !== null && div !== void 0 && div.children && div.children.length > 0) {
|
|
@@ -36,14 +31,12 @@ const FlyoutMenu = _ref => {
|
|
|
36
31
|
}
|
|
37
32
|
}
|
|
38
33
|
};
|
|
39
|
-
|
|
40
34
|
const handleKeyDown = event => {
|
|
41
35
|
if (event.key === 'Escape') {
|
|
42
36
|
event.preventDefault();
|
|
43
37
|
closeMenu && closeMenu();
|
|
44
38
|
}
|
|
45
39
|
};
|
|
46
|
-
|
|
47
40
|
div.addEventListener('focus', handleFocus);
|
|
48
41
|
div.addEventListener('keydown', handleKeyDown);
|
|
49
42
|
return () => {
|
|
@@ -64,9 +57,8 @@ const FlyoutMenu = _ref => {
|
|
|
64
57
|
}) : child)), /*#__PURE__*/React.createElement(_JSXStyle, {
|
|
65
58
|
id: "3833750986",
|
|
66
59
|
dynamic: [colors.white, colors.grey200, elevations.e300, dense ? '128' : '180', maxWidth, maxHeight, spacers.dp4]
|
|
67
|
-
}, [
|
|
60
|
+
}, [`div.__jsx-style-dynamic-selector{background:${colors.white};border:1px solid ${colors.grey200};border-radius:3px;box-shadow:${elevations.e300};display:inline-block;min-width:${dense ? '128' : '180'}px;max-width:${maxWidth};max-height:${maxHeight};padding:${spacers.dp4} 0;overflow:auto;}`]));
|
|
68
61
|
};
|
|
69
|
-
|
|
70
62
|
FlyoutMenu.defaultProps = {
|
|
71
63
|
dataTest: 'dhis2-uicore-menu',
|
|
72
64
|
maxWidth: '380px',
|
|
@@ -76,11 +68,9 @@ FlyoutMenu.propTypes = {
|
|
|
76
68
|
/** Typically, but not limited to, `MenuItem` components */
|
|
77
69
|
children: PropTypes.node,
|
|
78
70
|
className: PropTypes.string,
|
|
79
|
-
|
|
80
71
|
/** when Escape key is pressed, this function is called to close the flyout menu */
|
|
81
72
|
closeMenu: PropTypes.func,
|
|
82
73
|
dataTest: PropTypes.string,
|
|
83
|
-
|
|
84
74
|
/** Menu uses smaller dimensions */
|
|
85
75
|
dense: PropTypes.bool,
|
|
86
76
|
maxHeight: PropTypes.string,
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
function _extends() { _extends = Object.assign
|
|
2
|
-
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import { IconChevronDown16 } from '@dhis2/ui-icons';
|
|
3
3
|
import { Layer } from '@dhis2-ui/layer';
|
|
4
4
|
import { MenuDivider, MenuItem, MenuSectionHeader } from '@dhis2-ui/menu';
|
|
5
5
|
import { Popper } from '@dhis2-ui/popper';
|
|
6
|
-
import { IconChevronDown16 } from '@dhis2/ui-icons';
|
|
7
6
|
import React, { useState, useRef } from 'react';
|
|
8
7
|
import { FlyoutMenu } from './flyout-menu.js';
|
|
9
|
-
const description =
|
|
8
|
+
const description = `
|
|
9
|
+
Use menus to provide access to options and actions where space is limited and displaying all the options would be impractical. For example, providing access to a range of actions for every dashboard item displayed. Containing all those actions in menus keeps the page manageable.
|
|
10
|
+
|
|
11
|
+
The menu component is flexible in where it can be used and its contents can be flexible too. However, the most common use case is a menu containing menu items.
|
|
12
|
+
|
|
13
|
+
Make sure the menu item labels are short and easy to understand. One word is often enough to describe an action or option. Do not use sentences as labels. Some examples of good menu item labels:
|
|
14
|
+
|
|
15
|
+
- "Save"
|
|
16
|
+
- "Open as map"
|
|
17
|
+
- "Export PDF"
|
|
18
|
+
- "Duplicate"
|
|
19
|
+
|
|
20
|
+
See more about how to use menus at the [design system](https://github.com/dhis2/design-system/blob/master/molecules/menu.md).
|
|
21
|
+
|
|
22
|
+
\`\`\`js
|
|
23
|
+
import { FlyoutMenu } from 'dhis2/ui'
|
|
24
|
+
\`\`\`
|
|
25
|
+
`;
|
|
10
26
|
export default {
|
|
11
27
|
title: 'Flyout Menu',
|
|
12
28
|
component: FlyoutMenu,
|
|
@@ -60,9 +76,11 @@ export const MaxWidth = args => /*#__PURE__*/React.createElement(React.Fragment,
|
|
|
60
76
|
}), /*#__PURE__*/React.createElement(MenuItem, {
|
|
61
77
|
label: "Item 2 - with a lot of text and using a default maxWidth value of 380px"
|
|
62
78
|
})), /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement(FlyoutMenu, args, /*#__PURE__*/React.createElement(MenuItem, {
|
|
63
|
-
label:
|
|
79
|
+
label: `Item 1 - with a lot of text and using a custom maxWidth value of
|
|
80
|
+
${args.maxWidth}`
|
|
64
81
|
}), /*#__PURE__*/React.createElement(MenuItem, {
|
|
65
|
-
label:
|
|
82
|
+
label: `Item 2 - with a lot of text and using a custom maxWidth value of
|
|
83
|
+
${args.maxWidth}`
|
|
66
84
|
})));
|
|
67
85
|
MaxWidth.args = {
|
|
68
86
|
maxWidth: '300px'
|
|
@@ -156,9 +174,7 @@ WithVariousChildren.parameters = {
|
|
|
156
174
|
export const DropDownMenu = args => {
|
|
157
175
|
const ref = useRef();
|
|
158
176
|
const [open, setOpen] = useState(false);
|
|
159
|
-
|
|
160
177
|
const toggle = () => setOpen(!open);
|
|
161
|
-
|
|
162
178
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
163
179
|
ref: ref,
|
|
164
180
|
onClick: toggle
|
|
@@ -198,15 +214,12 @@ export const WithCustomMenuItem = args => {
|
|
|
198
214
|
const WIDTH = 1400;
|
|
199
215
|
const centerY = (window.screen.height - HEIGHT) / 2;
|
|
200
216
|
const centerX = (window.screen.width - WIDTH) / 2;
|
|
201
|
-
|
|
202
|
-
const onClick = () => window.open(to, 'Popup', ['menubar=no', 'location=no', 'resizable=no', 'scrollbars=no', 'status=no', "width=".concat(WIDTH), "height=".concat(HEIGHT), "top=".concat(centerY), "left=".concat(centerX)].join());
|
|
203
|
-
|
|
217
|
+
const onClick = () => window.open(to, 'Popup', ['menubar=no', 'location=no', 'resizable=no', 'scrollbars=no', 'status=no', `width=${WIDTH}`, `height=${HEIGHT}`, `top=${centerY}`, `left=${centerX}`].join());
|
|
204
218
|
return /*#__PURE__*/React.createElement(MenuItem, _extends({
|
|
205
219
|
onClick: onClick,
|
|
206
220
|
label: children
|
|
207
221
|
}, rest));
|
|
208
222
|
};
|
|
209
|
-
|
|
210
223
|
return /*#__PURE__*/React.createElement(FlyoutMenu, args, /*#__PURE__*/React.createElement(MenuItem, {
|
|
211
224
|
label: "A normal menu item"
|
|
212
225
|
}), /*#__PURE__*/React.createElement(PopupWindowMenuItem, {
|
|
@@ -72,9 +72,9 @@ describe('Menu Component', () => {
|
|
|
72
72
|
const menuItem1 = getByText(/Menu item 1/i);
|
|
73
73
|
const menuItem2 = getByText(/Menu item 2/i);
|
|
74
74
|
expect(menu).not.toHaveFocus();
|
|
75
|
-
userEvent.tab();
|
|
75
|
+
userEvent.tab();
|
|
76
|
+
// check if LI parent node has focus or not
|
|
76
77
|
// headers and dividers do not receive focus
|
|
77
|
-
|
|
78
78
|
expect(header.parentNode.parentNode).not.toHaveFocus();
|
|
79
79
|
expect(divider.parentNode.parentNode).not.toHaveFocus();
|
|
80
80
|
expect(menuItem2.parentNode.parentNode).not.toHaveFocus();
|
|
@@ -100,8 +100,8 @@ describe('Menu Component', () => {
|
|
|
100
100
|
const menuItem1 = getByText(/Menu item 1/i);
|
|
101
101
|
const menuItem2 = getByText(/Menu item 2/i);
|
|
102
102
|
userEvent.tab();
|
|
103
|
-
expect(menuItem1.parentNode.parentNode).toHaveFocus();
|
|
104
|
-
|
|
103
|
+
expect(menuItem1.parentNode.parentNode).toHaveFocus();
|
|
104
|
+
// simulate arrowDown press
|
|
105
105
|
userEvent.keyboard('{ArrowDown}');
|
|
106
106
|
expect(menuItem1.parentNode.parentNode).not.toHaveFocus();
|
|
107
107
|
expect(menuItem2.parentNode.parentNode).toHaveFocus();
|
|
@@ -129,8 +129,9 @@ describe('Menu Component', () => {
|
|
|
129
129
|
const menuItem1 = getByText(/Menu item 1/i);
|
|
130
130
|
const menuItem2 = getByText(/Menu item 2/i);
|
|
131
131
|
userEvent.tab();
|
|
132
|
-
expect(menuItem1.parentNode.parentNode).toHaveFocus();
|
|
132
|
+
expect(menuItem1.parentNode.parentNode).toHaveFocus();
|
|
133
133
|
|
|
134
|
+
// simulate arrowUp press
|
|
134
135
|
userEvent.keyboard('{ArrowUp}');
|
|
135
136
|
expect(menuItem1.parentNode.parentNode).not.toHaveFocus();
|
|
136
137
|
expect(menuItem2.parentNode.parentNode).toHaveFocus();
|
|
@@ -178,8 +179,9 @@ describe('Menu Component', () => {
|
|
|
178
179
|
}, "Span 2"))));
|
|
179
180
|
const nonListMenuItem = getByText(/span 1/i);
|
|
180
181
|
const listMenuItem = getByText(/link 2/i);
|
|
181
|
-
const plainListItem = getByText(/span 2/i);
|
|
182
|
+
const plainListItem = getByText(/span 2/i);
|
|
182
183
|
|
|
184
|
+
// all children must be list items
|
|
183
185
|
expect(nonListMenuItem.parentElement.nodeName).toBe('LI');
|
|
184
186
|
userEvent.tab();
|
|
185
187
|
expect(nonListMenuItem.parentElement).toHaveFocus();
|
|
@@ -190,8 +192,8 @@ describe('Menu Component', () => {
|
|
|
190
192
|
userEvent.keyboard('{ArrowDown}');
|
|
191
193
|
expect(listMenuItem.parentElement).toHaveFocus();
|
|
192
194
|
userEvent.keyboard('{ArrowDown}');
|
|
193
|
-
expect(nonListMenuItem.parentElement).toHaveFocus();
|
|
194
|
-
|
|
195
|
+
expect(nonListMenuItem.parentElement).toHaveFocus();
|
|
196
|
+
// non menu items do not receive focus
|
|
195
197
|
expect(plainListItem.parentElement).not.toHaveFocus();
|
|
196
198
|
});
|
|
197
199
|
it('does not hijack input change value if space entered [bug]', () => {
|
package/build/es/menu/helpers.js
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
const isMenuItem = role => {
|
|
2
2
|
return ['menuitem', 'menuitemcheckbox', 'menuitemradio'].includes(role);
|
|
3
3
|
};
|
|
4
|
-
|
|
5
4
|
const isValidMenuItemNode = node => {
|
|
6
5
|
if (node.nodeName === 'LI' && node.firstElementChild) {
|
|
7
6
|
return isValidMenuItemNode(node.firstElementChild);
|
|
8
7
|
}
|
|
8
|
+
const role = node.getAttribute('role');
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// for h1 - h6 headings since their heading role is not explicitly set
|
|
11
11
|
// style elements do not have roles
|
|
12
|
-
|
|
13
12
|
if (node.nodeName.startsWith('H') || node.nodeName === 'STYLE') {
|
|
14
13
|
return false;
|
|
15
14
|
}
|
|
16
|
-
|
|
17
15
|
if (role) {
|
|
18
16
|
return isMenuItem(role);
|
|
19
17
|
} else {
|
|
20
18
|
console.warn('Missing: role attribute on the menu child');
|
|
21
19
|
}
|
|
22
20
|
};
|
|
23
|
-
|
|
24
21
|
export const getFocusableItemsIndices = elements => {
|
|
25
22
|
const focusableIndices = [];
|
|
26
23
|
elements.forEach((node, index) => {
|
|
@@ -32,6 +29,5 @@ export const getFocusableItemsIndices = elements => {
|
|
|
32
29
|
};
|
|
33
30
|
export const hasMenuItemRole = component => {
|
|
34
31
|
var _component$props;
|
|
35
|
-
|
|
36
32
|
return isMenuItem(component === null || component === void 0 ? void 0 : (_component$props = component.props) === null || _component$props === void 0 ? void 0 : _component$props['role']);
|
|
37
33
|
};
|
package/build/es/menu/menu.js
CHANGED
|
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import React, { Children, cloneElement, isValidElement, useMemo } from 'react';
|
|
4
4
|
import { hasMenuItemRole } from './helpers.js';
|
|
5
5
|
import { useMenuNavigation } from './use-menu.js';
|
|
6
|
-
|
|
7
6
|
const Menu = _ref => {
|
|
8
7
|
let {
|
|
9
8
|
children,
|
|
@@ -19,18 +18,20 @@ const Menu = _ref => {
|
|
|
19
18
|
if (! /*#__PURE__*/isValidElement(child)) {
|
|
20
19
|
return child;
|
|
21
20
|
}
|
|
22
|
-
|
|
23
21
|
const tabIndex = index === focusedIndex ? 0 : -1;
|
|
24
|
-
const childProps = {
|
|
25
|
-
|
|
22
|
+
const childProps = {
|
|
23
|
+
...child.props
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// this check is based on the type of child.
|
|
26
27
|
// if it is a native HTML element, like li, a, span, only apply its child props
|
|
27
28
|
// if it is a functional (React) component, it applies custom props, like dense, hideDivider, etc
|
|
28
|
-
|
|
29
29
|
if (typeof child.type === 'string') {
|
|
30
30
|
// if the native HTML element child is not li, then wrap it in an li tag
|
|
31
31
|
// apply the tabindex prop if a child has the menuitem role to make it focusable
|
|
32
32
|
if (child.type === 'li') {
|
|
33
|
-
return hasMenuItemRole(child.props.children[0]) ? /*#__PURE__*/cloneElement(child, {
|
|
33
|
+
return hasMenuItemRole(child.props.children[0]) ? /*#__PURE__*/cloneElement(child, {
|
|
34
|
+
...childProps,
|
|
34
35
|
tabIndex
|
|
35
36
|
}) : /*#__PURE__*/cloneElement(child, childProps);
|
|
36
37
|
} else {
|
|
@@ -41,7 +42,8 @@ const Menu = _ref => {
|
|
|
41
42
|
} else {
|
|
42
43
|
childProps.dense = typeof child.props.dense === 'boolean' ? child.props.dense : dense;
|
|
43
44
|
childProps.hideDivider = typeof child.props.hideDivider !== 'boolean' && index === 0 ? true : child.props.hideDivider;
|
|
44
|
-
return /*#__PURE__*/cloneElement(child, {
|
|
45
|
+
return /*#__PURE__*/cloneElement(child, {
|
|
46
|
+
...childProps,
|
|
45
47
|
tabIndex
|
|
46
48
|
});
|
|
47
49
|
}
|
|
@@ -56,7 +58,6 @@ const Menu = _ref => {
|
|
|
56
58
|
id: "1636612837"
|
|
57
59
|
}, ["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;}"]));
|
|
58
60
|
};
|
|
59
|
-
|
|
60
61
|
Menu.defaultProps = {
|
|
61
62
|
dataTest: 'dhis2-uicore-menulist'
|
|
62
63
|
};
|
|
@@ -65,7 +66,6 @@ Menu.propTypes = {
|
|
|
65
66
|
children: PropTypes.node,
|
|
66
67
|
className: PropTypes.string,
|
|
67
68
|
dataTest: PropTypes.string,
|
|
68
|
-
|
|
69
69
|
/** Applies `dense` property to all child components unless already specified */
|
|
70
70
|
dense: PropTypes.bool
|
|
71
71
|
};
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { MenuItem, MenuSectionHeader } from '../index.js';
|
|
3
3
|
import { Menu } from './index.js';
|
|
4
|
-
const description =
|
|
4
|
+
const description = `
|
|
5
|
+
Use menus to provide access to options and actions where space is limited and displaying all the options would be impractical. For example, providing access to a range of actions for every dashboard item displayed. Containing all those actions in menus keeps the page manageable.
|
|
6
|
+
|
|
7
|
+
The menu component is flexible in where it can be used and its contents can be flexible too. However, the most common use case is a menu containing menu items.
|
|
8
|
+
|
|
9
|
+
Make sure the menu item labels are short and easy to understand. One word is often enough to describe an action or option. Do not use sentences as labels. Some examples of good menu item labels:
|
|
10
|
+
|
|
11
|
+
- "Save"
|
|
12
|
+
- "Open as map"
|
|
13
|
+
- "Export PDF"
|
|
14
|
+
- "Duplicate"
|
|
15
|
+
|
|
16
|
+
Typical children are Menu Items, Menu Dividers, and Menu Section Headers.
|
|
17
|
+
|
|
18
|
+
\`\`\`js
|
|
19
|
+
import { Menu } from '@dhis2/ui'
|
|
20
|
+
\`\`\`
|
|
21
|
+
`;
|
|
5
22
|
export default {
|
|
6
23
|
title: 'Menu',
|
|
7
24
|
component: Menu,
|
|
@@ -3,17 +3,19 @@ import { getFocusableItemsIndices } from './helpers.js';
|
|
|
3
3
|
export const useMenuNavigation = children => {
|
|
4
4
|
const menuRef = useRef(null);
|
|
5
5
|
const [focusableItemsIndices, setFocusableItemsIndices] = useState(null);
|
|
6
|
-
const [activeItemIndex, setActiveItemIndex] = useState(-1);
|
|
7
|
-
// focusable items have the role of menuitem || menuitemcheckbox || menuitemradio
|
|
6
|
+
const [activeItemIndex, setActiveItemIndex] = useState(-1);
|
|
8
7
|
|
|
8
|
+
// Initializes the indices for focusable items
|
|
9
|
+
// focusable items have the role of menuitem || menuitemcheckbox || menuitemradio
|
|
9
10
|
useEffect(() => {
|
|
10
11
|
if (menuRef) {
|
|
11
12
|
const menuItems = Array.from(menuRef.current.children);
|
|
12
13
|
const itemsIndices = getFocusableItemsIndices(menuItems);
|
|
13
14
|
setFocusableItemsIndices(itemsIndices);
|
|
14
15
|
}
|
|
15
|
-
}, [children]);
|
|
16
|
+
}, [children]);
|
|
16
17
|
|
|
18
|
+
// Focus the active menu child
|
|
17
19
|
useEffect(() => {
|
|
18
20
|
if (menuRef) {
|
|
19
21
|
if (focusableItemsIndices !== null && focusableItemsIndices !== void 0 && focusableItemsIndices.length && activeItemIndex > -1) {
|
|
@@ -21,50 +23,45 @@ export const useMenuNavigation = children => {
|
|
|
21
23
|
menuRef.current.children[currentIndex].focus();
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
|
-
}, [activeItemIndex, focusableItemsIndices]);
|
|
25
|
-
// Trigger actionable items
|
|
26
|
+
}, [activeItemIndex, focusableItemsIndices]);
|
|
26
27
|
|
|
28
|
+
// Navigate through focusable children using arrow keys
|
|
29
|
+
// Trigger actionable items
|
|
27
30
|
const handleKeyDown = useCallback(event => {
|
|
28
31
|
const totalFocusablePositions = focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices.length;
|
|
29
|
-
|
|
30
32
|
if (totalFocusablePositions) {
|
|
31
33
|
const lastIndex = totalFocusablePositions - 1;
|
|
32
|
-
|
|
33
34
|
switch (event.key) {
|
|
34
35
|
case 'ArrowUp':
|
|
35
36
|
event.preventDefault();
|
|
36
37
|
setActiveItemIndex(activeItemIndex > 0 ? activeItemIndex - 1 : lastIndex);
|
|
37
38
|
break;
|
|
38
|
-
|
|
39
39
|
case 'ArrowDown':
|
|
40
40
|
event.preventDefault();
|
|
41
41
|
setActiveItemIndex(activeItemIndex >= lastIndex ? 0 : activeItemIndex + 1);
|
|
42
42
|
break;
|
|
43
|
-
|
|
44
43
|
case 'Enter':
|
|
45
44
|
case ' ':
|
|
46
45
|
if (event.target.nodeName === 'LI') {
|
|
47
46
|
var _event$target$childre, _event$target$childre2;
|
|
48
|
-
|
|
49
47
|
event.preventDefault();
|
|
50
48
|
(_event$target$childre = event.target.children) === null || _event$target$childre === void 0 ? void 0 : (_event$target$childre2 = _event$target$childre[0]) === null || _event$target$childre2 === void 0 ? void 0 : _event$target$childre2.click();
|
|
51
49
|
}
|
|
52
|
-
|
|
53
50
|
break;
|
|
54
|
-
|
|
55
51
|
default:
|
|
56
52
|
break;
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
|
-
}, [activeItemIndex, focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices.length]);
|
|
55
|
+
}, [activeItemIndex, focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices.length]);
|
|
60
56
|
|
|
57
|
+
// Event listeners for menu focus and key handling
|
|
61
58
|
useEffect(() => {
|
|
62
59
|
if (!menuRef) {
|
|
63
60
|
return;
|
|
64
61
|
}
|
|
62
|
+
const menu = menuRef.current;
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// Focus the first menu item when the menu receives focus
|
|
68
65
|
const handleFocus = event => {
|
|
69
66
|
if (event.target === menuRef.current) {
|
|
70
67
|
const firstItemIndex = focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices[0];
|
|
@@ -72,7 +69,6 @@ export const useMenuNavigation = children => {
|
|
|
72
69
|
setActiveItemIndex(0);
|
|
73
70
|
}
|
|
74
71
|
};
|
|
75
|
-
|
|
76
72
|
menu.addEventListener('focus', handleFocus);
|
|
77
73
|
menu.addEventListener('keydown', handleKeyDown);
|
|
78
74
|
return () => {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import _JSXStyle from "styled-jsx/style";
|
|
2
|
-
import { Divider } from '@dhis2-ui/divider';
|
|
3
2
|
import { colors } from '@dhis2/ui-constants';
|
|
3
|
+
import { Divider } from '@dhis2-ui/divider';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import React from 'react';
|
|
6
|
-
|
|
7
6
|
const MenuDivider = _ref => {
|
|
8
7
|
let {
|
|
9
8
|
className,
|
|
@@ -18,9 +17,8 @@ const MenuDivider = _ref => {
|
|
|
18
17
|
}), /*#__PURE__*/React.createElement(_JSXStyle, {
|
|
19
18
|
id: "591815244",
|
|
20
19
|
dynamic: [colors.white]
|
|
21
|
-
}, [
|
|
20
|
+
}, [`li.__jsx-style-dynamic-selector{list-style:none;background-color:${colors.white};-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;padding:0;line-height:0;}`]));
|
|
22
21
|
};
|
|
23
|
-
|
|
24
22
|
MenuDivider.defaultProps = {
|
|
25
23
|
dataTest: 'dhis2-uicore-menudivider'
|
|
26
24
|
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Menu, MenuItem } from '../index.js';
|
|
3
3
|
import { MenuDivider } from './menu-divider.js';
|
|
4
|
-
const description =
|
|
4
|
+
const description = `
|
|
5
|
+
Items in a menu can be split into separate sections by using dividers. Group relevant menu items together to help the user understand the options quickly. A divider can be used alone. If using a MenuSectionHeader, a divider will be automatically included. Try not to group single menu items together. An exception to this is a critical destructive menu item, like 'Delete', which can be separated from other menu items.
|
|
6
|
+
|
|
7
|
+
\`\`\`js
|
|
8
|
+
import { MenuDivider } from '@dhis2/ui'
|
|
9
|
+
\`\`\`
|
|
10
|
+
`;
|
|
5
11
|
export default {
|
|
6
12
|
title: 'Menu Divider',
|
|
7
13
|
component: MenuDivider,
|
|
@@ -13,13 +19,11 @@ export default {
|
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
21
|
};
|
|
16
|
-
|
|
17
22
|
const Template = args => /*#__PURE__*/React.createElement(Menu, null, /*#__PURE__*/React.createElement(MenuItem, {
|
|
18
23
|
label: "Item above divider"
|
|
19
24
|
}), /*#__PURE__*/React.createElement(MenuDivider, args), /*#__PURE__*/React.createElement(MenuItem, {
|
|
20
25
|
label: "Item below divider"
|
|
21
26
|
}));
|
|
22
|
-
|
|
23
27
|
export const Default = Template.bind({});
|
|
24
28
|
export const Dense = Template.bind({});
|
|
25
29
|
Dense.args = {
|
|
@@ -3,5 +3,5 @@ Given('a MenuItem with href is rendered', () => {
|
|
|
3
3
|
cy.visitStory('MenuItem', 'With Href');
|
|
4
4
|
});
|
|
5
5
|
Then('a link is rendered with the href', () => {
|
|
6
|
-
cy.get('a').should('have.attr', 'href').and('include', 'url.test');
|
|
6
|
+
cy.get('#storybook-root a').should('have.attr', 'href').and('include', 'url.test');
|
|
7
7
|
});
|
|
@@ -4,5 +4,5 @@ Given('a MenuItem supplied with a label is rendered', () => {
|
|
|
4
4
|
cy.get('[data-test="dhis2-uicore-menuitem"]').should('be.visible');
|
|
5
5
|
});
|
|
6
6
|
Then('the label is visible', () => {
|
|
7
|
-
cy.contains(
|
|
7
|
+
cy.get(':contains("label")').should('be.visible');
|
|
8
8
|
});
|
|
@@ -3,5 +3,5 @@ Given('a MenuItem with target is rendered', () => {
|
|
|
3
3
|
cy.visitStory('MenuItem', 'With Target');
|
|
4
4
|
});
|
|
5
5
|
Then('a link is rendered with the target', () => {
|
|
6
|
-
cy.get('a').should('have.attr', 'target').and('include', '_blank');
|
|
6
|
+
cy.get('#storybook-root a').should('have.attr', 'target').and('include', '_blank');
|
|
7
7
|
});
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import _JSXStyle from "styled-jsx/style";
|
|
2
|
+
import { IconChevronRight24 } from '@dhis2/ui-icons';
|
|
2
3
|
import { Popper } from '@dhis2-ui/popper';
|
|
3
4
|
import { Portal } from '@dhis2-ui/portal';
|
|
4
|
-
import { IconChevronRight24 } from '@dhis2/ui-icons';
|
|
5
5
|
import cx from 'classnames';
|
|
6
6
|
import PropTypes from 'prop-types';
|
|
7
7
|
import React, { useEffect, useRef, useState } from 'react';
|
|
8
|
-
import { FlyoutMenu } from '../index.js';
|
|
8
|
+
import { FlyoutMenu } from '../flyout-menu/index.js';
|
|
9
9
|
import styles from './menu-item.styles.js';
|
|
10
|
-
|
|
11
10
|
const isModifiedEvent = evt => evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey;
|
|
12
|
-
|
|
13
11
|
const createOnClickHandler = _ref => {
|
|
14
12
|
let {
|
|
15
13
|
onClick,
|
|
@@ -21,7 +19,6 @@ const createOnClickHandler = _ref => {
|
|
|
21
19
|
if (isLink && isModifiedEvent(evt) || !(onClick || toggleSubMenu)) {
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
24
|
-
|
|
25
22
|
evt.preventDefault();
|
|
26
23
|
evt.stopPropagation();
|
|
27
24
|
onClick && onClick({
|
|
@@ -30,7 +27,6 @@ const createOnClickHandler = _ref => {
|
|
|
30
27
|
toggleSubMenu && toggleSubMenu();
|
|
31
28
|
};
|
|
32
29
|
};
|
|
33
|
-
|
|
34
30
|
const MenuItem = _ref2 => {
|
|
35
31
|
let {
|
|
36
32
|
href,
|
|
@@ -64,26 +60,19 @@ const MenuItem = _ref2 => {
|
|
|
64
60
|
if (!menuItemRef.current) {
|
|
65
61
|
return;
|
|
66
62
|
}
|
|
67
|
-
|
|
68
63
|
const menuItem = menuItemRef.current;
|
|
69
|
-
|
|
70
64
|
const handleKeyDown = event => {
|
|
71
65
|
var _openSubMenus, _openSubMenus2;
|
|
72
|
-
|
|
73
66
|
const firstChild = event.target.children[0];
|
|
74
67
|
const hasSubMenu = firstChild === null || firstChild === void 0 ? void 0 : firstChild.getAttribute('aria-haspopup');
|
|
75
|
-
|
|
76
68
|
switch (event.key) {
|
|
77
69
|
// for submenus
|
|
78
70
|
case 'ArrowRight':
|
|
79
71
|
event.preventDefault();
|
|
80
|
-
|
|
81
72
|
if (hasSubMenu) {
|
|
82
73
|
firstChild.click();
|
|
83
74
|
}
|
|
84
|
-
|
|
85
75
|
break;
|
|
86
|
-
|
|
87
76
|
case 'ArrowLeft':
|
|
88
77
|
case 'Escape':
|
|
89
78
|
// close flyout menu
|
|
@@ -93,7 +82,6 @@ const MenuItem = _ref2 => {
|
|
|
93
82
|
break;
|
|
94
83
|
}
|
|
95
84
|
};
|
|
96
|
-
|
|
97
85
|
menuItem.addEventListener('keydown', handleKeyDown);
|
|
98
86
|
return () => {
|
|
99
87
|
menuItem.removeEventListener('keydown', handleKeyDown);
|
|
@@ -105,7 +93,7 @@ const MenuItem = _ref2 => {
|
|
|
105
93
|
role: "presentation",
|
|
106
94
|
tabIndex: tabIndex,
|
|
107
95
|
"data-submenu-open": children && showSubMenu,
|
|
108
|
-
className:
|
|
96
|
+
className: `jsx-${styles.__hash}` + " " + (cx(className, {
|
|
109
97
|
destructive,
|
|
110
98
|
disabled,
|
|
111
99
|
dense,
|
|
@@ -127,15 +115,15 @@ const MenuItem = _ref2 => {
|
|
|
127
115
|
"aria-haspopup": children && 'menu',
|
|
128
116
|
"aria-expanded": showSubMenu,
|
|
129
117
|
"aria-label": label,
|
|
130
|
-
className:
|
|
118
|
+
className: `jsx-${styles.__hash}`
|
|
131
119
|
}, icon && /*#__PURE__*/React.createElement("span", {
|
|
132
|
-
className:
|
|
120
|
+
className: `jsx-${styles.__hash}` + " " + "icon"
|
|
133
121
|
}, icon), /*#__PURE__*/React.createElement("span", {
|
|
134
|
-
className:
|
|
122
|
+
className: `jsx-${styles.__hash}` + " " + "label"
|
|
135
123
|
}, label), suffix && /*#__PURE__*/React.createElement("span", {
|
|
136
|
-
className:
|
|
124
|
+
className: `jsx-${styles.__hash}` + " " + "suffix"
|
|
137
125
|
}, suffix), (chevron || children) && /*#__PURE__*/React.createElement("span", {
|
|
138
|
-
className:
|
|
126
|
+
className: `jsx-${styles.__hash}` + " " + "chevron"
|
|
139
127
|
}, /*#__PURE__*/React.createElement(IconChevronRight24, null))), /*#__PURE__*/React.createElement(_JSXStyle, {
|
|
140
128
|
id: styles.__hash
|
|
141
129
|
}, styles)), children && showSubMenu && /*#__PURE__*/React.createElement(Portal, null, /*#__PURE__*/React.createElement(Popper, {
|
|
@@ -145,7 +133,6 @@ const MenuItem = _ref2 => {
|
|
|
145
133
|
dense: dense
|
|
146
134
|
}, children))));
|
|
147
135
|
};
|
|
148
|
-
|
|
149
136
|
MenuItem.defaultProps = {
|
|
150
137
|
dataTest: 'dhis2-uicore-menuitem'
|
|
151
138
|
};
|
|
@@ -154,7 +141,6 @@ MenuItem.propTypes = {
|
|
|
154
141
|
checkbox: PropTypes.bool,
|
|
155
142
|
checked: PropTypes.bool,
|
|
156
143
|
chevron: PropTypes.bool,
|
|
157
|
-
|
|
158
144
|
/**
|
|
159
145
|
* Nested menu items can become submenus.
|
|
160
146
|
* See `showSubMenu` and `toggleSubMenu` props, and 'Children' demo
|
|
@@ -165,32 +151,23 @@ MenuItem.propTypes = {
|
|
|
165
151
|
dense: PropTypes.bool,
|
|
166
152
|
destructive: PropTypes.bool,
|
|
167
153
|
disabled: PropTypes.bool,
|
|
168
|
-
|
|
169
154
|
/** For using menu item as a link */
|
|
170
155
|
href: PropTypes.string,
|
|
171
|
-
|
|
172
156
|
/** An icon for the left side of the menu item */
|
|
173
157
|
icon: PropTypes.node,
|
|
174
|
-
|
|
175
158
|
/** Text in the menu item */
|
|
176
159
|
label: PropTypes.node,
|
|
177
|
-
|
|
178
160
|
/** When true, nested menu items are shown in a Popper */
|
|
179
161
|
showSubMenu: PropTypes.bool,
|
|
180
|
-
|
|
181
162
|
/** A supporting element shown at the end of the menu item */
|
|
182
163
|
suffix: PropTypes.node,
|
|
183
164
|
tabIndex: PropTypes.number,
|
|
184
|
-
|
|
185
165
|
/** For using menu item as a link */
|
|
186
166
|
target: PropTypes.string,
|
|
187
|
-
|
|
188
167
|
/** On click, this function is called (without args) */
|
|
189
168
|
toggleSubMenu: PropTypes.func,
|
|
190
|
-
|
|
191
169
|
/** Value associated with item. Passed as an argument to onClick handler. */
|
|
192
170
|
value: PropTypes.string,
|
|
193
|
-
|
|
194
171
|
/** Click handler called with signature `({ value: string }, event)` */
|
|
195
172
|
onClick: PropTypes.func
|
|
196
173
|
};
|