@dhis2-ui/menu 9.10.3 → 9.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ var _react = require("@testing-library/react");
4
+
5
+ var _userEvent = _interopRequireDefault(require("@testing-library/user-event"));
6
+
7
+ var _react2 = _interopRequireDefault(require("react"));
8
+
9
+ var _menuItem = require("../../menu-item/menu-item.js");
10
+
11
+ var _flyoutMenu = require("../flyout-menu.js");
12
+
13
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
+
15
+ describe('Flyout Menu Component', () => {
16
+ it('can handle navigation of submenus', () => {
17
+ const {
18
+ getByText,
19
+ queryByText,
20
+ getAllByRole
21
+ } = (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_flyoutMenu.FlyoutMenu, null, /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
22
+ label: "Item 1"
23
+ }), /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
24
+ label: "Item 2"
25
+ }, /*#__PURE__*/_react2.default.createElement(_menuItem.MenuItem, {
26
+ label: "Item 2 a"
27
+ }))));
28
+ const itemOne = getByText(/Item 1/i);
29
+ const itemTwo = getByText(/Item 2/i);
30
+ let submenuChild = queryByText(/Item 2 a/i);
31
+ const menuItems = getAllByRole('menuitem');
32
+ expect(menuItems.length).toBe(2);
33
+ expect(menuItems[0]).toBe(itemOne.parentNode);
34
+ expect(menuItems[1]).toBe(itemTwo.parentNode);
35
+ expect(submenuChild).not.toBeInTheDocument();
36
+
37
+ _userEvent.default.tab();
38
+
39
+ expect(menuItems[0].parentNode).toHaveFocus();
40
+ expect(menuItems[1].parentNode).not.toHaveFocus();
41
+
42
+ _userEvent.default.keyboard('{ArrowDown}');
43
+
44
+ expect(menuItems[0].parentNode).not.toHaveFocus();
45
+ expect(menuItems[1].parentNode).toHaveFocus();
46
+
47
+ _userEvent.default.keyboard('{ArrowRight}');
48
+
49
+ submenuChild = getByText(/Item 2 a/i);
50
+ expect(submenuChild).toBeInTheDocument();
51
+ expect(submenuChild.parentElement.parentElement).toHaveFocus();
52
+
53
+ _userEvent.default.keyboard('{ArrowLeft}');
54
+
55
+ expect(queryByText(/Item 2 a/i)).not.toBeInTheDocument();
56
+ expect(menuItems[1].parentNode).toHaveFocus();
57
+ });
58
+ });
@@ -28,7 +28,8 @@ const FlyoutMenu = _ref => {
28
28
  dataTest,
29
29
  dense,
30
30
  maxHeight,
31
- maxWidth
31
+ maxWidth,
32
+ closeMenu
32
33
  } = _ref;
33
34
  const [openedSubMenu, setOpenedSubMenu] = (0, _react.useState)(null);
34
35
 
@@ -37,8 +38,40 @@ const FlyoutMenu = _ref => {
37
38
  setOpenedSubMenu(toggleValue);
38
39
  };
39
40
 
41
+ const divRef = (0, _react.useRef)(null);
42
+ (0, _react.useEffect)(() => {
43
+ if (!divRef.current) {
44
+ return;
45
+ }
46
+
47
+ const div = divRef.current;
48
+
49
+ const handleFocus = event => {
50
+ if (event.target === div) {
51
+ if (div !== null && div !== void 0 && div.children && div.children.length > 0) {
52
+ div.children[0].focus();
53
+ }
54
+ }
55
+ };
56
+
57
+ const handleKeyDown = event => {
58
+ if (event.key === 'Escape') {
59
+ event.preventDefault();
60
+ closeMenu && closeMenu();
61
+ }
62
+ };
63
+
64
+ div.addEventListener('focus', handleFocus);
65
+ div.addEventListener('keydown', handleKeyDown);
66
+ return () => {
67
+ div.removeEventListener('focus', handleFocus);
68
+ div.removeEventListener('keydown', handleKeyDown);
69
+ };
70
+ }, [closeMenu]);
40
71
  return /*#__PURE__*/_react.default.createElement("div", {
41
72
  "data-test": dataTest,
73
+ tabIndex: 0,
74
+ ref: divRef,
42
75
  className: _style.default.dynamic([["3833750986", [_uiConstants.colors.white, _uiConstants.colors.grey200, _uiConstants.elevations.e300, dense ? '128' : '180', maxWidth, maxHeight, _uiConstants.spacers.dp4]]]) + " " + (className || "")
43
76
  }, /*#__PURE__*/_react.default.createElement(_index.Menu, {
44
77
  dense: dense
@@ -61,6 +94,9 @@ FlyoutMenu.propTypes = {
61
94
  /** Typically, but not limited to, `MenuItem` components */
62
95
  children: _propTypes.default.node,
63
96
  className: _propTypes.default.string,
97
+
98
+ /** when Escape key is pressed, this function is called to close the flyout menu */
99
+ closeMenu: _propTypes.default.func,
64
100
  dataTest: _propTypes.default.string,
65
101
 
66
102
  /** Menu uses smaller dimensions */
@@ -202,7 +202,9 @@ const DropDownMenu = args => {
202
202
  }, /*#__PURE__*/_react.default.createElement(_popper.Popper, {
203
203
  reference: ref,
204
204
  placement: "bottom-start"
205
- }, /*#__PURE__*/_react.default.createElement(_flyoutMenu.FlyoutMenu, args, /*#__PURE__*/_react.default.createElement(_menu.MenuItem, {
205
+ }, /*#__PURE__*/_react.default.createElement(_flyoutMenu.FlyoutMenu, _extends({}, args, {
206
+ closeMenu: toggle
207
+ }), /*#__PURE__*/_react.default.createElement(_menu.MenuItem, {
206
208
  label: "Item 1"
207
209
  }), /*#__PURE__*/_react.default.createElement(_menu.MenuItem, {
208
210
  label: "Item 2"
@@ -65,7 +65,7 @@ const useMenuNavigation = children => {
65
65
  break;
66
66
  }
67
67
  }
68
- }, [activeItemIndex, focusableItemsIndices]); // Event listeners for menu focus and key handling
68
+ }, [activeItemIndex, focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices.length]); // Event listeners for menu focus and key handling
69
69
 
70
70
  (0, _react.useEffect)(() => {
71
71
  if (!menuRef) {
@@ -76,11 +76,56 @@ const MenuItem = _ref2 => {
76
76
  tabIndex
77
77
  } = _ref2;
78
78
  const menuItemRef = (0, _react.useRef)();
79
+ const [openSubMenus, setOpenSubMenus] = (0, _react.useState)([]);
80
+ (0, _react.useEffect)(() => {
81
+ // track open submenus
82
+ setOpenSubMenus(document.querySelectorAll('[data-submenu-open=true]'));
83
+ }, []);
84
+ (0, _react.useEffect)(() => {
85
+ if (!menuItemRef.current) {
86
+ return;
87
+ }
88
+
89
+ const menuItem = menuItemRef.current;
90
+
91
+ const handleKeyDown = event => {
92
+ var _openSubMenus, _openSubMenus2;
93
+
94
+ const firstChild = event.target.children[0];
95
+ const hasSubMenu = firstChild === null || firstChild === void 0 ? void 0 : firstChild.getAttribute('aria-haspopup');
96
+
97
+ switch (event.key) {
98
+ // for submenus
99
+ case 'ArrowRight':
100
+ event.preventDefault();
101
+
102
+ if (hasSubMenu) {
103
+ firstChild.click();
104
+ }
105
+
106
+ break;
107
+
108
+ case 'ArrowLeft':
109
+ case 'Escape':
110
+ // close flyout menu
111
+ event.preventDefault();
112
+ (_openSubMenus = openSubMenus[openSubMenus.length - 1]) === null || _openSubMenus === void 0 ? void 0 : _openSubMenus.focus();
113
+ (_openSubMenus2 = openSubMenus[openSubMenus.length - 1]) === null || _openSubMenus2 === void 0 ? void 0 : _openSubMenus2.children[0].click();
114
+ break;
115
+ }
116
+ };
117
+
118
+ menuItem.addEventListener('keydown', handleKeyDown);
119
+ return () => {
120
+ menuItem.removeEventListener('keydown', handleKeyDown);
121
+ };
122
+ }, [openSubMenus]);
79
123
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("li", {
80
124
  ref: menuItemRef,
81
125
  "data-test": dataTest,
82
126
  role: "presentation",
83
127
  tabIndex: tabIndex,
128
+ "data-submenu-open": children && showSubMenu,
84
129
  className: "jsx-".concat(_menuItemStyles.default.__hash) + " " + ((0, _classnames.default)(className, {
85
130
  destructive,
86
131
  disabled,
@@ -0,0 +1,41 @@
1
+ import { render } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import React from 'react';
4
+ import { MenuItem } from '../../menu-item/menu-item.js';
5
+ import { FlyoutMenu } from '../flyout-menu.js';
6
+ describe('Flyout Menu Component', () => {
7
+ it('can handle navigation of submenus', () => {
8
+ const {
9
+ getByText,
10
+ queryByText,
11
+ getAllByRole
12
+ } = render( /*#__PURE__*/React.createElement(FlyoutMenu, null, /*#__PURE__*/React.createElement(MenuItem, {
13
+ label: "Item 1"
14
+ }), /*#__PURE__*/React.createElement(MenuItem, {
15
+ label: "Item 2"
16
+ }, /*#__PURE__*/React.createElement(MenuItem, {
17
+ label: "Item 2 a"
18
+ }))));
19
+ const itemOne = getByText(/Item 1/i);
20
+ const itemTwo = getByText(/Item 2/i);
21
+ let submenuChild = queryByText(/Item 2 a/i);
22
+ const menuItems = getAllByRole('menuitem');
23
+ expect(menuItems.length).toBe(2);
24
+ expect(menuItems[0]).toBe(itemOne.parentNode);
25
+ expect(menuItems[1]).toBe(itemTwo.parentNode);
26
+ expect(submenuChild).not.toBeInTheDocument();
27
+ userEvent.tab();
28
+ expect(menuItems[0].parentNode).toHaveFocus();
29
+ expect(menuItems[1].parentNode).not.toHaveFocus();
30
+ userEvent.keyboard('{ArrowDown}');
31
+ expect(menuItems[0].parentNode).not.toHaveFocus();
32
+ expect(menuItems[1].parentNode).toHaveFocus();
33
+ userEvent.keyboard('{ArrowRight}');
34
+ submenuChild = getByText(/Item 2 a/i);
35
+ expect(submenuChild).toBeInTheDocument();
36
+ expect(submenuChild.parentElement.parentElement).toHaveFocus();
37
+ userEvent.keyboard('{ArrowLeft}');
38
+ expect(queryByText(/Item 2 a/i)).not.toBeInTheDocument();
39
+ expect(menuItems[1].parentNode).toHaveFocus();
40
+ });
41
+ });
@@ -1,7 +1,7 @@
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';
4
+ import React, { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react';
5
5
  import { Menu } from '../index.js';
6
6
 
7
7
  const FlyoutMenu = _ref => {
@@ -11,7 +11,8 @@ const FlyoutMenu = _ref => {
11
11
  dataTest,
12
12
  dense,
13
13
  maxHeight,
14
- maxWidth
14
+ maxWidth,
15
+ closeMenu
15
16
  } = _ref;
16
17
  const [openedSubMenu, setOpenedSubMenu] = useState(null);
17
18
 
@@ -20,8 +21,40 @@ const FlyoutMenu = _ref => {
20
21
  setOpenedSubMenu(toggleValue);
21
22
  };
22
23
 
24
+ const divRef = useRef(null);
25
+ useEffect(() => {
26
+ if (!divRef.current) {
27
+ return;
28
+ }
29
+
30
+ const div = divRef.current;
31
+
32
+ const handleFocus = event => {
33
+ if (event.target === div) {
34
+ if (div !== null && div !== void 0 && div.children && div.children.length > 0) {
35
+ div.children[0].focus();
36
+ }
37
+ }
38
+ };
39
+
40
+ const handleKeyDown = event => {
41
+ if (event.key === 'Escape') {
42
+ event.preventDefault();
43
+ closeMenu && closeMenu();
44
+ }
45
+ };
46
+
47
+ div.addEventListener('focus', handleFocus);
48
+ div.addEventListener('keydown', handleKeyDown);
49
+ return () => {
50
+ div.removeEventListener('focus', handleFocus);
51
+ div.removeEventListener('keydown', handleKeyDown);
52
+ };
53
+ }, [closeMenu]);
23
54
  return /*#__PURE__*/React.createElement("div", {
24
55
  "data-test": dataTest,
56
+ tabIndex: 0,
57
+ ref: divRef,
25
58
  className: _JSXStyle.dynamic([["3833750986", [colors.white, colors.grey200, elevations.e300, dense ? '128' : '180', maxWidth, maxHeight, spacers.dp4]]]) + " " + (className || "")
26
59
  }, /*#__PURE__*/React.createElement(Menu, {
27
60
  dense: dense
@@ -43,6 +76,9 @@ FlyoutMenu.propTypes = {
43
76
  /** Typically, but not limited to, `MenuItem` components */
44
77
  children: PropTypes.node,
45
78
  className: PropTypes.string,
79
+
80
+ /** when Escape key is pressed, this function is called to close the flyout menu */
81
+ closeMenu: PropTypes.func,
46
82
  dataTest: PropTypes.string,
47
83
 
48
84
  /** Menu uses smaller dimensions */
@@ -167,7 +167,9 @@ export const DropDownMenu = args => {
167
167
  }, /*#__PURE__*/React.createElement(Popper, {
168
168
  reference: ref,
169
169
  placement: "bottom-start"
170
- }, /*#__PURE__*/React.createElement(FlyoutMenu, args, /*#__PURE__*/React.createElement(MenuItem, {
170
+ }, /*#__PURE__*/React.createElement(FlyoutMenu, _extends({}, args, {
171
+ closeMenu: toggle
172
+ }), /*#__PURE__*/React.createElement(MenuItem, {
171
173
  label: "Item 1"
172
174
  }), /*#__PURE__*/React.createElement(MenuItem, {
173
175
  label: "Item 2"
@@ -56,7 +56,7 @@ export const useMenuNavigation = children => {
56
56
  break;
57
57
  }
58
58
  }
59
- }, [activeItemIndex, focusableItemsIndices]); // Event listeners for menu focus and key handling
59
+ }, [activeItemIndex, focusableItemsIndices === null || focusableItemsIndices === void 0 ? void 0 : focusableItemsIndices.length]); // Event listeners for menu focus and key handling
60
60
 
61
61
  useEffect(() => {
62
62
  if (!menuRef) {
@@ -4,7 +4,7 @@ import { Portal } from '@dhis2-ui/portal';
4
4
  import { IconChevronRight24 } from '@dhis2/ui-icons';
5
5
  import cx from 'classnames';
6
6
  import PropTypes from 'prop-types';
7
- import React, { useRef } from 'react';
7
+ import React, { useEffect, useRef, useState } from 'react';
8
8
  import { FlyoutMenu } from '../index.js';
9
9
  import styles from './menu-item.styles.js';
10
10
 
@@ -55,11 +55,56 @@ const MenuItem = _ref2 => {
55
55
  tabIndex
56
56
  } = _ref2;
57
57
  const menuItemRef = useRef();
58
+ const [openSubMenus, setOpenSubMenus] = useState([]);
59
+ useEffect(() => {
60
+ // track open submenus
61
+ setOpenSubMenus(document.querySelectorAll('[data-submenu-open=true]'));
62
+ }, []);
63
+ useEffect(() => {
64
+ if (!menuItemRef.current) {
65
+ return;
66
+ }
67
+
68
+ const menuItem = menuItemRef.current;
69
+
70
+ const handleKeyDown = event => {
71
+ var _openSubMenus, _openSubMenus2;
72
+
73
+ const firstChild = event.target.children[0];
74
+ const hasSubMenu = firstChild === null || firstChild === void 0 ? void 0 : firstChild.getAttribute('aria-haspopup');
75
+
76
+ switch (event.key) {
77
+ // for submenus
78
+ case 'ArrowRight':
79
+ event.preventDefault();
80
+
81
+ if (hasSubMenu) {
82
+ firstChild.click();
83
+ }
84
+
85
+ break;
86
+
87
+ case 'ArrowLeft':
88
+ case 'Escape':
89
+ // close flyout menu
90
+ event.preventDefault();
91
+ (_openSubMenus = openSubMenus[openSubMenus.length - 1]) === null || _openSubMenus === void 0 ? void 0 : _openSubMenus.focus();
92
+ (_openSubMenus2 = openSubMenus[openSubMenus.length - 1]) === null || _openSubMenus2 === void 0 ? void 0 : _openSubMenus2.children[0].click();
93
+ break;
94
+ }
95
+ };
96
+
97
+ menuItem.addEventListener('keydown', handleKeyDown);
98
+ return () => {
99
+ menuItem.removeEventListener('keydown', handleKeyDown);
100
+ };
101
+ }, [openSubMenus]);
58
102
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("li", {
59
103
  ref: menuItemRef,
60
104
  "data-test": dataTest,
61
105
  role: "presentation",
62
106
  tabIndex: tabIndex,
107
+ "data-submenu-open": children && showSubMenu,
63
108
  className: "jsx-".concat(styles.__hash) + " " + (cx(className, {
64
109
  destructive,
65
110
  disabled,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2-ui/menu",
3
- "version": "9.10.3",
3
+ "version": "9.11.0",
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.10.3",
37
- "@dhis2-ui/divider": "9.10.3",
38
- "@dhis2-ui/layer": "9.10.3",
39
- "@dhis2-ui/popper": "9.10.3",
40
- "@dhis2-ui/portal": "9.10.3",
41
- "@dhis2/ui-constants": "9.10.3",
42
- "@dhis2/ui-icons": "9.10.3",
36
+ "@dhis2-ui/card": "9.11.0",
37
+ "@dhis2-ui/divider": "9.11.0",
38
+ "@dhis2-ui/layer": "9.11.0",
39
+ "@dhis2-ui/popper": "9.11.0",
40
+ "@dhis2-ui/portal": "9.11.0",
41
+ "@dhis2/ui-constants": "9.11.0",
42
+ "@dhis2/ui-icons": "9.11.0",
43
43
  "classnames": "^2.3.1",
44
44
  "prop-types": "^15.7.2"
45
45
  },
package/types/index.d.ts CHANGED
@@ -6,6 +6,10 @@ export interface FlyoutMenuProps {
6
6
  */
7
7
  children?: React.ReactNode
8
8
  className?: string
9
+ /**
10
+ * On Escape key press, this function is called
11
+ */
12
+ closeMenu?: () => void
9
13
  dataTest?: string
10
14
  /**
11
15
  * Menu uses smaller dimensions