@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.
@@ -1,207 +1,21 @@
1
- import styled from '@emotion/styled'
2
- import Button, { ButtonProps } from '@ltht-react/button/lib/atoms/button'
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
- const defaultMenuButtonProps: IconButtonMenuProps = {
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
- popupStyle?: React.CSSProperties
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: () => void
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
+ `