@ltht-react/menu 2.0.168 → 2.0.170
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/lib/index.d.ts +4 -2
- package/lib/index.js +28 -3
- package/lib/index.js.map +1 -1
- package/lib/molecules/action-menu.d.ts +10 -7
- package/lib/molecules/action-menu.js +15 -90
- package/lib/molecules/action-menu.js.map +1 -1
- package/lib/molecules/menu.d.ts +193 -0
- package/lib/molecules/menu.js +325 -0
- package/lib/molecules/menu.js.map +1 -0
- package/lib/molecules/menu.style.d.ts +27 -0
- package/lib/molecules/menu.style.js +26 -0
- package/lib/molecules/menu.style.js.map +1 -0
- package/package.json +8 -9
- package/src/index.tsx +4 -2
- package/src/molecules/action-menu.tsx +72 -194
- package/src/molecules/menu.style.tsx +130 -0
- package/src/molecules/menu.tsx +532 -0
|
@@ -1,207 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import Icon, { IconButton, IconProps } from '@ltht-react/icon'
|
|
4
|
-
import { BTN_COLOURS, CSS_RESET, PopUp, TableDataWithPopUp, getZIndex } from '@ltht-react/styles'
|
|
5
|
-
import FocusTrap from 'focus-trap-react'
|
|
6
|
-
import { FC, HTMLAttributes, useRef, useState, useEffect } from 'react'
|
|
7
|
-
import { usePopper } from 'react-popper'
|
|
1
|
+
import { FC, HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
import Icon, { IconProps } from '@ltht-react/icon'
|
|
8
3
|
import { stringToHtmlId } from '@ltht-react/utils'
|
|
4
|
+
import { ButtonProps } from '@ltht-react/button'
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
type: 'icon',
|
|
12
|
-
iconProps: {
|
|
13
|
-
type: 'ellipsis-vertical',
|
|
14
|
-
size: 'large',
|
|
15
|
-
},
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const StyledUnorderedList = styled.ul`
|
|
19
|
-
${CSS_RESET}
|
|
20
|
-
list-style-type: none;
|
|
21
|
-
padding: 0;
|
|
22
|
-
margin: 0;
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
const StyledListItem = styled.li`
|
|
26
|
-
${CSS_RESET}
|
|
27
|
-
background-color: 'white';
|
|
28
|
-
padding: 0.5rem;
|
|
29
|
-
line-height: 1em;
|
|
30
|
-
display: flex;
|
|
31
|
-
border-radius: 4px;
|
|
32
|
-
|
|
33
|
-
&:hover {
|
|
34
|
-
background: ${BTN_COLOURS.PRIMARY.VALUE};
|
|
35
|
-
cursor: pointer;
|
|
36
|
-
color: white;
|
|
37
|
-
}
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
const StyledListItemIcon = styled.div`
|
|
41
|
-
flex-basis: 25%;
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
const StyledListItemContent = styled.div`
|
|
45
|
-
flex: 1;
|
|
46
|
-
text-align: left;
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
const StyledCard = styled.div`
|
|
50
|
-
${CSS_RESET}
|
|
51
|
-
display: inline-block;
|
|
52
|
-
min-width: 10rem;
|
|
53
|
-
z-index: 1;
|
|
54
|
-
background: white;
|
|
55
|
-
border-radius: 4px;
|
|
56
|
-
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.04), 0px 4px 5px rgba(0, 0, 0, 0.06), 0px 2px 4px -1px rgba(0, 0, 0, 0.09);
|
|
57
|
-
`
|
|
58
|
-
|
|
59
|
-
const StyledRightIcon = styled(Icon)`
|
|
60
|
-
margin-right: 0.5rem;
|
|
61
|
-
margin-left: 3rem;
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
const StyledLeftIcon = styled(Icon)`
|
|
65
|
-
margin-right: 0.5rem;
|
|
66
|
-
margin-left: 0.5rem;
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
const StyledMenuButtonWrapper = styled.div`
|
|
70
|
-
display: inline-block;
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
const ActionMenu: FC<IProps> = ({
|
|
74
|
-
actions,
|
|
75
|
-
menuButtonOptions = defaultMenuButtonProps,
|
|
76
|
-
id = 'action-menu-button',
|
|
77
|
-
popupStyle = {},
|
|
78
|
-
popupPlacement = 'bottom-start',
|
|
79
|
-
...rest
|
|
80
|
-
}) => {
|
|
81
|
-
const menuItemIdPrefix = id ? `${id}-` : ''
|
|
82
|
-
const popperRef = useRef<HTMLDivElement>(null)
|
|
83
|
-
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
|
|
84
|
-
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null)
|
|
85
|
-
|
|
86
|
-
const popper = usePopper(popperRef.current, popperElement, {
|
|
87
|
-
placement: popupPlacement,
|
|
88
|
-
strategy: 'fixed',
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const closePopper = () => {
|
|
92
|
-
setShowMenu(false)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const [showMenu, setShowMenu] = useState(false)
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (containerElement?.parentElement?.style) {
|
|
99
|
-
containerElement.parentElement.style.zIndex = showMenu
|
|
100
|
-
? `${getZIndex(PopUp)}`
|
|
101
|
-
: `${getZIndex(TableDataWithPopUp)}`
|
|
102
|
-
}
|
|
103
|
-
}, [containerElement, showMenu])
|
|
104
|
-
|
|
105
|
-
const menuButtonClickHandler = () => {
|
|
106
|
-
setShowMenu(!showMenu)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<div ref={setContainerElement}>
|
|
111
|
-
<FocusTrap
|
|
112
|
-
active={showMenu}
|
|
113
|
-
focusTrapOptions={{
|
|
114
|
-
tabbableOptions: {
|
|
115
|
-
displayCheck: 'none',
|
|
116
|
-
},
|
|
117
|
-
initialFocus: false,
|
|
118
|
-
allowOutsideClick: false,
|
|
119
|
-
clickOutsideDeactivates: true,
|
|
120
|
-
onDeactivate: closePopper,
|
|
121
|
-
}}
|
|
122
|
-
>
|
|
123
|
-
<StyledMenuButtonWrapper ref={popperRef}>
|
|
124
|
-
{menuButtonOptions.type === 'icon' && (
|
|
125
|
-
<IconButton
|
|
126
|
-
iconProps={menuButtonOptions.iconProps}
|
|
127
|
-
text={menuButtonOptions.text}
|
|
128
|
-
{...rest}
|
|
129
|
-
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
130
|
-
e.stopPropagation()
|
|
131
|
-
menuButtonClickHandler()
|
|
132
|
-
}}
|
|
133
|
-
id={id}
|
|
134
|
-
data-testid={id}
|
|
135
|
-
/>
|
|
136
|
-
)}
|
|
137
|
-
{menuButtonOptions.type === 'button' && (
|
|
138
|
-
<Button
|
|
139
|
-
{...menuButtonOptions.buttonProps}
|
|
140
|
-
{...rest}
|
|
141
|
-
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
142
|
-
e.stopPropagation()
|
|
143
|
-
menuButtonClickHandler()
|
|
144
|
-
}}
|
|
145
|
-
id={id}
|
|
146
|
-
data-testid={id}
|
|
147
|
-
>
|
|
148
|
-
{menuButtonOptions.text}
|
|
149
|
-
</Button>
|
|
150
|
-
)}
|
|
151
|
-
{showMenu && (
|
|
152
|
-
<StyledCard
|
|
153
|
-
tabIndex={-1}
|
|
154
|
-
ref={setPopperElement}
|
|
155
|
-
style={{ ...popper.styles.popper, ...popupStyle }}
|
|
156
|
-
{...popper.attributes.popper}
|
|
157
|
-
>
|
|
158
|
-
<StyledUnorderedList role="menu" aria-labelledby={id}>
|
|
159
|
-
{actions.map((action, idx) => {
|
|
160
|
-
const textId = stringToHtmlId(action.text)
|
|
161
|
-
const actionMenuItemId = `${menuItemIdPrefix}action-menu-item-${textId}-${idx}`
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<StyledListItem
|
|
165
|
-
data-testid={actionMenuItemId}
|
|
166
|
-
id={actionMenuItemId}
|
|
167
|
-
role="menuitem"
|
|
168
|
-
key={`menu-action-${idx}`}
|
|
169
|
-
onClick={(e) => {
|
|
170
|
-
e.stopPropagation()
|
|
171
|
-
menuButtonClickHandler()
|
|
172
|
-
action.clickHandler()
|
|
173
|
-
}}
|
|
174
|
-
>
|
|
175
|
-
<StyledListItemIcon>
|
|
176
|
-
{action.leftIcon && <StyledLeftIcon {...action.leftIcon} />}
|
|
177
|
-
</StyledListItemIcon>
|
|
178
|
-
<StyledListItemContent>{action.text}</StyledListItemContent>
|
|
179
|
-
<StyledListItemIcon>
|
|
180
|
-
{action.rightIcon && <StyledRightIcon {...action.rightIcon} />}
|
|
181
|
-
</StyledListItemIcon>
|
|
182
|
-
</StyledListItem>
|
|
183
|
-
)
|
|
184
|
-
})}
|
|
185
|
-
</StyledUnorderedList>
|
|
186
|
-
</StyledCard>
|
|
187
|
-
)}
|
|
188
|
-
</StyledMenuButtonWrapper>
|
|
189
|
-
</FocusTrap>
|
|
190
|
-
</div>
|
|
191
|
-
)
|
|
192
|
-
}
|
|
6
|
+
import { Menu, MenuItem } from './menu'
|
|
193
7
|
|
|
194
8
|
interface IProps extends HTMLAttributes<HTMLButtonElement> {
|
|
195
9
|
actions: ActionMenuOption[]
|
|
196
10
|
menuButtonOptions?: IconButtonMenuProps | ButtonMenuProps
|
|
197
|
-
|
|
198
|
-
popupPlacement?: 'bottom-start' | 'right-start'
|
|
11
|
+
disabled?: boolean
|
|
199
12
|
}
|
|
200
13
|
|
|
201
14
|
interface IconButtonMenuProps {
|
|
202
15
|
type: 'icon'
|
|
203
16
|
iconProps: IconProps
|
|
204
17
|
text?: string
|
|
18
|
+
disabled?: boolean
|
|
205
19
|
}
|
|
206
20
|
|
|
207
21
|
interface ButtonMenuProps {
|
|
@@ -210,11 +24,75 @@ interface ButtonMenuProps {
|
|
|
210
24
|
text: string
|
|
211
25
|
}
|
|
212
26
|
|
|
213
|
-
export interface ActionMenuOption {
|
|
27
|
+
export interface ActionMenuOption extends HTMLAttributes<HTMLButtonElement> {
|
|
214
28
|
text: string
|
|
215
|
-
clickHandler
|
|
29
|
+
clickHandler?: VoidFunction
|
|
216
30
|
leftIcon?: IconProps
|
|
217
31
|
rightIcon?: IconProps
|
|
32
|
+
disabled?: boolean
|
|
33
|
+
actions?: ActionMenuOption[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DefaultTrigger: IconButtonMenuProps = {
|
|
37
|
+
type: 'icon',
|
|
38
|
+
iconProps: {
|
|
39
|
+
type: 'ellipsis-vertical',
|
|
40
|
+
size: 'large',
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ActionMenu: FC<IProps> = ({
|
|
45
|
+
id = 'action-menu-button',
|
|
46
|
+
actions,
|
|
47
|
+
disabled,
|
|
48
|
+
menuButtonOptions = { ...DefaultTrigger, disabled },
|
|
49
|
+
...rest
|
|
50
|
+
}) => {
|
|
51
|
+
const menuItemIdPrefix = id ? `${id}-` : ''
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Menu rootTrigger={menuButtonOptions} data-testid={id} {...rest}>
|
|
55
|
+
{actions.map((action, index) => renderAction(menuItemIdPrefix, action, index))}
|
|
56
|
+
</Menu>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const renderAction = (
|
|
61
|
+
idPrefix: string,
|
|
62
|
+
{ text, leftIcon, rightIcon, clickHandler, onClick, actions, ...rest }: ActionMenuOption,
|
|
63
|
+
index: number
|
|
64
|
+
): ReactNode => {
|
|
65
|
+
const textId = stringToHtmlId(text)
|
|
66
|
+
const actionMenuItemId = `${idPrefix}action-menu-item-${textId}-${index}`
|
|
67
|
+
|
|
68
|
+
if (!!actions?.length && actions.length > 0) {
|
|
69
|
+
return (
|
|
70
|
+
<Menu
|
|
71
|
+
key={actionMenuItemId}
|
|
72
|
+
id={actionMenuItemId}
|
|
73
|
+
data-testid={actionMenuItemId}
|
|
74
|
+
label={text}
|
|
75
|
+
leftIcon={leftIcon ? <Icon {...leftIcon} /> : null}
|
|
76
|
+
rightIcon={rightIcon ? <Icon {...rightIcon} /> : null}
|
|
77
|
+
{...rest}
|
|
78
|
+
>
|
|
79
|
+
{actions.map((action, index) => renderAction(actionMenuItemId, action, index))}
|
|
80
|
+
</Menu>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<MenuItem
|
|
86
|
+
key={actionMenuItemId}
|
|
87
|
+
id={actionMenuItemId}
|
|
88
|
+
data-testid={actionMenuItemId}
|
|
89
|
+
label={text}
|
|
90
|
+
leftIcon={leftIcon ? <Icon {...leftIcon} /> : null}
|
|
91
|
+
rightIcon={rightIcon ? <Icon {...rightIcon} /> : null}
|
|
92
|
+
onClick={clickHandler ?? onClick}
|
|
93
|
+
{...rest}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
218
96
|
}
|
|
219
97
|
|
|
220
98
|
export default ActionMenu
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { css } from '@emotion/react'
|
|
2
|
+
import styled from '@emotion/styled'
|
|
3
|
+
import { BTN_COLOURS, getZIndex, PopUp } from '@ltht-react/styles'
|
|
4
|
+
|
|
5
|
+
const nestedStyles = css`
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 0.25rem;
|
|
9
|
+
background: none;
|
|
10
|
+
width: 100%;
|
|
11
|
+
border: none;
|
|
12
|
+
text-align: left;
|
|
13
|
+
line-height: 1.8;
|
|
14
|
+
min-width: 80px;
|
|
15
|
+
margin: 0;
|
|
16
|
+
outline: 0;
|
|
17
|
+
|
|
18
|
+
&:focus {
|
|
19
|
+
background: ${BTN_COLOURS.PRIMARY.HOVER};
|
|
20
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&[data-nested][data-open]:not([data-focus-inside]) {
|
|
24
|
+
background: ${BTN_COLOURS.PRIMARY.HOVER};
|
|
25
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&[data-focus-inside][data-open] {
|
|
29
|
+
background: ${BTN_COLOURS.PRIMARY.HOVER};
|
|
30
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:not(:last-child) {
|
|
34
|
+
border-bottom: 1px solid #e0e6ef;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&:hover > div > svg {
|
|
38
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&[data-focus-inside][data-open] > div > svg {
|
|
42
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&[disabled] > div > svg {
|
|
46
|
+
color: rgba(16, 16, 16, 0.3);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&[disabled]:hover {
|
|
50
|
+
cursor: not-allowed;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&:hover {
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
export const RightIconWrapper = styled.div`
|
|
59
|
+
margin-left: auto;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
export const LeftIconWrapper = styled.div`
|
|
65
|
+
width: 20px;
|
|
66
|
+
display: flex;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
align-items: center;
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
export const TextWrapper = styled.span`
|
|
72
|
+
flex: 1;
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
const rootStyles = css`
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
align-content: center;
|
|
79
|
+
gap: 0.25rem;
|
|
80
|
+
padding: 6px 10px;
|
|
81
|
+
border: 0;
|
|
82
|
+
font-size: 0.9rem;
|
|
83
|
+
background: none;
|
|
84
|
+
border-radius: 6px;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
|
|
87
|
+
&[disabled]:hover {
|
|
88
|
+
background: none;
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&[disabled]:hover > div > svg {
|
|
93
|
+
color: ${BTN_COLOURS.PRIMARY.DISABLED};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&[data-open],
|
|
97
|
+
&:hover {
|
|
98
|
+
background: ${BTN_COLOURS.PRIMARY.HOVER};
|
|
99
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&[data-open] > div > svg,
|
|
103
|
+
&:focus > div > svg,
|
|
104
|
+
&:hover > div > svg {
|
|
105
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&[data-open] > svg,
|
|
109
|
+
&:focus > svg,
|
|
110
|
+
&:hover > svg {
|
|
111
|
+
color: ${BTN_COLOURS.PRIMARY.TEXT};
|
|
112
|
+
}
|
|
113
|
+
`
|
|
114
|
+
|
|
115
|
+
export const StyledRootMenu = styled.button<{ isNested?: boolean }>`
|
|
116
|
+
${({ isNested }) => (isNested ? nestedStyles : rootStyles)}
|
|
117
|
+
`
|
|
118
|
+
|
|
119
|
+
export const StyledMenu = styled.div`
|
|
120
|
+
background: rgba(255, 255, 255, 0.8);
|
|
121
|
+
-webkit-backdrop-filter: blur(10px);
|
|
122
|
+
backdrop-filter: blur(10px);
|
|
123
|
+
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.1);
|
|
124
|
+
outline: 0;
|
|
125
|
+
z-index: ${getZIndex(PopUp)};
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
export const StyledMenuItem = styled.button`
|
|
129
|
+
${nestedStyles}
|
|
130
|
+
`
|