@dhis2-ui/menu 10.16.1 → 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2-ui/menu",
|
|
3
|
-
"version": "10.16.1",
|
|
3
|
+
"version": "10.16.3-alpha.1",
|
|
4
4
|
"description": "UI Menu",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,19 +33,20 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@dhis2/prop-types": "^3.1.2",
|
|
36
|
-
"@dhis2-ui/card": "10.16.1",
|
|
37
|
-
"@dhis2-ui/divider": "10.16.1",
|
|
38
|
-
"@dhis2-ui/layer": "10.16.1",
|
|
39
|
-
"@dhis2-ui/popper": "10.16.1",
|
|
40
|
-
"@dhis2-ui/portal": "10.16.1",
|
|
41
|
-
"@dhis2/ui-constants": "10.16.1",
|
|
42
|
-
"@dhis2/ui-icons": "10.16.1",
|
|
36
|
+
"@dhis2-ui/card": "10.16.3-alpha.1",
|
|
37
|
+
"@dhis2-ui/divider": "10.16.3-alpha.1",
|
|
38
|
+
"@dhis2-ui/layer": "10.16.3-alpha.1",
|
|
39
|
+
"@dhis2-ui/popper": "10.16.3-alpha.1",
|
|
40
|
+
"@dhis2-ui/portal": "10.16.3-alpha.1",
|
|
41
|
+
"@dhis2/ui-constants": "10.16.3-alpha.1",
|
|
42
|
+
"@dhis2/ui-icons": "10.16.3-alpha.1",
|
|
43
43
|
"classnames": "^2.3.1",
|
|
44
44
|
"prop-types": "^15.7.2"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"build",
|
|
48
|
-
"types"
|
|
48
|
+
"types",
|
|
49
|
+
"src"
|
|
49
50
|
],
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"react": "^18.3.1",
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
|
|
7
|
+
describe('Flyout Menu Component', () => {
|
|
8
|
+
it('can handle navigation of submenus', async () => {
|
|
9
|
+
const { getByText, queryByText, getAllByRole } = render(
|
|
10
|
+
<FlyoutMenu>
|
|
11
|
+
<MenuItem label="Item 1" />
|
|
12
|
+
<MenuItem label="Item 2">
|
|
13
|
+
<MenuItem label="Item 2 a" />
|
|
14
|
+
</MenuItem>
|
|
15
|
+
</FlyoutMenu>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const itemOne = getByText(/Item 1/i)
|
|
19
|
+
const itemTwo = getByText(/Item 2/i)
|
|
20
|
+
let submenuChild = queryByText(/Item 2 a/i)
|
|
21
|
+
|
|
22
|
+
const menuItems = getAllByRole('menuitem')
|
|
23
|
+
|
|
24
|
+
expect(menuItems.length).toBe(2)
|
|
25
|
+
expect(menuItems[0]).toBe(itemOne.parentNode)
|
|
26
|
+
expect(menuItems[1]).toBe(itemTwo.parentNode)
|
|
27
|
+
|
|
28
|
+
expect(submenuChild).not.toBeInTheDocument()
|
|
29
|
+
|
|
30
|
+
await userEvent.tab()
|
|
31
|
+
expect(menuItems[0].parentNode).toHaveFocus()
|
|
32
|
+
expect(menuItems[1].parentNode).not.toHaveFocus()
|
|
33
|
+
|
|
34
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
35
|
+
expect(menuItems[0].parentNode).not.toHaveFocus()
|
|
36
|
+
expect(menuItems[1].parentNode).toHaveFocus()
|
|
37
|
+
|
|
38
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
39
|
+
submenuChild = getByText(/Item 2 a/i)
|
|
40
|
+
|
|
41
|
+
expect(submenuChild).toBeInTheDocument()
|
|
42
|
+
expect(submenuChild.parentElement.parentElement).toHaveFocus()
|
|
43
|
+
|
|
44
|
+
await userEvent.keyboard('{ArrowLeft}')
|
|
45
|
+
expect(queryByText(/Item 2 a/i)).not.toBeInTheDocument()
|
|
46
|
+
expect(menuItems[1].parentNode).toHaveFocus()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a FlyoutMenu with children is rendered', () => {
|
|
4
|
+
cy.visitStory('FlyoutMenu', 'With Children')
|
|
5
|
+
cy.get('[data-test="dhis2-uicore-menu"]').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,80 @@
|
|
|
1
|
+
import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
const CLOSE_TO_DELTA = 1
|
|
4
|
+
|
|
5
|
+
Given(
|
|
6
|
+
'there is enough space to the right of the MenuItem to fit the SubMenu',
|
|
7
|
+
() => {
|
|
8
|
+
cy.visitStory('FlyoutMenu', 'Default Position')
|
|
9
|
+
}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
Given(
|
|
13
|
+
'there is not enough space to the right of the MenuItem to fit the SubMenu',
|
|
14
|
+
() => {
|
|
15
|
+
cy.visitStory('FlyoutMenu', 'Flipped Position')
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Given(
|
|
20
|
+
'there is not enough space to the right or the left of the MenuItem to fit the SubMenu',
|
|
21
|
+
() => {
|
|
22
|
+
cy.visitStory('FlyoutMenu', 'Shift Into View')
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
When('the user clicks on the MenuItem', () => {
|
|
27
|
+
cy.get('[data-test="dhis2-uicore-menuitem"]').click()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
Then(
|
|
31
|
+
'the right of the MenuItem is aligned with the left of the SubMenu',
|
|
32
|
+
() => {
|
|
33
|
+
getMenuItemAndSubMenuRects().should(([menuItemRect, subMenuRect]) => {
|
|
34
|
+
expect(menuItemRect.right).to.closeTo(
|
|
35
|
+
subMenuRect.left,
|
|
36
|
+
CLOSE_TO_DELTA
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
Then(
|
|
43
|
+
'the left of the MenuItem is aligned with the right of the SubMenu',
|
|
44
|
+
() => {
|
|
45
|
+
getMenuItemAndSubMenuRects().should(([menuItemRect, subMenuRect]) => {
|
|
46
|
+
expect(menuItemRect.left).to.closeTo(
|
|
47
|
+
subMenuRect.right,
|
|
48
|
+
CLOSE_TO_DELTA
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
Then('the SubMenu is rendered on top of the MenuItem', () => {
|
|
55
|
+
getMenuItemAndSubMenuRects().should(([menuItemRect, subMenuRect]) => {
|
|
56
|
+
expect(subMenuRect.left).to.be.at.most(menuItemRect.right)
|
|
57
|
+
expect(subMenuRect.right).to.be.at.least(menuItemRect.left)
|
|
58
|
+
expect(subMenuRect.top).to.be.at.most(menuItemRect.bottom)
|
|
59
|
+
expect(subMenuRect.bottom).to.be.at.least(menuItemRect.top)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
Then(
|
|
64
|
+
'the top of the MenuItem is aligned with the top of the SubMenu wrapper',
|
|
65
|
+
() => {
|
|
66
|
+
cy.getPositionsBySelectors(
|
|
67
|
+
'[data-test="dhis2-uicore-menuitem"]',
|
|
68
|
+
'[data-test="dhis2-uicore-popper"]'
|
|
69
|
+
).should(([menuItemRect, popperRect]) => {
|
|
70
|
+
expect(menuItemRect.top).to.closeTo(popperRect.top, CLOSE_TO_DELTA)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
function getMenuItemAndSubMenuRects() {
|
|
76
|
+
return cy.getPositionsBySelectors(
|
|
77
|
+
'[data-test="dhis2-uicore-menuitem"]',
|
|
78
|
+
'[data-test="dhis2-uicore-popper"]'
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Feature: Position of a SubMenu component
|
|
2
|
+
|
|
3
|
+
Scenario: Default rendering
|
|
4
|
+
Given there is enough space to the right of the MenuItem to fit the SubMenu
|
|
5
|
+
When the user clicks on the MenuItem
|
|
6
|
+
Then the right of the MenuItem is aligned with the left of the SubMenu
|
|
7
|
+
And the top of the MenuItem is aligned with the top of the SubMenu wrapper
|
|
8
|
+
|
|
9
|
+
Scenario: Flipped rendering when insufficient space to the right of
|
|
10
|
+
Given there is not enough space to the right of the MenuItem to fit the SubMenu
|
|
11
|
+
When the user clicks on the MenuItem
|
|
12
|
+
Then the left of the MenuItem is aligned with the right of the SubMenu
|
|
13
|
+
And the top of the MenuItem is aligned with the top of the SubMenu wrapper
|
|
14
|
+
|
|
15
|
+
Scenario: Shifting into view when insufficient space to the right of and above
|
|
16
|
+
Given there is not enough space to the right or the left of the MenuItem to fit the SubMenu
|
|
17
|
+
When the user clicks on the MenuItem
|
|
18
|
+
Then the SubMenu is rendered on top of the MenuItem
|
|
19
|
+
And the top of the MenuItem is aligned with the top of the SubMenu wrapper
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a FlyoutMenu with two SubMenus is rendered', () => {
|
|
4
|
+
cy.visitStory('FlyoutMenu', 'Toggles Sub Menus')
|
|
5
|
+
cy.get('[data-test="dhis2-uicore-menu"]').should('be.visible')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
When('the user clicks the first SubMenu anchor', () => {
|
|
9
|
+
cy.contains('Item 1').click()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
Then('first SubMenu is visible', () => {
|
|
13
|
+
cy.contains('SubMenu 1').should('be.visible')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
When('the user clicks the second SubMenu anchor', () => {
|
|
17
|
+
cy.contains('Item 2').click()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
Then('second SubMenu is visible', () => {
|
|
21
|
+
cy.contains('SubMenu 2').should('be.visible')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
Then('the first SubMenu is not rendered', () => {
|
|
25
|
+
cy.contains('SubMenu 1').should('not.exist')
|
|
26
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Feature: The FlyoutMenu toggles SubMenus
|
|
2
|
+
|
|
3
|
+
Scenario: a FlyoutMenu with two SubMenus
|
|
4
|
+
Given a FlyoutMenu with two SubMenus is rendered
|
|
5
|
+
When the user clicks the first SubMenu anchor
|
|
6
|
+
Then first SubMenu is visible
|
|
7
|
+
When the user clicks the second SubMenu anchor
|
|
8
|
+
Then second SubMenu is visible
|
|
9
|
+
And the first SubMenu is not rendered
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { MenuItem } from '@dhis2-ui/menu'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { FlyoutMenu } from './flyout-menu.js'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'FlyoutMenu',
|
|
7
|
+
component: FlyoutMenu,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MenuItemSubMenuPositions = () => (
|
|
11
|
+
<FlyoutMenu>
|
|
12
|
+
<MenuItem label="Item 1">
|
|
13
|
+
<div style={{ padding: 30 }}>SubMenu 1</div>
|
|
14
|
+
</MenuItem>
|
|
15
|
+
</FlyoutMenu>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export const WithChildren = () => <FlyoutMenu>I am a child</FlyoutMenu>
|
|
19
|
+
|
|
20
|
+
export const TogglesSubMenus = () => (
|
|
21
|
+
<FlyoutMenu>
|
|
22
|
+
<MenuItem label="Item 1">SubMenu 1</MenuItem>
|
|
23
|
+
<MenuItem label="Item 2">SubMenu 2</MenuItem>
|
|
24
|
+
</FlyoutMenu>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export const DefaultPosition = () => <MenuItemSubMenuPositions />
|
|
28
|
+
export const FlippedPosition = () => (
|
|
29
|
+
<div>
|
|
30
|
+
<MenuItemSubMenuPositions />
|
|
31
|
+
<style jsx>{`
|
|
32
|
+
div {
|
|
33
|
+
text-align: right;
|
|
34
|
+
}
|
|
35
|
+
`}</style>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
export const ShiftIntoView = () => (
|
|
39
|
+
<div>
|
|
40
|
+
<MenuItemSubMenuPositions />
|
|
41
|
+
<style jsx>{`
|
|
42
|
+
:global(html),
|
|
43
|
+
:global(body),
|
|
44
|
+
:global(#root) {
|
|
45
|
+
position: relative;
|
|
46
|
+
width: 200px;
|
|
47
|
+
max-width: 200px;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
50
|
+
:global(#root) {
|
|
51
|
+
border: 3px dashed grey;
|
|
52
|
+
}
|
|
53
|
+
`}</style>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { colors, elevations, spacers } from '@dhis2/ui-constants'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import React, {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
isValidElement,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import { Menu } from '../menu/index.js'
|
|
12
|
+
|
|
13
|
+
const FlyoutMenu = ({
|
|
14
|
+
children,
|
|
15
|
+
className,
|
|
16
|
+
dataTest = 'dhis2-uicore-menu',
|
|
17
|
+
dense,
|
|
18
|
+
maxHeight = 'auto',
|
|
19
|
+
maxWidth = '380px',
|
|
20
|
+
closeMenu,
|
|
21
|
+
}) => {
|
|
22
|
+
const [openedSubMenu, setOpenedSubMenu] = useState(null)
|
|
23
|
+
const toggleSubMenu = (index) => {
|
|
24
|
+
const toggleValue = index === openedSubMenu ? null : index
|
|
25
|
+
setOpenedSubMenu(toggleValue)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const divRef = useRef(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!divRef.current) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const div = divRef.current
|
|
35
|
+
|
|
36
|
+
const handleFocus = (event) => {
|
|
37
|
+
if (event.target === div) {
|
|
38
|
+
if (div?.children && div.children.length > 0) {
|
|
39
|
+
div.children[0].focus()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleKeyDown = (event) => {
|
|
45
|
+
if (event.key === 'Escape') {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
closeMenu && closeMenu()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
div.addEventListener('focus', handleFocus)
|
|
52
|
+
div.addEventListener('keydown', handleKeyDown)
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
div.removeEventListener('focus', handleFocus)
|
|
56
|
+
div.removeEventListener('keydown', handleKeyDown)
|
|
57
|
+
}
|
|
58
|
+
}, [closeMenu])
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={className}
|
|
63
|
+
data-test={dataTest}
|
|
64
|
+
tabIndex={0}
|
|
65
|
+
ref={divRef}
|
|
66
|
+
>
|
|
67
|
+
<Menu dense={dense}>
|
|
68
|
+
{Children.map(children, (child, index) =>
|
|
69
|
+
isValidElement(child)
|
|
70
|
+
? cloneElement(child, {
|
|
71
|
+
showSubMenu: openedSubMenu === index,
|
|
72
|
+
toggleSubMenu: toggleSubMenu.bind(this, index),
|
|
73
|
+
})
|
|
74
|
+
: child
|
|
75
|
+
)}
|
|
76
|
+
</Menu>
|
|
77
|
+
|
|
78
|
+
<style jsx>{`
|
|
79
|
+
div {
|
|
80
|
+
background: ${colors.white};
|
|
81
|
+
border-radius: 3px;
|
|
82
|
+
box-shadow: ${elevations.popover};
|
|
83
|
+
display: inline-block;
|
|
84
|
+
min-width: ${dense ? '128' : '180'}px;
|
|
85
|
+
max-width: ${maxWidth};
|
|
86
|
+
max-height: ${maxHeight};
|
|
87
|
+
padding: ${spacers.dp4} 0;
|
|
88
|
+
overflow: auto;
|
|
89
|
+
}
|
|
90
|
+
`}</style>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
FlyoutMenu.propTypes = {
|
|
96
|
+
/** Typically, but not limited to, `MenuItem` components */
|
|
97
|
+
children: PropTypes.node,
|
|
98
|
+
className: PropTypes.string,
|
|
99
|
+
/** when Escape key is pressed, this function is called to close the flyout menu */
|
|
100
|
+
closeMenu: PropTypes.func,
|
|
101
|
+
dataTest: PropTypes.string,
|
|
102
|
+
/** Menu uses smaller dimensions */
|
|
103
|
+
dense: PropTypes.bool,
|
|
104
|
+
maxHeight: PropTypes.string,
|
|
105
|
+
maxWidth: PropTypes.string,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { FlyoutMenu }
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { IconChevronDown16 } from '@dhis2/ui-icons'
|
|
2
|
+
import { Layer } from '@dhis2-ui/layer'
|
|
3
|
+
import { MenuDivider, MenuItem, MenuSectionHeader } from '@dhis2-ui/menu'
|
|
4
|
+
import { Popper } from '@dhis2-ui/popper'
|
|
5
|
+
import React, { useState, useRef } from 'react'
|
|
6
|
+
import { FlyoutMenu } from './flyout-menu.js'
|
|
7
|
+
|
|
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
|
+
`
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
title: 'Flyout Menu',
|
|
29
|
+
component: FlyoutMenu,
|
|
30
|
+
parameters: { docs: { description: { component: description } } },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Default = (args) => (
|
|
34
|
+
<FlyoutMenu {...args}>
|
|
35
|
+
<MenuItem label="Item 1" />
|
|
36
|
+
<MenuItem label="Item 2" />
|
|
37
|
+
</FlyoutMenu>
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
export const Dense = Default.bind({})
|
|
41
|
+
Dense.args = { dense: true }
|
|
42
|
+
|
|
43
|
+
export const MaxHeight = (args) => (
|
|
44
|
+
<FlyoutMenu {...args}>
|
|
45
|
+
<MenuItem label="Item 1" />
|
|
46
|
+
<MenuItem label="Item 2" />
|
|
47
|
+
<MenuItem label="Item 3" />
|
|
48
|
+
<MenuItem label="Item 4" />
|
|
49
|
+
<MenuItem label="Item 5" />
|
|
50
|
+
<MenuItem label="Item 6" />
|
|
51
|
+
<MenuItem label="Item 7" />
|
|
52
|
+
<MenuItem label="Item 8" />
|
|
53
|
+
<MenuItem label="Item 9" />
|
|
54
|
+
<MenuItem label="Item 10" />
|
|
55
|
+
</FlyoutMenu>
|
|
56
|
+
)
|
|
57
|
+
MaxHeight.args = { maxHeight: '250px' }
|
|
58
|
+
|
|
59
|
+
export const MaxWidth = (args) => (
|
|
60
|
+
<>
|
|
61
|
+
<FlyoutMenu>
|
|
62
|
+
<MenuItem label="Short item 1" />
|
|
63
|
+
<MenuItem label="Short item 2" />
|
|
64
|
+
</FlyoutMenu>
|
|
65
|
+
<br />
|
|
66
|
+
<FlyoutMenu>
|
|
67
|
+
<MenuItem
|
|
68
|
+
label="Item 1 - with a lot of text and using a default maxWidth value
|
|
69
|
+
of 380px"
|
|
70
|
+
/>
|
|
71
|
+
<MenuItem
|
|
72
|
+
label="Item 2 - with a lot of text and using a default maxWidth value
|
|
73
|
+
of 380px"
|
|
74
|
+
/>
|
|
75
|
+
</FlyoutMenu>
|
|
76
|
+
<br />
|
|
77
|
+
<FlyoutMenu {...args}>
|
|
78
|
+
<MenuItem
|
|
79
|
+
label={`Item 1 - with a lot of text and using a custom maxWidth value of
|
|
80
|
+
${args.maxWidth}`}
|
|
81
|
+
/>
|
|
82
|
+
<MenuItem
|
|
83
|
+
label={`Item 2 - with a lot of text and using a custom maxWidth value of
|
|
84
|
+
${args.maxWidth}`}
|
|
85
|
+
/>
|
|
86
|
+
</FlyoutMenu>
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
MaxWidth.args = { maxWidth: '300px' }
|
|
90
|
+
|
|
91
|
+
export const WithSubMenus = (args) => (
|
|
92
|
+
<FlyoutMenu {...args}>
|
|
93
|
+
<MenuItem label="Item 1" />
|
|
94
|
+
<MenuItem label="Item 2">
|
|
95
|
+
<MenuItem label="Item 2 a" />
|
|
96
|
+
<MenuItem label="Item 2 b">
|
|
97
|
+
<MenuItem label="Item 2 b i" />
|
|
98
|
+
<MenuItem label="Item 2 b ii" />
|
|
99
|
+
</MenuItem>
|
|
100
|
+
<MenuItem label="Item 2 c" />
|
|
101
|
+
</MenuItem>
|
|
102
|
+
<MenuItem label="Item 3" />
|
|
103
|
+
<MenuItem label="Item 4">
|
|
104
|
+
<MenuItem label="Item 4 a" />
|
|
105
|
+
<MenuItem label="Item 4 b">
|
|
106
|
+
<MenuItem label="Item 4 b i" />
|
|
107
|
+
<MenuItem label="Item 4 b ii" />
|
|
108
|
+
</MenuItem>
|
|
109
|
+
<MenuItem label="Item 4 c" />
|
|
110
|
+
</MenuItem>
|
|
111
|
+
<MenuItem label="Item 5" />
|
|
112
|
+
</FlyoutMenu>
|
|
113
|
+
)
|
|
114
|
+
WithSubMenus.parameters = {
|
|
115
|
+
docs: {
|
|
116
|
+
description: {
|
|
117
|
+
story: 'See this demo in the Canvas tab for proper alignment of sub menus.',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const WithVariousChildren = (args) => (
|
|
123
|
+
<FlyoutMenu {...args}>
|
|
124
|
+
<MenuSectionHeader label="Section with sub-menus" />
|
|
125
|
+
<MenuItem label="Item 1" />
|
|
126
|
+
<MenuItem label="Item 2">
|
|
127
|
+
<MenuItem label="Item 2 a" />
|
|
128
|
+
<MenuItem label="Item 2 b">
|
|
129
|
+
<MenuItem label="Item 2 b i" />
|
|
130
|
+
<MenuItem label="Item 2 b ii" />
|
|
131
|
+
</MenuItem>
|
|
132
|
+
<MenuItem label="Item 2 c" />
|
|
133
|
+
</MenuItem>
|
|
134
|
+
<MenuItem label="Item 3" />
|
|
135
|
+
<MenuItem label="Item 4">
|
|
136
|
+
<MenuItem label="Item 4 a" />
|
|
137
|
+
<MenuItem label="Item 4 b">
|
|
138
|
+
<MenuItem label="Item 4 b i" />
|
|
139
|
+
<MenuItem label="Item 4 b ii" />
|
|
140
|
+
</MenuItem>
|
|
141
|
+
<MenuItem label="Item 4 c" />
|
|
142
|
+
</MenuItem>
|
|
143
|
+
<MenuItem label="Item 5" />
|
|
144
|
+
<MenuSectionHeader label="Section with dividers between menu items" />
|
|
145
|
+
<MenuItem label="Item 1" />
|
|
146
|
+
<MenuDivider />
|
|
147
|
+
<MenuItem label="Item 2" />
|
|
148
|
+
<MenuDivider />
|
|
149
|
+
<MenuItem label="Item 3" />
|
|
150
|
+
</FlyoutMenu>
|
|
151
|
+
)
|
|
152
|
+
WithVariousChildren.parameters = {
|
|
153
|
+
docs: {
|
|
154
|
+
description: {
|
|
155
|
+
story: 'See this demo in the Canvas tab for proper alignment of sub menus.',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const DropDownMenu = (args) => {
|
|
161
|
+
const ref = useRef()
|
|
162
|
+
const [open, setOpen] = useState(false)
|
|
163
|
+
const toggle = () => setOpen(!open)
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<button ref={ref} onClick={toggle}>
|
|
168
|
+
Open menu <IconChevronDown16 />
|
|
169
|
+
</button>
|
|
170
|
+
{open && (
|
|
171
|
+
<Layer onBackdropClick={toggle}>
|
|
172
|
+
<Popper reference={ref} placement="bottom-start">
|
|
173
|
+
<FlyoutMenu {...args} closeMenu={toggle}>
|
|
174
|
+
<MenuItem label="Item 1" />
|
|
175
|
+
<MenuItem label="Item 2" />
|
|
176
|
+
</FlyoutMenu>
|
|
177
|
+
</Popper>
|
|
178
|
+
</Layer>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
DropDownMenu.parameters = {
|
|
184
|
+
docs: {
|
|
185
|
+
description: {
|
|
186
|
+
story: 'View this demo in the canvas to see the dropdown menu.',
|
|
187
|
+
},
|
|
188
|
+
source: { type: 'code' },
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const WithCustomMenuItem = (args) => {
|
|
193
|
+
// You should not create custom components in the render cycle
|
|
194
|
+
// this is just for demo purposes
|
|
195
|
+
const PopupWindowMenuItem = ({ to, children, ...rest }) => {
|
|
196
|
+
const HEIGHT = 1000
|
|
197
|
+
const WIDTH = 1400
|
|
198
|
+
const centerY = (window.screen.height - HEIGHT) / 2
|
|
199
|
+
const centerX = (window.screen.width - WIDTH) / 2
|
|
200
|
+
|
|
201
|
+
const onClick = () =>
|
|
202
|
+
window.open(
|
|
203
|
+
to,
|
|
204
|
+
'Popup',
|
|
205
|
+
[
|
|
206
|
+
'menubar=no',
|
|
207
|
+
'location=no',
|
|
208
|
+
'resizable=no',
|
|
209
|
+
'scrollbars=no',
|
|
210
|
+
'status=no',
|
|
211
|
+
`width=${WIDTH}`,
|
|
212
|
+
`height=${HEIGHT}`,
|
|
213
|
+
`top=${centerY}`,
|
|
214
|
+
`left=${centerX}`,
|
|
215
|
+
].join()
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return <MenuItem onClick={onClick} label={children} {...rest} />
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<FlyoutMenu {...args}>
|
|
223
|
+
<MenuItem label="A normal menu item" />
|
|
224
|
+
<PopupWindowMenuItem to="http://dhis2.org">
|
|
225
|
+
A custom menu item (popup window)
|
|
226
|
+
</PopupWindowMenuItem>
|
|
227
|
+
</FlyoutMenu>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FlyoutMenu } from './flyout-menu.js'
|
package/src/index.js
ADDED