@dhis2-ui/menu 9.10.3 → 9.11.1-beta.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.
- package/build/cjs/flyout-menu/__tests__/flyout-menu.test.js +44 -0
- 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 +37 -19
- package/build/cjs/flyout-menu/{flyout-menu.stories.js → flyout-menu.prod.stories.js} +28 -38
- 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 +48 -38
- 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/__tests__/flyout-menu.test.js +41 -0
- 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 +35 -9
- package/build/es/flyout-menu/{flyout-menu.stories.js → flyout-menu.prod.stories.js} +28 -13
- 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 +46 -24
- 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/types/index.d.ts +4 -0
- 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
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import _JSXStyle from "styled-jsx/style";
|
|
2
2
|
import { colors, elevations, spacers } from '@dhis2/ui-constants';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
|
-
import React, { Children, cloneElement, isValidElement, useState } from 'react';
|
|
5
|
-
import { Menu } from '../index.js';
|
|
6
|
-
|
|
4
|
+
import React, { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { Menu } from '../menu/index.js';
|
|
7
6
|
const FlyoutMenu = _ref => {
|
|
8
7
|
let {
|
|
9
8
|
children,
|
|
@@ -11,17 +10,44 @@ const FlyoutMenu = _ref => {
|
|
|
11
10
|
dataTest,
|
|
12
11
|
dense,
|
|
13
12
|
maxHeight,
|
|
14
|
-
maxWidth
|
|
13
|
+
maxWidth,
|
|
14
|
+
closeMenu
|
|
15
15
|
} = _ref;
|
|
16
16
|
const [openedSubMenu, setOpenedSubMenu] = useState(null);
|
|
17
|
-
|
|
18
17
|
const toggleSubMenu = index => {
|
|
19
18
|
const toggleValue = index === openedSubMenu ? null : index;
|
|
20
19
|
setOpenedSubMenu(toggleValue);
|
|
21
20
|
};
|
|
22
|
-
|
|
21
|
+
const divRef = useRef(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!divRef.current) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const div = divRef.current;
|
|
27
|
+
const handleFocus = event => {
|
|
28
|
+
if (event.target === div) {
|
|
29
|
+
if (div !== null && div !== void 0 && div.children && div.children.length > 0) {
|
|
30
|
+
div.children[0].focus();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const handleKeyDown = event => {
|
|
35
|
+
if (event.key === 'Escape') {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
closeMenu && closeMenu();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
div.addEventListener('focus', handleFocus);
|
|
41
|
+
div.addEventListener('keydown', handleKeyDown);
|
|
42
|
+
return () => {
|
|
43
|
+
div.removeEventListener('focus', handleFocus);
|
|
44
|
+
div.removeEventListener('keydown', handleKeyDown);
|
|
45
|
+
};
|
|
46
|
+
}, [closeMenu]);
|
|
23
47
|
return /*#__PURE__*/React.createElement("div", {
|
|
24
48
|
"data-test": dataTest,
|
|
49
|
+
tabIndex: 0,
|
|
50
|
+
ref: divRef,
|
|
25
51
|
className: _JSXStyle.dynamic([["3833750986", [colors.white, colors.grey200, elevations.e300, dense ? '128' : '180', maxWidth, maxHeight, spacers.dp4]]]) + " " + (className || "")
|
|
26
52
|
}, /*#__PURE__*/React.createElement(Menu, {
|
|
27
53
|
dense: dense
|
|
@@ -31,9 +57,8 @@ const FlyoutMenu = _ref => {
|
|
|
31
57
|
}) : child)), /*#__PURE__*/React.createElement(_JSXStyle, {
|
|
32
58
|
id: "3833750986",
|
|
33
59
|
dynamic: [colors.white, colors.grey200, elevations.e300, dense ? '128' : '180', maxWidth, maxHeight, spacers.dp4]
|
|
34
|
-
}, [
|
|
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;}`]));
|
|
35
61
|
};
|
|
36
|
-
|
|
37
62
|
FlyoutMenu.defaultProps = {
|
|
38
63
|
dataTest: 'dhis2-uicore-menu',
|
|
39
64
|
maxWidth: '380px',
|
|
@@ -43,8 +68,9 @@ FlyoutMenu.propTypes = {
|
|
|
43
68
|
/** Typically, but not limited to, `MenuItem` components */
|
|
44
69
|
children: PropTypes.node,
|
|
45
70
|
className: PropTypes.string,
|
|
71
|
+
/** when Escape key is pressed, this function is called to close the flyout menu */
|
|
72
|
+
closeMenu: PropTypes.func,
|
|
46
73
|
dataTest: PropTypes.string,
|
|
47
|
-
|
|
48
74
|
/** Menu uses smaller dimensions */
|
|
49
75
|
dense: PropTypes.bool,
|
|
50
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
|
|
@@ -167,7 +183,9 @@ export const DropDownMenu = args => {
|
|
|
167
183
|
}, /*#__PURE__*/React.createElement(Popper, {
|
|
168
184
|
reference: ref,
|
|
169
185
|
placement: "bottom-start"
|
|
170
|
-
}, /*#__PURE__*/React.createElement(FlyoutMenu,
|
|
186
|
+
}, /*#__PURE__*/React.createElement(FlyoutMenu, _extends({}, args, {
|
|
187
|
+
closeMenu: toggle
|
|
188
|
+
}), /*#__PURE__*/React.createElement(MenuItem, {
|
|
171
189
|
label: "Item 1"
|
|
172
190
|
}), /*#__PURE__*/React.createElement(MenuItem, {
|
|
173
191
|
label: "Item 2"
|
|
@@ -196,15 +214,12 @@ export const WithCustomMenuItem = args => {
|
|
|
196
214
|
const WIDTH = 1400;
|
|
197
215
|
const centerY = (window.screen.height - HEIGHT) / 2;
|
|
198
216
|
const centerX = (window.screen.width - WIDTH) / 2;
|
|
199
|
-
|
|
200
|
-
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());
|
|
201
|
-
|
|
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());
|
|
202
218
|
return /*#__PURE__*/React.createElement(MenuItem, _extends({
|
|
203
219
|
onClick: onClick,
|
|
204
220
|
label: children
|
|
205
221
|
}, rest));
|
|
206
222
|
};
|
|
207
|
-
|
|
208
223
|
return /*#__PURE__*/React.createElement(FlyoutMenu, args, /*#__PURE__*/React.createElement(MenuItem, {
|
|
209
224
|
label: "A normal menu item"
|
|
210
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
|
|
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
|
});
|