@dhis2-ui/menu 10.16.2 → 10.16.3-alpha.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/package.json +10 -9
- package/src/flyout-menu/__tests__/flyout-menu.test.js +48 -0
- package/src/flyout-menu/features/accepts_children/index.js +10 -0
- package/src/flyout-menu/features/accepts_children.feature +5 -0
- package/src/flyout-menu/features/position/index.js +80 -0
- package/src/flyout-menu/features/position.feature +19 -0
- package/src/flyout-menu/features/toggles_submenus/index.js +26 -0
- package/src/flyout-menu/features/toggles_submenus.feature +9 -0
- package/src/flyout-menu/flyout-menu.e2e.stories.js +55 -0
- package/src/flyout-menu/flyout-menu.js +108 -0
- package/src/flyout-menu/flyout-menu.prod.stories.js +229 -0
- package/src/flyout-menu/index.js +1 -0
- package/src/index.js +6 -0
- package/src/menu/__tests__/menu.test.js +213 -0
- package/src/menu/features/accepts_children/index.js +10 -0
- package/src/menu/features/accepts_children.feature +5 -0
- package/src/menu/helpers.js +36 -0
- package/src/menu/index.js +1 -0
- package/src/menu/menu.e2e.stories.js +5 -0
- package/src/menu/menu.js +96 -0
- package/src/menu/menu.prod.stories.js +90 -0
- package/src/menu/use-menu.js +100 -0
- package/src/menu-divider/index.js +1 -0
- package/src/menu-divider/menu-divider.js +32 -0
- package/src/menu-divider/menu-divider.prod.stories.js +30 -0
- package/src/menu-item/__tests__/menu-item.test.js +118 -0
- package/src/menu-item/features/accepts_href/index.js +11 -0
- package/src/menu-item/features/accepts_href.feature +5 -0
- package/src/menu-item/features/accepts_icon/index.js +10 -0
- package/src/menu-item/features/accepts_icon.feature +5 -0
- package/src/menu-item/features/accepts_label/index.js +10 -0
- package/src/menu-item/features/accepts_label.feature +5 -0
- package/src/menu-item/features/accepts_suffix/index.js +10 -0
- package/src/menu-item/features/accepts_suffix.feature +5 -0
- package/src/menu-item/features/accepts_target/index.js +11 -0
- package/src/menu-item/features/accepts_target.feature +5 -0
- package/src/menu-item/features/is_clickable/index.js +17 -0
- package/src/menu-item/features/is_clickable.feature +6 -0
- package/src/menu-item/index.js +1 -0
- package/src/menu-item/menu-item.e2e.stories.js +29 -0
- package/src/menu-item/menu-item.js +204 -0
- package/src/menu-item/menu-item.prod.stories.js +197 -0
- package/src/menu-item/menu-item.styles.js +133 -0
- package/src/menu-section-header/features/accepts_label/index.js +10 -0
- package/src/menu-section-header/features/accepts_label.feature +5 -0
- package/src/menu-section-header/index.js +1 -0
- package/src/menu-section-header/menu-section-header.e2e.stories.js +11 -0
- package/src/menu-section-header/menu-section-header.js +52 -0
- package/src/menu-section-header/menu-section-header.prod.stories.js +52 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mount } from 'enzyme'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { MenuItem } from '../menu-item.js'
|
|
4
|
+
|
|
5
|
+
describe('Menu Component', () => {
|
|
6
|
+
it('Default menu item has role', () => {
|
|
7
|
+
const menuItemDataTest = 'data-test-menu-item'
|
|
8
|
+
const wrapper = mount(
|
|
9
|
+
<MenuItem dataTest={menuItemDataTest} label="Menu item" />
|
|
10
|
+
)
|
|
11
|
+
const menuItem = wrapper.find({ 'data-test': menuItemDataTest })
|
|
12
|
+
expect(menuItem.childAt(0).prop('role')).toBe('menuitem')
|
|
13
|
+
expect(menuItem.childAt(0).prop('aria-disabled')).toBe(undefined)
|
|
14
|
+
expect(menuItem.childAt(0).prop('aria-label')).toBe('Menu item')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('Disabled menu item has aria-disabled attribute', () => {
|
|
18
|
+
const menuItemDataTest = 'data-test-menu-item'
|
|
19
|
+
const wrapper = mount(
|
|
20
|
+
<MenuItem
|
|
21
|
+
dataTest={menuItemDataTest}
|
|
22
|
+
label="Disabled menu item"
|
|
23
|
+
disabled={true}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
const menuItem = wrapper.find({ 'data-test': menuItemDataTest })
|
|
27
|
+
|
|
28
|
+
expect(menuItem.childAt(0).prop('role')).toBe('menuitem')
|
|
29
|
+
expect(menuItem.childAt(0).prop('aria-disabled')).toBe(true)
|
|
30
|
+
expect(menuItem.childAt(0).prop('aria-label')).toBe(
|
|
31
|
+
'Disabled menu item'
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('Toggle-able menu item has menuitemcheckbox role', () => {
|
|
36
|
+
const menuItemDataTest = 'data-test-menu-item'
|
|
37
|
+
const wrapper = mount(
|
|
38
|
+
<MenuItem
|
|
39
|
+
dataTest={menuItemDataTest}
|
|
40
|
+
label="Toggle-able menu item"
|
|
41
|
+
checkbox
|
|
42
|
+
checked={false}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
const menuItem = wrapper.find({ 'data-test': menuItemDataTest })
|
|
46
|
+
|
|
47
|
+
expect(menuItem.childAt(0).prop('role')).not.toBe('menuitem')
|
|
48
|
+
expect(menuItem.childAt(0).prop('role')).toBe('menuitemcheckbox')
|
|
49
|
+
expect(menuItem.childAt(0).prop('aria-checked')).toBe(false)
|
|
50
|
+
expect(menuItem.childAt(0).prop('aria-label')).toBe(
|
|
51
|
+
'Toggle-able menu item'
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('Submenu has relevant aria attributes', () => {
|
|
56
|
+
const showSubMenu = false
|
|
57
|
+
const wrapper = mount(
|
|
58
|
+
<MenuItem
|
|
59
|
+
showSubMenu={showSubMenu}
|
|
60
|
+
toggleSubMenu={() => jest.fn()}
|
|
61
|
+
label="Parent of submenus"
|
|
62
|
+
>
|
|
63
|
+
<MenuItem label="Test submenu child" />
|
|
64
|
+
</MenuItem>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const menuItem = wrapper.find({ role: 'menuitem' })
|
|
68
|
+
|
|
69
|
+
expect(menuItem.prop('aria-haspopup')).toBe('menu')
|
|
70
|
+
expect(menuItem.prop('aria-expanded')).toBe(false)
|
|
71
|
+
expect(menuItem.prop('aria-label')).toBe('Parent of submenus')
|
|
72
|
+
expect(wrapper.find({ label: 'Test submenu child' }).exists()).toBe(
|
|
73
|
+
false
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('aria-label attribute', () => {
|
|
78
|
+
it('produces a log if the label prop is not a string', () => {
|
|
79
|
+
// Prep to test logs
|
|
80
|
+
const originalConsoleDebug = console.debug
|
|
81
|
+
const consoleDebugMock = jest.fn()
|
|
82
|
+
console.debug = consoleDebugMock
|
|
83
|
+
|
|
84
|
+
const menuItemDataTest = 'data-test-menu-item'
|
|
85
|
+
const wrapper = mount(
|
|
86
|
+
<MenuItem
|
|
87
|
+
dataTest={menuItemDataTest}
|
|
88
|
+
label={<span>{'Node label'}</span>}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
const menuItem = wrapper.find({ 'data-test': menuItemDataTest })
|
|
92
|
+
|
|
93
|
+
expect(menuItem.childAt(0).prop('role')).toBe('menuitem')
|
|
94
|
+
expect(menuItem.childAt(0).prop('aria-label')).toBe(undefined)
|
|
95
|
+
expect(consoleDebugMock).toHaveBeenCalledWith(
|
|
96
|
+
'The label prop on MenuItem is not a string; a value for the ariaLabel prop should be provided'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// Teardown
|
|
100
|
+
console.debug = originalConsoleDebug
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('uses the ariaLabel prop for aria-label if defined', () => {
|
|
104
|
+
const menuItemDataTest = 'data-test-menu-item'
|
|
105
|
+
const wrapper = mount(
|
|
106
|
+
<MenuItem
|
|
107
|
+
dataTest={menuItemDataTest}
|
|
108
|
+
label={<span>{'Node label'}</span>}
|
|
109
|
+
ariaLabel="Aria label"
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
const menuItem = wrapper.find({ 'data-test': menuItemDataTest })
|
|
113
|
+
|
|
114
|
+
expect(menuItem.childAt(0).prop('role')).toBe('menuitem')
|
|
115
|
+
expect(menuItem.childAt(0).prop('aria-label')).toBe('Aria label')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a MenuItem with href is rendered', () => {
|
|
4
|
+
cy.visitStory('MenuItem', 'With Href')
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
Then('a link is rendered with the href', () => {
|
|
8
|
+
cy.get('#storybook-root a')
|
|
9
|
+
.should('have.attr', 'href')
|
|
10
|
+
.and('include', 'url.test')
|
|
11
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a MenuItem supplied with an icon is rendered', () => {
|
|
4
|
+
cy.visitStory('MenuItem', 'With Icon')
|
|
5
|
+
cy.get('[data-test="dhis2-uicore-menuitem"]').should('be.visible')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
Then('the icon will be visible', () => {
|
|
9
|
+
cy.contains('Icon').should('be.visible')
|
|
10
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a MenuItem supplied with a label is rendered', () => {
|
|
4
|
+
cy.visitStory('MenuItem', 'With Label')
|
|
5
|
+
cy.get('[data-test="dhis2-uicore-menuitem"]').should('be.visible')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
Then('the label is visible', () => {
|
|
9
|
+
cy.get(':contains("label")').should('be.visible')
|
|
10
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a MenuItem supplied with a suffix is rendered', () => {
|
|
4
|
+
cy.visitStory('MenuItem', 'With Suffix')
|
|
5
|
+
cy.get('[data-test="dhis2-uicore-menuitem"]').should('be.visible')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
Then('the suffix will be visible', () => {
|
|
9
|
+
cy.contains('Suffix').should('be.visible')
|
|
10
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a MenuItem with target is rendered', () => {
|
|
4
|
+
cy.visitStory('MenuItem', 'With Target')
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
Then('a link is rendered with the target', () => {
|
|
8
|
+
cy.get('#storybook-root a')
|
|
9
|
+
.should('have.attr', 'target')
|
|
10
|
+
.and('include', '_blank')
|
|
11
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a MenuItem with onClick handler and value is rendered', () => {
|
|
4
|
+
cy.visitStory('MenuItem', 'With On Click And Value')
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
When('the MenuItem is clicked', () => {
|
|
8
|
+
cy.get('[data-test="dhis2-uicore-menuitem"]').click()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
Then('the onClick handler is called with value', () => {
|
|
12
|
+
cy.window().should((win) => {
|
|
13
|
+
expect(win.onClick).to.be.calledWith({
|
|
14
|
+
value: 'Value',
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MenuItem } from './menu-item.js'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Menu } from '../index.js'
|
|
3
|
+
import { MenuItem } from './menu-item.js'
|
|
4
|
+
|
|
5
|
+
window.onClick = window.Cypress && window.Cypress.cy.stub()
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'MenuItem',
|
|
9
|
+
component: MenuItem,
|
|
10
|
+
decorators: [(story) => <Menu>{story()}</Menu>],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const WithLabel = () => <MenuItem label="label" />
|
|
14
|
+
|
|
15
|
+
export const WithOnClickAndValue = () => (
|
|
16
|
+
<MenuItem value="Value" onClick={window.onClick} label="Menu item" />
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export const WithHref = () => <MenuItem href="url.test" label="Menu item" />
|
|
20
|
+
|
|
21
|
+
export const WithTarget = () => (
|
|
22
|
+
<MenuItem href="url.test" target="_blank" label="Menu item" />
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export const WithIcon = () => (
|
|
26
|
+
<MenuItem icon={<div>Icon</div>} label="Menu item" />
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
export const WithSuffix = () => <MenuItem suffix="Suffix" label="Menu item" />
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { IconChevronRight24 } from '@dhis2/ui-icons'
|
|
2
|
+
import { Popper } from '@dhis2-ui/popper'
|
|
3
|
+
import { Portal } from '@dhis2-ui/portal'
|
|
4
|
+
import cx from 'classnames'
|
|
5
|
+
import PropTypes from 'prop-types'
|
|
6
|
+
import React, { useEffect, useRef, useState, useMemo } from 'react'
|
|
7
|
+
import { FlyoutMenu } from '../flyout-menu/index.js'
|
|
8
|
+
import styles from './menu-item.styles.js'
|
|
9
|
+
|
|
10
|
+
const isModifiedEvent = (evt) =>
|
|
11
|
+
evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey
|
|
12
|
+
|
|
13
|
+
const createOnClickHandler =
|
|
14
|
+
({ onClick, toggleSubMenu, isLink, value }) =>
|
|
15
|
+
(evt) => {
|
|
16
|
+
if ((isLink && isModifiedEvent(evt)) || !(onClick || toggleSubMenu)) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
evt.preventDefault()
|
|
20
|
+
evt.stopPropagation()
|
|
21
|
+
|
|
22
|
+
onClick && onClick({ value }, evt)
|
|
23
|
+
toggleSubMenu && toggleSubMenu()
|
|
24
|
+
}
|
|
25
|
+
const MenuItem = ({
|
|
26
|
+
href,
|
|
27
|
+
onClick,
|
|
28
|
+
children,
|
|
29
|
+
target,
|
|
30
|
+
icon,
|
|
31
|
+
className,
|
|
32
|
+
destructive,
|
|
33
|
+
disabled,
|
|
34
|
+
dense,
|
|
35
|
+
active,
|
|
36
|
+
dataTest = 'dhis2-uicore-menuitem',
|
|
37
|
+
chevron,
|
|
38
|
+
value,
|
|
39
|
+
label,
|
|
40
|
+
ariaLabel,
|
|
41
|
+
showSubMenu,
|
|
42
|
+
toggleSubMenu,
|
|
43
|
+
suffix,
|
|
44
|
+
checkbox,
|
|
45
|
+
checked,
|
|
46
|
+
tabIndex,
|
|
47
|
+
}) => {
|
|
48
|
+
const menuItemRef = useRef()
|
|
49
|
+
const [openSubMenus, setOpenSubMenus] = useState([])
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// track open submenus
|
|
53
|
+
setOpenSubMenus(document.querySelectorAll('[data-submenu-open=true]'))
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!menuItemRef.current) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const menuItem = menuItemRef.current
|
|
62
|
+
|
|
63
|
+
const handleKeyDown = (event) => {
|
|
64
|
+
const firstChild = event.target.children[0]
|
|
65
|
+
const hasSubMenu = firstChild?.getAttribute('aria-haspopup')
|
|
66
|
+
switch (event.key) {
|
|
67
|
+
// for submenus
|
|
68
|
+
case 'ArrowRight':
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
if (hasSubMenu) {
|
|
71
|
+
firstChild.click()
|
|
72
|
+
}
|
|
73
|
+
break
|
|
74
|
+
case 'ArrowLeft':
|
|
75
|
+
case 'Escape': // close flyout menu
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
openSubMenus[openSubMenus.length - 1]?.focus()
|
|
78
|
+
openSubMenus[openSubMenus.length - 1]?.children[0].click()
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
menuItem.addEventListener('keydown', handleKeyDown)
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
menuItem.removeEventListener('keydown', handleKeyDown)
|
|
87
|
+
}
|
|
88
|
+
}, [openSubMenus])
|
|
89
|
+
|
|
90
|
+
const resolvedAriaLabel = useMemo(() => {
|
|
91
|
+
if (typeof label !== 'string' && ariaLabel === undefined) {
|
|
92
|
+
console.debug(
|
|
93
|
+
'The label prop on MenuItem is not a string; a value for the ariaLabel prop should be provided'
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return ariaLabel ?? (typeof label === 'string' ? label : undefined)
|
|
98
|
+
}, [ariaLabel, label])
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<li
|
|
103
|
+
className={cx(className, {
|
|
104
|
+
destructive,
|
|
105
|
+
disabled,
|
|
106
|
+
dense,
|
|
107
|
+
active: active || showSubMenu,
|
|
108
|
+
'with-chevron': children || chevron,
|
|
109
|
+
})}
|
|
110
|
+
ref={menuItemRef}
|
|
111
|
+
data-test={dataTest}
|
|
112
|
+
role="presentation"
|
|
113
|
+
tabIndex={tabIndex}
|
|
114
|
+
data-submenu-open={children && showSubMenu}
|
|
115
|
+
>
|
|
116
|
+
<a
|
|
117
|
+
target={target}
|
|
118
|
+
href={!disabled && href ? href : undefined}
|
|
119
|
+
onClick={
|
|
120
|
+
!disabled
|
|
121
|
+
? createOnClickHandler({
|
|
122
|
+
onClick,
|
|
123
|
+
toggleSubMenu,
|
|
124
|
+
isLink: !!href,
|
|
125
|
+
value,
|
|
126
|
+
})
|
|
127
|
+
: undefined
|
|
128
|
+
}
|
|
129
|
+
role={checkbox ? 'menuitemcheckbox' : 'menuitem'}
|
|
130
|
+
aria-checked={checkbox ? checked : null}
|
|
131
|
+
aria-disabled={disabled}
|
|
132
|
+
aria-haspopup={children && 'menu'}
|
|
133
|
+
aria-expanded={showSubMenu}
|
|
134
|
+
aria-label={resolvedAriaLabel}
|
|
135
|
+
>
|
|
136
|
+
{icon && <span className="icon">{icon}</span>}
|
|
137
|
+
|
|
138
|
+
<span className="label">{label}</span>
|
|
139
|
+
|
|
140
|
+
{suffix && <span className="suffix">{suffix}</span>}
|
|
141
|
+
|
|
142
|
+
{(chevron || children) && (
|
|
143
|
+
<span className="chevron">
|
|
144
|
+
<IconChevronRight24 />
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
</a>
|
|
148
|
+
|
|
149
|
+
<style jsx>{styles}</style>
|
|
150
|
+
</li>
|
|
151
|
+
{children && showSubMenu && (
|
|
152
|
+
<Portal>
|
|
153
|
+
<Popper placement="right-start" reference={menuItemRef}>
|
|
154
|
+
<FlyoutMenu dense={dense}>{children}</FlyoutMenu>
|
|
155
|
+
</Popper>
|
|
156
|
+
</Portal>
|
|
157
|
+
)}
|
|
158
|
+
</>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
MenuItem.propTypes = {
|
|
163
|
+
active: PropTypes.bool,
|
|
164
|
+
/**
|
|
165
|
+
* By default, the label prop is used for the aria-label attribute on the
|
|
166
|
+
* underlying HTML element. If this prop is defined, it will be used as the
|
|
167
|
+
* aria-label instead
|
|
168
|
+
*/
|
|
169
|
+
ariaLabel: PropTypes.string,
|
|
170
|
+
checkbox: PropTypes.bool,
|
|
171
|
+
checked: PropTypes.bool,
|
|
172
|
+
chevron: PropTypes.bool,
|
|
173
|
+
/**
|
|
174
|
+
* Nested menu items can become submenus.
|
|
175
|
+
* See `showSubMenu` and `toggleSubMenu` props, and 'Children' demo
|
|
176
|
+
*/
|
|
177
|
+
children: PropTypes.node,
|
|
178
|
+
className: PropTypes.string,
|
|
179
|
+
dataTest: PropTypes.string,
|
|
180
|
+
dense: PropTypes.bool,
|
|
181
|
+
destructive: PropTypes.bool,
|
|
182
|
+
disabled: PropTypes.bool,
|
|
183
|
+
/** For using menu item as a link */
|
|
184
|
+
href: PropTypes.string,
|
|
185
|
+
/** An icon for the left side of the menu item */
|
|
186
|
+
icon: PropTypes.node,
|
|
187
|
+
/** Text in the menu item. If it's a string, will be used as aria-label */
|
|
188
|
+
label: PropTypes.node,
|
|
189
|
+
/** When true, nested menu items are shown in a Popper */
|
|
190
|
+
showSubMenu: PropTypes.bool,
|
|
191
|
+
/** A supporting element shown at the end of the menu item */
|
|
192
|
+
suffix: PropTypes.node,
|
|
193
|
+
tabIndex: PropTypes.number,
|
|
194
|
+
/** For using menu item as a link */
|
|
195
|
+
target: PropTypes.string,
|
|
196
|
+
/** On click, this function is called (without args) */
|
|
197
|
+
toggleSubMenu: PropTypes.func,
|
|
198
|
+
/** Value associated with item. Passed as an argument to onClick handler. */
|
|
199
|
+
value: PropTypes.string,
|
|
200
|
+
/** Click handler called with signature `({ value: string }, event)` */
|
|
201
|
+
onClick: PropTypes.func,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { MenuItem }
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Tag } from '@dhis2/ui'
|
|
2
|
+
import { colors } from '@dhis2/ui-constants'
|
|
3
|
+
import {
|
|
4
|
+
IconApps24,
|
|
5
|
+
IconVisualizationColumn24,
|
|
6
|
+
IconLaunch16,
|
|
7
|
+
} from '@dhis2/ui-icons'
|
|
8
|
+
import React, { useState } from 'react'
|
|
9
|
+
import { Menu } from '../index.js'
|
|
10
|
+
import { MenuItem } from './menu-item.js'
|
|
11
|
+
|
|
12
|
+
const description = `
|
|
13
|
+
Menu Items are intended to be children of Menu and Flyout Menu components. They can be nested to create submenus.
|
|
14
|
+
|
|
15
|
+
Splitting menus into several levels with child menus makes sense when there are a lot of options that can be grouped together. An example may be an option in level 1 menu of 'Download' that has several different download formats as child menu items. Make sure that child menu items relate to their parent item, otherwise a user will struggle to discover them. A menu item with children is not selectable/actionable itself, it serves only as a container for the child elements. Try to keep menus to a maximum of 1, 2 or 3 levels, anything more than this can easily confuse the user.
|
|
16
|
+
|
|
17
|
+
There is no enforced ordering of menu items, they should be presented in order of relevance. Put the most commonly used items at the top of the menu for easy discovery and access.
|
|
18
|
+
|
|
19
|
+
See the [design system](https://github.com/dhis2/design-system/blob/master/molecules/menu.md) for more information about menus.
|
|
20
|
+
|
|
21
|
+
\`\`\`js
|
|
22
|
+
import { MenuItem } from '@dhis2/ui'
|
|
23
|
+
\`\`\`
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
title: 'Menu Item',
|
|
28
|
+
component: MenuItem,
|
|
29
|
+
args: { label: 'Menu item' },
|
|
30
|
+
parameters: { docs: { description: { component: description } } },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const Template = (args) => (
|
|
34
|
+
<Menu>
|
|
35
|
+
<MenuItem {...args} />
|
|
36
|
+
</Menu>
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export const Default = Template.bind({})
|
|
40
|
+
|
|
41
|
+
export const Active = Template.bind({})
|
|
42
|
+
Active.args = { active: true }
|
|
43
|
+
|
|
44
|
+
export const Chevron = Template.bind({})
|
|
45
|
+
Chevron.args = { chevron: true }
|
|
46
|
+
|
|
47
|
+
export const Dense = Template.bind({})
|
|
48
|
+
Dense.args = { dense: true }
|
|
49
|
+
|
|
50
|
+
export const Destructive = Template.bind({})
|
|
51
|
+
Destructive.args = { destructive: true }
|
|
52
|
+
Destructive.parameters = {
|
|
53
|
+
docs: {
|
|
54
|
+
description: {
|
|
55
|
+
story: "Destructive menu items should be used for critical, destructive actions such as 'Delete', 'Remove' or 'End process'. Do not use destructive menu items for actions that are simply important, they must also be destructive in nature. A menu should, ideally, only have one destructive action. Using a divider to separate normal and destructive options helps the user to understand that the destructive options is different from the rest of the options.",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Disabled = Template.bind({})
|
|
61
|
+
Disabled.args = { disabled: true }
|
|
62
|
+
Disabled.parameters = {
|
|
63
|
+
docs: {
|
|
64
|
+
description: {
|
|
65
|
+
story: 'Menu items should be disabled when they are not available, but could be available if something changes. Do not include menu items that will never be available, this will confuse a user. Instead, remove them from the menu.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const Link = Template.bind({})
|
|
71
|
+
Link.args = { target: '_blank', href: 'http://dhis2.org' }
|
|
72
|
+
|
|
73
|
+
export const Icon = (args) => {
|
|
74
|
+
// import { IconApps24 } from '@dhis2/ui'
|
|
75
|
+
return (
|
|
76
|
+
<Menu>
|
|
77
|
+
<MenuItem {...args} icon={<IconApps24 />} label="Menu item" />
|
|
78
|
+
<MenuItem
|
|
79
|
+
icon={<IconApps24 color="magenta" />}
|
|
80
|
+
label="Menu item - with custom icon fill"
|
|
81
|
+
/>
|
|
82
|
+
</Menu>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
Icon.parameters = {
|
|
86
|
+
docs: {
|
|
87
|
+
source: { type: 'code' },
|
|
88
|
+
description: {
|
|
89
|
+
story: 'A menu item can include an icon to help the user understand or recognize the option. An icon should support the menu item text and be simple enough to be understood in a dense UI. Icons add a lot of visual noise a menu, so only include them where they will help the user. Do not include icons only for visual reasons, the icon must functionally support the users understanding. Do not use complex icons. All menu items in a single menu do not need to have icons.',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const InContainer = (args) => {
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
style={{
|
|
98
|
+
height: 40,
|
|
99
|
+
width: 300,
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<Menu {...args}>
|
|
104
|
+
<MenuItem label="Menu item" />
|
|
105
|
+
</Menu>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const Suffix = Template.bind({})
|
|
111
|
+
Suffix.args = {
|
|
112
|
+
label: 'Open in Data Visualizer',
|
|
113
|
+
icon: <IconVisualizationColumn24 color={colors.grey600} />,
|
|
114
|
+
suffix: <IconLaunch16 color={colors.grey600} />,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const SuffixAndChevron = Template.bind({})
|
|
118
|
+
SuffixAndChevron.args = {
|
|
119
|
+
label: 'Security notifications',
|
|
120
|
+
chevron: true,
|
|
121
|
+
suffix: <Tag>3</Tag>,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const OnClick = (args) => (
|
|
125
|
+
<Menu>
|
|
126
|
+
<MenuItem
|
|
127
|
+
onClick={(payload, event) => {
|
|
128
|
+
console.log(payload.value, event.target)
|
|
129
|
+
}}
|
|
130
|
+
value="myValue"
|
|
131
|
+
label="Menu item"
|
|
132
|
+
{...args}
|
|
133
|
+
/>
|
|
134
|
+
</Menu>
|
|
135
|
+
)
|
|
136
|
+
OnClick.parameters = { docs: { source: { type: 'code' } } }
|
|
137
|
+
OnClick.args = { onClick: console.log }
|
|
138
|
+
|
|
139
|
+
export const ToggleMenuItem = (args) => {
|
|
140
|
+
const [on, setOn] = useState(false)
|
|
141
|
+
const toggleOn = () => setOn(!on)
|
|
142
|
+
const checkMarkStyle = { fontSize: '24px', lineHeight: '24px' }
|
|
143
|
+
const icon = on ? <span style={checkMarkStyle}>✓</span> : <span />
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Menu>
|
|
147
|
+
<MenuItem
|
|
148
|
+
{...args}
|
|
149
|
+
onClick={toggleOn}
|
|
150
|
+
icon={icon}
|
|
151
|
+
label="A toggle menu item"
|
|
152
|
+
checkbox
|
|
153
|
+
checked={on}
|
|
154
|
+
/>
|
|
155
|
+
</Menu>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
ToggleMenuItem.parameters = { docs: { source: { type: 'code' } } }
|
|
159
|
+
|
|
160
|
+
export const SubMenus = (args) => {
|
|
161
|
+
const [showSubMenu, setShowSubMenu] = React.useState(false)
|
|
162
|
+
const toggleSubMenu = () => setShowSubMenu(!showSubMenu)
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Menu>
|
|
166
|
+
<MenuItem
|
|
167
|
+
showSubMenu={showSubMenu}
|
|
168
|
+
toggleSubMenu={toggleSubMenu}
|
|
169
|
+
{...args}
|
|
170
|
+
label="Parent of submenus"
|
|
171
|
+
>
|
|
172
|
+
<MenuItem label="Submenu child 1" />
|
|
173
|
+
<MenuItem label="Submenu child 2" />
|
|
174
|
+
</MenuItem>
|
|
175
|
+
<MenuItem {...args} label="Regular item" />
|
|
176
|
+
</Menu>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
SubMenus.parameters = {
|
|
180
|
+
docs: {
|
|
181
|
+
source: { type: 'code' },
|
|
182
|
+
description: {
|
|
183
|
+
story: "_View this story in the 'Canvas' tab for proper submenu alignment._",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const RTLSuffixAndChevron = (args) => (
|
|
189
|
+
<div dir="rtl">
|
|
190
|
+
<Template {...args} />
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
RTLSuffixAndChevron.args = {
|
|
194
|
+
label: 'Security notifications',
|
|
195
|
+
chevron: true,
|
|
196
|
+
suffix: <Tag>3</Tag>,
|
|
197
|
+
}
|