@dhis2-ui/menu 10.16.2 → 10.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +10 -9
  2. package/src/flyout-menu/__tests__/flyout-menu.test.js +48 -0
  3. package/src/flyout-menu/features/accepts_children/index.js +10 -0
  4. package/src/flyout-menu/features/accepts_children.feature +5 -0
  5. package/src/flyout-menu/features/position/index.js +80 -0
  6. package/src/flyout-menu/features/position.feature +19 -0
  7. package/src/flyout-menu/features/toggles_submenus/index.js +26 -0
  8. package/src/flyout-menu/features/toggles_submenus.feature +9 -0
  9. package/src/flyout-menu/flyout-menu.e2e.stories.js +55 -0
  10. package/src/flyout-menu/flyout-menu.js +108 -0
  11. package/src/flyout-menu/flyout-menu.prod.stories.js +229 -0
  12. package/src/flyout-menu/index.js +1 -0
  13. package/src/index.js +6 -0
  14. package/src/menu/__tests__/menu.test.js +213 -0
  15. package/src/menu/features/accepts_children/index.js +10 -0
  16. package/src/menu/features/accepts_children.feature +5 -0
  17. package/src/menu/helpers.js +36 -0
  18. package/src/menu/index.js +1 -0
  19. package/src/menu/menu.e2e.stories.js +5 -0
  20. package/src/menu/menu.js +96 -0
  21. package/src/menu/menu.prod.stories.js +90 -0
  22. package/src/menu/use-menu.js +100 -0
  23. package/src/menu-divider/index.js +1 -0
  24. package/src/menu-divider/menu-divider.js +32 -0
  25. package/src/menu-divider/menu-divider.prod.stories.js +30 -0
  26. package/src/menu-item/__tests__/menu-item.test.js +118 -0
  27. package/src/menu-item/features/accepts_href/index.js +11 -0
  28. package/src/menu-item/features/accepts_href.feature +5 -0
  29. package/src/menu-item/features/accepts_icon/index.js +10 -0
  30. package/src/menu-item/features/accepts_icon.feature +5 -0
  31. package/src/menu-item/features/accepts_label/index.js +10 -0
  32. package/src/menu-item/features/accepts_label.feature +5 -0
  33. package/src/menu-item/features/accepts_suffix/index.js +10 -0
  34. package/src/menu-item/features/accepts_suffix.feature +5 -0
  35. package/src/menu-item/features/accepts_target/index.js +11 -0
  36. package/src/menu-item/features/accepts_target.feature +5 -0
  37. package/src/menu-item/features/is_clickable/index.js +17 -0
  38. package/src/menu-item/features/is_clickable.feature +6 -0
  39. package/src/menu-item/index.js +1 -0
  40. package/src/menu-item/menu-item.e2e.stories.js +29 -0
  41. package/src/menu-item/menu-item.js +204 -0
  42. package/src/menu-item/menu-item.prod.stories.js +197 -0
  43. package/src/menu-item/menu-item.styles.js +133 -0
  44. package/src/menu-section-header/features/accepts_label/index.js +10 -0
  45. package/src/menu-section-header/features/accepts_label.feature +5 -0
  46. package/src/menu-section-header/index.js +1 -0
  47. package/src/menu-section-header/menu-section-header.e2e.stories.js +11 -0
  48. package/src/menu-section-header/menu-section-header.js +52 -0
  49. package/src/menu-section-header/menu-section-header.prod.stories.js +52 -0
@@ -0,0 +1,213 @@
1
+ import { Input } from '@dhis2-ui/input'
2
+ import { render } from '@testing-library/react'
3
+ import { userEvent } from '@testing-library/user-event'
4
+ import { mount } from 'enzyme'
5
+ import React from 'react'
6
+ import { MenuDivider } from '../../menu-divider/menu-divider.js'
7
+ import { MenuItem } from '../../menu-item/menu-item.js'
8
+ import { MenuSectionHeader } from '../../menu-section-header/menu-section-header.js'
9
+ import { Menu } from '../menu.js'
10
+
11
+ describe('Menu Component', () => {
12
+ const menuDataTest = 'data-test-menu'
13
+ const menuItemDataTest = 'data-test-menu-item'
14
+ const dividerDataTest = 'data-test-menu-divider'
15
+
16
+ it('Menu and menu items have aria attributes', () => {
17
+ const wrapper = mount(
18
+ <Menu dataTest={menuDataTest} dense={false}>
19
+ <MenuSectionHeader label="Header" />
20
+ <MenuDivider dataTest={dividerDataTest} />
21
+ <MenuItem dataTest={menuItemDataTest} label="Menu item" />
22
+ </Menu>
23
+ )
24
+ const menuElement = wrapper.find({ 'data-test': menuDataTest })
25
+ const menuItem = wrapper.find({ 'data-test': menuItemDataTest })
26
+ const menuDivider = wrapper.find({ 'data-test': dividerDataTest })
27
+
28
+ expect(menuElement.prop('role')).toBe('menu')
29
+
30
+ expect(menuItem.childAt(0).props().role).toBe('menuitem')
31
+ expect(menuItem.childAt(0).prop('aria-label')).toBe('Menu item')
32
+ expect(menuDivider.find('[role="separator"]').exists()).toBe(true)
33
+ })
34
+
35
+ it('Empty menu has role menu', () => {
36
+ const menuDataTest = 'data-test-menu'
37
+ const wrapper = mount(<Menu dataTest={menuDataTest} dense={false} />)
38
+ const menuElement = wrapper.find({ 'data-test': menuDataTest })
39
+ expect(menuElement.prop('role')).toBe('menu')
40
+ })
41
+
42
+ it('can handle focus of first focusable element when tabbed to', async () => {
43
+ const { getByRole, getByText } = render(
44
+ <Menu dataTest={menuDataTest} dense={false}>
45
+ <MenuSectionHeader label="Header" />
46
+ <MenuDivider dataTest={dividerDataTest} />
47
+ <MenuItem dataTest={menuItemDataTest} label="Menu item 1" />
48
+ <MenuItem dataTest={menuItemDataTest} label="Menu item 2" />
49
+ </Menu>
50
+ )
51
+
52
+ const menu = getByRole('menu')
53
+ const divider = getByRole('separator')
54
+ const header = getByText(/Header/i)
55
+ const menuItem1 = getByText(/Menu item 1/i)
56
+ const menuItem2 = getByText(/Menu item 2/i)
57
+
58
+ const user = userEvent.setup()
59
+ expect(menu).not.toHaveFocus()
60
+ await user.keyboard('{Tab}')
61
+ // check if LI parent node has focus or not
62
+ // headers and dividers do not receive focus
63
+ expect(header.parentNode.parentNode).not.toHaveFocus()
64
+ expect(divider.parentNode.parentNode).not.toHaveFocus()
65
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus()
66
+ expect(menuItem1.parentNode.parentNode).toHaveFocus()
67
+ })
68
+
69
+ it('can handle arrow down key navigation', async () => {
70
+ const { getByText } = render(
71
+ <Menu dataTest={menuDataTest} dense={false}>
72
+ <MenuSectionHeader label="Header" />
73
+ <MenuDivider dataTest={dividerDataTest} />
74
+ <MenuItem dataTest={menuItemDataTest} label="Menu item 1" />
75
+ <MenuItem dataTest={menuItemDataTest} label="Menu item 2" />
76
+ </Menu>
77
+ )
78
+
79
+ const menuItem1 = getByText(/Menu item 1/i)
80
+ const menuItem2 = getByText(/Menu item 2/i)
81
+
82
+ const user = userEvent.setup()
83
+ expect(document.body).toHaveFocus()
84
+
85
+ // await fireEvent.focus(getByRole('menu'))
86
+ await user.keyboard('{Tab}')
87
+ expect(menuItem1.parentNode.parentNode).toHaveFocus()
88
+ // simulate arrowDown press
89
+ await user.keyboard('{ArrowDown}')
90
+ expect(menuItem1.parentNode.parentNode).not.toHaveFocus()
91
+ expect(menuItem2.parentNode.parentNode).toHaveFocus()
92
+
93
+ await user.keyboard('{ArrowDown}')
94
+ expect(menuItem1.parentNode.parentNode).toHaveFocus()
95
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus()
96
+ })
97
+
98
+ it('can handle arrow up key navigation', async () => {
99
+ const { getByText } = render(
100
+ <Menu dataTest={menuDataTest} dense={false}>
101
+ <MenuSectionHeader label="Header" />
102
+ <MenuItem dataTest={menuItemDataTest} label="Menu item 1" />
103
+ <MenuDivider dataTest={dividerDataTest} />
104
+ <MenuItem dataTest={menuItemDataTest} label="Menu item 2" />
105
+ </Menu>
106
+ )
107
+
108
+ const menuItem1 = getByText(/Menu item 1/i)
109
+ const menuItem2 = getByText(/Menu item 2/i)
110
+
111
+ const user = userEvent.setup()
112
+ await user.tab()
113
+ expect(menuItem1.parentNode.parentNode).toHaveFocus()
114
+
115
+ // simulate arrowUp press
116
+ await user.keyboard('{ArrowUp}')
117
+ expect(menuItem1.parentNode.parentNode).not.toHaveFocus()
118
+ expect(menuItem2.parentNode.parentNode).toHaveFocus()
119
+
120
+ await user.keyboard('{ArrowUp}')
121
+ expect(menuItem1.parentNode.parentNode).toHaveFocus()
122
+ expect(menuItem2.parentNode.parentNode).not.toHaveFocus()
123
+ })
124
+
125
+ it('can handle space and enter key presses for clickable menu items', async () => {
126
+ const onClick = jest.fn()
127
+ const { getByText } = render(
128
+ <Menu dataTest={menuDataTest} dense={false}>
129
+ <MenuItem
130
+ onClick={onClick}
131
+ value="myValue"
132
+ label="Click menu item"
133
+ />
134
+ </Menu>
135
+ )
136
+
137
+ const clickableItem = getByText(/Click menu item/i)
138
+
139
+ const user = userEvent.setup()
140
+ await user.tab()
141
+ expect(clickableItem.parentNode.parentNode).toHaveFocus()
142
+
143
+ await user.keyboard('[Space]')
144
+ expect(onClick).toHaveBeenCalledTimes(1)
145
+
146
+ await user.keyboard('{Enter}')
147
+ expect(onClick).toHaveBeenCalledTimes(2)
148
+ })
149
+
150
+ it('can handle non MenuItem components', async () => {
151
+ const onClick = jest.fn()
152
+ const { getByText } = render(
153
+ <Menu dataTest={menuDataTest} dense={false}>
154
+ <span role="menuitem" onClick={onClick}>
155
+ Span 1
156
+ </span>
157
+ <li tabIndex={-1}>
158
+ <a href="#" role="menuitem">
159
+ Link 2
160
+ </a>
161
+ </li>
162
+ <li>
163
+ <span onClick={onClick}>Span 2</span>
164
+ </li>
165
+ </Menu>
166
+ )
167
+
168
+ const nonListMenuItem = getByText(/span 1/i)
169
+ const listMenuItem = getByText(/link 2/i)
170
+ const plainListItem = getByText(/span 2/i)
171
+
172
+ // all children must be list items
173
+ expect(nonListMenuItem.parentElement.nodeName).toBe('LI')
174
+
175
+ const user = userEvent.setup()
176
+ await user.tab()
177
+ expect(nonListMenuItem.parentElement).toHaveFocus()
178
+ expect(nonListMenuItem.parentElement.tabIndex).toBe(0)
179
+
180
+ expect(onClick).toHaveBeenCalledTimes(0)
181
+ await user.keyboard(' ')
182
+ expect(onClick).toHaveBeenCalledTimes(1)
183
+
184
+ await user.keyboard('{ArrowDown}')
185
+ expect(listMenuItem.parentElement).toHaveFocus()
186
+
187
+ await user.keyboard('{ArrowDown}')
188
+ expect(nonListMenuItem.parentElement).toHaveFocus()
189
+ // non menu items do not receive focus
190
+ expect(plainListItem.parentElement).not.toHaveFocus()
191
+ })
192
+
193
+ it('does not hijack input change value if space entered [bug]', async () => {
194
+ const onChange = jest.fn()
195
+ const { getByPlaceholderText } = render(
196
+ <Menu dataTest={menuDataTest} dense={false}>
197
+ <MenuItem value="myValue" label="Click menu item" />
198
+ <Input onChange={onChange} placeholder="test"></Input>
199
+ </Menu>
200
+ )
201
+
202
+ const inputField = getByPlaceholderText('test')
203
+ const user = userEvent.setup()
204
+ await inputField.focus()
205
+ await user.type(inputField, 't')
206
+ await user.type(inputField, 'e')
207
+ await user.type(inputField, ' ')
208
+ await user.type(inputField, 'st')
209
+
210
+ expect(inputField.value).toBe('te st')
211
+ expect(onChange).toHaveBeenCalled()
212
+ })
213
+ })
@@ -0,0 +1,10 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Menu with children is rendered', () => {
4
+ cy.visitStory('Menu', 'With children')
5
+ cy.get('[data-test="dhis2-uicore-menulist"]').should('be.visible')
6
+ })
7
+
8
+ Then('the children are visible', () => {
9
+ cy.contains('I am a child').should('be.visible')
10
+ })
@@ -0,0 +1,5 @@
1
+ Feature: The Menu renders children
2
+
3
+ Scenario: A Menu with children
4
+ Given a Menu with children is rendered
5
+ Then the children are visible
@@ -0,0 +1,36 @@
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
+ const role = node.getAttribute('role')
10
+
11
+ // for h1 - h6 headings since their heading role is not explicitly set
12
+ // style elements do not have roles
13
+ if (node.nodeName.startsWith('H') || node.nodeName === 'STYLE') {
14
+ return false
15
+ }
16
+
17
+ if (role) {
18
+ return isMenuItem(role)
19
+ } else {
20
+ console.warn('Missing: role attribute on the menu child')
21
+ }
22
+ }
23
+
24
+ export const getFocusableItemsIndices = (elements) => {
25
+ const focusableIndices = []
26
+ elements.forEach((node, index) => {
27
+ if (isValidMenuItemNode(node)) {
28
+ focusableIndices.push(index)
29
+ }
30
+ })
31
+ return focusableIndices
32
+ }
33
+
34
+ export const hasMenuItemRole = (component) => {
35
+ return isMenuItem(component?.props?.['role'])
36
+ }
@@ -0,0 +1 @@
1
+ export { Menu } from './menu.js'
@@ -0,0 +1,5 @@
1
+ import React from 'react'
2
+ import { Menu } from './index.js'
3
+
4
+ export default { title: 'Menu' }
5
+ export const WithChildren = () => <Menu>I am a child</Menu>
@@ -0,0 +1,96 @@
1
+ import PropTypes from 'prop-types'
2
+ import React, { Children, cloneElement, isValidElement, useMemo } from 'react'
3
+ import { hasMenuItemRole } from './helpers.js'
4
+ import { useMenuNavigation } from './use-menu.js'
5
+
6
+ const Menu = ({
7
+ children,
8
+ className,
9
+ dataTest = 'dhis2-uicore-menulist',
10
+ dense,
11
+ }) => {
12
+ const { menuRef, focusedIndex } = useMenuNavigation(children)
13
+
14
+ const childrenToRender = useMemo(
15
+ () =>
16
+ Children.map(children, (child, index) => {
17
+ if (!isValidElement(child)) {
18
+ return child
19
+ }
20
+ const tabIndex = index === focusedIndex ? 0 : -1
21
+
22
+ const childProps = {
23
+ ...child.props,
24
+ }
25
+
26
+ // this check is based on the type of child.
27
+ // if it is a native HTML element, like li, a, span, only apply its child props
28
+ // if it is a functional (React) component, it applies custom props, like dense, hideDivider, etc
29
+ if (typeof child.type === 'string') {
30
+ // if the native HTML element child is not li, then wrap it in an li tag
31
+ // apply the tabindex prop if a child has the menuitem role to make it focusable
32
+ if (child.type === 'li') {
33
+ return hasMenuItemRole(child.props.children[0])
34
+ ? cloneElement(child, { ...childProps, tabIndex })
35
+ : cloneElement(child, childProps)
36
+ } else {
37
+ return (
38
+ <li
39
+ tabIndex={
40
+ hasMenuItemRole(child) ? tabIndex : null
41
+ }
42
+ >
43
+ {cloneElement(child, childProps)}
44
+ </li>
45
+ )
46
+ }
47
+ } else {
48
+ childProps.dense =
49
+ typeof child.props.dense === 'boolean'
50
+ ? child.props.dense
51
+ : dense
52
+ childProps.hideDivider =
53
+ typeof child.props.hideDivider !== 'boolean' &&
54
+ index === 0
55
+ ? true
56
+ : child.props.hideDivider
57
+ return cloneElement(child, { ...childProps, tabIndex })
58
+ }
59
+ }),
60
+ [children, dense, focusedIndex]
61
+ )
62
+
63
+ return (
64
+ <ul
65
+ className={className}
66
+ data-test={dataTest}
67
+ role="menu"
68
+ ref={menuRef}
69
+ tabIndex={0}
70
+ >
71
+ {childrenToRender}
72
+ <style jsx>{`
73
+ ul {
74
+ display: block;
75
+ position: relative;
76
+ width: 100%;
77
+ margin: 0;
78
+
79
+ padding: 0;
80
+ user-select: none;
81
+ }
82
+ `}</style>
83
+ </ul>
84
+ )
85
+ }
86
+
87
+ Menu.propTypes = {
88
+ /** Typically `MenuItem`, `MenuDivider`, and `MenuSectionHeader` */
89
+ children: PropTypes.node,
90
+ className: PropTypes.string,
91
+ dataTest: PropTypes.string,
92
+ /** Applies `dense` property to all child components unless already specified */
93
+ dense: PropTypes.bool,
94
+ }
95
+
96
+ export { Menu }
@@ -0,0 +1,90 @@
1
+ import React from 'react'
2
+ import { MenuItem, MenuSectionHeader } from '../index.js'
3
+ import { Menu } from './index.js'
4
+
5
+ const description = `
6
+ 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.
7
+
8
+ 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.
9
+
10
+ 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:
11
+
12
+ - "Save"
13
+ - "Open as map"
14
+ - "Export PDF"
15
+ - "Duplicate"
16
+
17
+ Typical children are Menu Items, Menu Dividers, and Menu Section Headers.
18
+
19
+ \`\`\`js
20
+ import { Menu } from '@dhis2/ui'
21
+ \`\`\`
22
+ `
23
+
24
+ export default {
25
+ title: 'Menu',
26
+ component: Menu,
27
+ parameters: { docs: { description: { component: description } } },
28
+ }
29
+
30
+ export const Default = (args) => (
31
+ <Menu {...args}>
32
+ <MenuItem label="Menu item" />
33
+ <MenuItem label="Menu item" />
34
+ </Menu>
35
+ )
36
+
37
+ export const Dense = (args) => (
38
+ <Menu {...args}>
39
+ <MenuItem label="Menu item" />
40
+ <MenuItem label="Menu item" />
41
+ </Menu>
42
+ )
43
+ Dense.args = { dense: true }
44
+ Dense.parameters = {
45
+ docs: {
46
+ description: {
47
+ story: 'Menus are available in regular or dense sizes. Use dense menus in data-heavy applications used by users comfortable with technology. Use regular menus in apps that are less complex or have few controls.',
48
+ },
49
+ },
50
+ }
51
+
52
+ export const AutoHideFirstSectionHeaderDivider = (args) => (
53
+ <Menu {...args}>
54
+ <MenuSectionHeader label="First - no divider above" />
55
+ <MenuSectionHeader label="Second - with divider above" />
56
+ </Menu>
57
+ )
58
+
59
+ export const SideBarMenu = (args) => (
60
+ <main
61
+ style={{
62
+ display: 'flex',
63
+ height: '100%',
64
+ border: '1px solid grey',
65
+ }}
66
+ >
67
+ <aside style={{ width: 200, height: '100%', flexGrow: 0 }}>
68
+ <Menu {...args}>
69
+ <MenuItem label="Menu item" />
70
+ <MenuItem label="Menu item" />
71
+ </Menu>
72
+ </aside>
73
+ <section
74
+ style={{
75
+ backgroundColor: '#f3ffff',
76
+ flexGrow: 1,
77
+ padding: 20,
78
+ borderInlineStart: '1px solid grey',
79
+ }}
80
+ >
81
+ Main content
82
+ </section>
83
+ </main>
84
+ )
85
+
86
+ export const SideBarMenuRTL = (args) => (
87
+ <div dir="rtl" style={{ height: '100%' }}>
88
+ <SideBarMenu {...args} />
89
+ </div>
90
+ )
@@ -0,0 +1,100 @@
1
+ import { useRef, useState, useEffect, useCallback } from 'react'
2
+ import { getFocusableItemsIndices } from './helpers.js'
3
+
4
+ export const useMenuNavigation = (children) => {
5
+ const menuRef = useRef(null)
6
+ const [focusableItemsIndices, setFocusableItemsIndices] = useState(null)
7
+ const [activeItemIndex, setActiveItemIndex] = useState(-1)
8
+
9
+ // Initializes the indices for focusable items
10
+ // focusable items have the role of menuitem || menuitemcheckbox || menuitemradio
11
+ useEffect(() => {
12
+ if (menuRef) {
13
+ const menuItems = Array.from(menuRef.current.children)
14
+ const itemsIndices = getFocusableItemsIndices(menuItems)
15
+ setFocusableItemsIndices(itemsIndices)
16
+ }
17
+ }, [children])
18
+
19
+ // Focus the active menu child
20
+ useEffect(() => {
21
+ if (menuRef) {
22
+ if (focusableItemsIndices?.length && activeItemIndex > -1) {
23
+ const currentIndex = focusableItemsIndices[activeItemIndex]
24
+ menuRef.current.children[currentIndex].focus()
25
+ }
26
+ }
27
+ }, [activeItemIndex, focusableItemsIndices])
28
+
29
+ // Navigate through focusable children using arrow keys
30
+ // Trigger actionable items
31
+ const handleKeyDown = useCallback(
32
+ (event) => {
33
+ const totalFocusablePositions = focusableItemsIndices?.length
34
+ if (totalFocusablePositions) {
35
+ const lastIndex = totalFocusablePositions - 1
36
+
37
+ switch (event.key) {
38
+ case 'ArrowUp':
39
+ event.preventDefault()
40
+ setActiveItemIndex(
41
+ activeItemIndex > 0
42
+ ? activeItemIndex - 1
43
+ : lastIndex
44
+ )
45
+ break
46
+ case 'ArrowDown':
47
+ event.preventDefault()
48
+ setActiveItemIndex(
49
+ activeItemIndex >= lastIndex
50
+ ? 0
51
+ : activeItemIndex + 1
52
+ )
53
+ break
54
+ case 'Enter':
55
+ case ' ':
56
+ if (event.target.nodeName === 'LI') {
57
+ event.preventDefault()
58
+ event.target.children?.[0]?.click()
59
+ }
60
+ break
61
+ default:
62
+ break
63
+ }
64
+ }
65
+ },
66
+ [activeItemIndex, focusableItemsIndices?.length]
67
+ )
68
+
69
+ // Event listeners for menu focus and key handling
70
+ useEffect(() => {
71
+ if (!menuRef) {
72
+ return
73
+ }
74
+
75
+ const menu = menuRef.current
76
+
77
+ // Focus the first menu item when the menu receives focus
78
+ const handleFocus = (event) => {
79
+ if (event.target === menuRef.current) {
80
+ const firstItemIndex = focusableItemsIndices?.[0]
81
+ firstItemIndex &&
82
+ menuRef.current.children[firstItemIndex].focus()
83
+ setActiveItemIndex(0)
84
+ }
85
+ }
86
+
87
+ menu.addEventListener('focus', handleFocus)
88
+ menu.addEventListener('keydown', handleKeyDown)
89
+
90
+ return () => {
91
+ menu.removeEventListener('focus', handleFocus)
92
+ menu.removeEventListener('keydown', handleKeyDown)
93
+ }
94
+ }, [activeItemIndex, focusableItemsIndices, handleKeyDown])
95
+
96
+ return {
97
+ menuRef,
98
+ focusedIndex: focusableItemsIndices?.[activeItemIndex],
99
+ }
100
+ }
@@ -0,0 +1 @@
1
+ export { MenuDivider } from './menu-divider.js'
@@ -0,0 +1,32 @@
1
+ import { colors } from '@dhis2/ui-constants'
2
+ import { Divider } from '@dhis2-ui/divider'
3
+ import PropTypes from 'prop-types'
4
+ import React from 'react'
5
+
6
+ const MenuDivider = ({
7
+ className,
8
+ dataTest = 'dhis2-uicore-menudivider',
9
+ dense,
10
+ }) => (
11
+ <li className={className} data-test={dataTest}>
12
+ <Divider dense={dense} />
13
+
14
+ <style jsx>{`
15
+ li {
16
+ list-style: none;
17
+ background-color: ${colors.white};
18
+ user-select: none;
19
+ padding: 0;
20
+ line-height: 0;
21
+ }
22
+ `}</style>
23
+ </li>
24
+ )
25
+
26
+ MenuDivider.propTypes = {
27
+ className: PropTypes.string,
28
+ dataTest: PropTypes.string,
29
+ dense: PropTypes.bool,
30
+ }
31
+
32
+ export { MenuDivider }
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+ import { Menu, MenuItem } from '../index.js'
3
+ import { MenuDivider } from './menu-divider.js'
4
+
5
+ const description = `
6
+ 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.
7
+
8
+ \`\`\`js
9
+ import { MenuDivider } from '@dhis2/ui'
10
+ \`\`\`
11
+ `
12
+
13
+ export default {
14
+ title: 'Menu Divider',
15
+ component: MenuDivider,
16
+ parameters: { docs: { description: { component: description } } },
17
+ }
18
+
19
+ const Template = (args) => (
20
+ <Menu>
21
+ <MenuItem label="Item above divider" />
22
+ <MenuDivider {...args} />
23
+ <MenuItem label="Item below divider" />
24
+ </Menu>
25
+ )
26
+
27
+ export const Default = Template.bind({})
28
+
29
+ export const Dense = Template.bind({})
30
+ Dense.args = { dense: true }