@ltht-react/menu 2.0.192 → 2.0.194

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/README.md CHANGED
@@ -1,15 +1,15 @@
1
- # Input
2
-
3
- <!-- STORY -->
4
-
5
- ### Import
6
-
7
- ```js
8
- import Menu from '@ltht-react/menu'
9
- ```
10
-
11
- ### Usage
12
-
13
- ```jsx
14
- <Menu />
15
- ```
1
+ # Input
2
+
3
+ <!-- STORY -->
4
+
5
+ ### Import
6
+
7
+ ```js
8
+ import Menu from '@ltht-react/menu'
9
+ ```
10
+
11
+ ### Usage
12
+
13
+ ```jsx
14
+ <Menu />
15
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ltht-react/menu",
3
- "version": "2.0.192",
3
+ "version": "2.0.194",
4
4
  "description": "ltht-react styled Menu component.",
5
5
  "author": "LTHT",
6
6
  "homepage": "",
@@ -29,12 +29,12 @@
29
29
  "@emotion/react": "^11.0.0",
30
30
  "@emotion/styled": "^11.0.0",
31
31
  "@floating-ui/react": "^0.27.15",
32
- "@ltht-react/button": "^2.0.192",
33
- "@ltht-react/hooks": "^2.0.192",
34
- "@ltht-react/icon": "^2.0.192",
35
- "@ltht-react/styles": "^2.0.192",
36
- "@ltht-react/utils": "^2.0.192",
32
+ "@ltht-react/button": "^2.0.194",
33
+ "@ltht-react/hooks": "^2.0.194",
34
+ "@ltht-react/icon": "^2.0.194",
35
+ "@ltht-react/styles": "^2.0.194",
36
+ "@ltht-react/utils": "^2.0.194",
37
37
  "react": "^18.2.0"
38
38
  },
39
- "gitHead": "d97c241ba79c5e28edb4dec124e156cda6e9af66"
39
+ "gitHead": "2eb4b88fa2bc19baa1be9433dbe29fe1a947dc62"
40
40
  }
package/src/index.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import ActionMenu, { ActionMenuOption, DefaultTrigger } from './molecules/action-menu'
2
- import { Menu, MenuItem } from './molecules/menu'
3
-
4
- export default ActionMenu
5
- export { Menu, MenuItem }
6
- export { ActionMenuOption, DefaultTrigger }
1
+ import ActionMenu, { ActionMenuOption, DefaultTrigger } from './molecules/action-menu'
2
+ import { Menu, MenuItem } from './molecules/menu'
3
+
4
+ export default ActionMenu
5
+ export { Menu, MenuItem }
6
+ export { ActionMenuOption, DefaultTrigger }
@@ -1,103 +1,103 @@
1
- import { useMemo, MouseEvent, FC, useCallback } from 'react'
2
- import Icon from '@ltht-react/icon'
3
- import { stringToHtmlId } from '@ltht-react/utils'
4
- import { useFullScreen } from '@ltht-react/hooks'
5
- import { ActionMenuOption as IActionMenuOption } from './action-menu'
6
- import { Menu, MenuItem } from './menu'
7
-
8
- interface Props extends IActionMenuOption {
9
- idPrefix: string
10
- index: number
11
- }
12
-
13
- // Utility function to generate consistent IDs
14
- const generateActionId = (idPrefix: string, text: string, index: number): string => {
15
- const textId = stringToHtmlId(text)
16
- return `${idPrefix}action-menu-item-${textId}-${index}`
17
- }
18
-
19
- const ActionMenuOption: FC<Props> = ({
20
- idPrefix,
21
- index,
22
- text,
23
- leftIcon,
24
- rightIcon,
25
- actions,
26
- exitFullScreenOnClick = false,
27
- clickHandler,
28
- onClick,
29
- disabled = false,
30
- ...rest
31
- }) => {
32
- const { isFullscreen, exitFullScreen } = useFullScreen()
33
-
34
- // Memoize the action ID to prevent recalculation on every render
35
- const actionMenuItemId = useMemo(() => generateActionId(idPrefix, text, index), [idPrefix, text, index])
36
-
37
- // Memoize icons to prevent unnecessary re-renders
38
- const leftIconElement = useMemo(() => leftIcon && <Icon {...leftIcon} />, [leftIcon])
39
-
40
- const rightIconElement = useMemo(() => rightIcon && <Icon {...rightIcon} />, [rightIcon])
41
-
42
- const handleOnClick = useCallback(
43
- async (e: MouseEvent<HTMLButtonElement>) => {
44
- e.stopPropagation()
45
-
46
- if (disabled) {
47
- return
48
- }
49
-
50
- if (!clickHandler && !onClick) {
51
- return
52
- }
53
-
54
- if (exitFullScreenOnClick && isFullscreen) {
55
- await exitFullScreen()
56
- }
57
-
58
- // Execute the appropriate handler
59
- if (clickHandler) {
60
- clickHandler()
61
- } else if (onClick) {
62
- onClick(e)
63
- }
64
- },
65
- [disabled, clickHandler, onClick, exitFullScreenOnClick, isFullscreen, exitFullScreen]
66
- )
67
-
68
- // Determine if this is a submenu (has actions)
69
- const isSubmenu = actions && actions.length > 0
70
-
71
- // Common props for both Menu and MenuItem
72
- const commonProps = useMemo(
73
- () => ({
74
- id: actionMenuItemId,
75
- 'data-testid': actionMenuItemId,
76
- label: text,
77
- leftIcon: leftIconElement,
78
- rightIcon: rightIconElement,
79
- disabled,
80
- ...rest,
81
- }),
82
- [actionMenuItemId, text, leftIconElement, rightIconElement, disabled, rest]
83
- )
84
-
85
- if (isSubmenu) {
86
- return (
87
- <Menu {...commonProps} aria-label={`${text} submenu`} aria-expanded="false">
88
- {actions.map((action, actionIndex) => (
89
- <ActionMenuOption
90
- key={`${actionMenuItemId}_menu_item_${actionIndex}`}
91
- idPrefix={`${actionMenuItemId}_`}
92
- index={actionIndex}
93
- {...action}
94
- />
95
- ))}
96
- </Menu>
97
- )
98
- }
99
-
100
- return <MenuItem {...commonProps} onClick={handleOnClick} aria-label={text} />
101
- }
102
-
103
- export default ActionMenuOption
1
+ import { useMemo, MouseEvent, FC, useCallback } from 'react'
2
+ import Icon from '@ltht-react/icon'
3
+ import { stringToHtmlId } from '@ltht-react/utils'
4
+ import { useFullScreen } from '@ltht-react/hooks'
5
+ import { ActionMenuOption as IActionMenuOption } from './action-menu'
6
+ import { Menu, MenuItem } from './menu'
7
+
8
+ interface Props extends IActionMenuOption {
9
+ idPrefix: string
10
+ index: number
11
+ }
12
+
13
+ // Utility function to generate consistent IDs
14
+ const generateActionId = (idPrefix: string, text: string, index: number): string => {
15
+ const textId = stringToHtmlId(text)
16
+ return `${idPrefix}action-menu-item-${textId}-${index}`
17
+ }
18
+
19
+ const ActionMenuOption: FC<Props> = ({
20
+ idPrefix,
21
+ index,
22
+ text,
23
+ leftIcon,
24
+ rightIcon,
25
+ actions,
26
+ exitFullScreenOnClick = false,
27
+ clickHandler,
28
+ onClick,
29
+ disabled = false,
30
+ ...rest
31
+ }) => {
32
+ const { isFullscreen, exitFullScreen } = useFullScreen()
33
+
34
+ // Memoize the action ID to prevent recalculation on every render
35
+ const actionMenuItemId = useMemo(() => generateActionId(idPrefix, text, index), [idPrefix, text, index])
36
+
37
+ // Memoize icons to prevent unnecessary re-renders
38
+ const leftIconElement = useMemo(() => leftIcon && <Icon {...leftIcon} />, [leftIcon])
39
+
40
+ const rightIconElement = useMemo(() => rightIcon && <Icon {...rightIcon} />, [rightIcon])
41
+
42
+ const handleOnClick = useCallback(
43
+ async (e: MouseEvent<HTMLButtonElement>) => {
44
+ e.stopPropagation()
45
+
46
+ if (disabled) {
47
+ return
48
+ }
49
+
50
+ if (!clickHandler && !onClick) {
51
+ return
52
+ }
53
+
54
+ if (exitFullScreenOnClick && isFullscreen) {
55
+ await exitFullScreen()
56
+ }
57
+
58
+ // Execute the appropriate handler
59
+ if (clickHandler) {
60
+ clickHandler()
61
+ } else if (onClick) {
62
+ onClick(e)
63
+ }
64
+ },
65
+ [disabled, clickHandler, onClick, exitFullScreenOnClick, isFullscreen, exitFullScreen]
66
+ )
67
+
68
+ // Determine if this is a submenu (has actions)
69
+ const isSubmenu = actions && actions.length > 0
70
+
71
+ // Common props for both Menu and MenuItem
72
+ const commonProps = useMemo(
73
+ () => ({
74
+ id: actionMenuItemId,
75
+ 'data-testid': actionMenuItemId,
76
+ label: text,
77
+ leftIcon: leftIconElement,
78
+ rightIcon: rightIconElement,
79
+ disabled,
80
+ ...rest,
81
+ }),
82
+ [actionMenuItemId, text, leftIconElement, rightIconElement, disabled, rest]
83
+ )
84
+
85
+ if (isSubmenu) {
86
+ return (
87
+ <Menu {...commonProps} aria-label={`${text} submenu`} aria-expanded="false">
88
+ {actions.map((action, actionIndex) => (
89
+ <ActionMenuOption
90
+ key={`${actionMenuItemId}_menu_item_${actionIndex}`}
91
+ idPrefix={`${actionMenuItemId}_`}
92
+ index={actionIndex}
93
+ {...action}
94
+ />
95
+ ))}
96
+ </Menu>
97
+ )
98
+ }
99
+
100
+ return <MenuItem {...commonProps} onClick={handleOnClick} aria-label={text} />
101
+ }
102
+
103
+ export default ActionMenuOption
@@ -1,69 +1,69 @@
1
- import { FC, HTMLAttributes } from 'react'
2
- import { IconProps } from '@ltht-react/icon'
3
- import { ButtonProps } from '@ltht-react/button'
4
-
5
- import { Menu } from './menu'
6
- import MenuOption from './action-menu-option'
7
-
8
- interface IProps<T extends HTMLElement = HTMLElement> extends HTMLAttributes<HTMLButtonElement> {
9
- actions: ActionMenuOption[]
10
- menuButtonOptions?: IconButtonMenuProps | ButtonMenuProps
11
- disabled?: boolean
12
- root?: React.MutableRefObject<T | null>
13
- }
14
-
15
- interface IconButtonMenuProps {
16
- type: 'icon'
17
- iconProps: IconProps
18
- text?: string
19
- disabled?: boolean
20
- }
21
-
22
- interface ButtonMenuProps {
23
- type: 'button'
24
- buttonProps: ButtonProps
25
- text: string
26
- }
27
-
28
- export interface ActionMenuOption extends HTMLAttributes<HTMLButtonElement> {
29
- text: string
30
- clickHandler?: VoidFunction
31
- leftIcon?: IconProps
32
- rightIcon?: IconProps
33
- disabled?: boolean
34
- exitFullScreenOnClick?: boolean
35
- actions?: ActionMenuOption[]
36
- }
37
-
38
- export const DefaultTrigger: IconButtonMenuProps = {
39
- type: 'icon',
40
- iconProps: {
41
- type: 'ellipsis-vertical',
42
- size: 'large',
43
- },
44
- }
45
-
46
- const ActionMenu: FC<IProps> = ({
47
- id = 'action-menu-button',
48
- actions,
49
- disabled,
50
- menuButtonOptions = { ...DefaultTrigger, disabled },
51
- ...rest
52
- }) => {
53
- const menuItemIdPrefix = id ? `${id}-` : ''
54
-
55
- return (
56
- <Menu rootTrigger={menuButtonOptions} data-testid={id} {...rest}>
57
- {actions.map((action, index) => (
58
- <MenuOption
59
- key={`${menuItemIdPrefix}_menu_item_${index}`}
60
- idPrefix={menuItemIdPrefix}
61
- index={index}
62
- {...action}
63
- />
64
- ))}
65
- </Menu>
66
- )
67
- }
68
-
69
- export default ActionMenu
1
+ import { FC, HTMLAttributes } from 'react'
2
+ import { IconProps } from '@ltht-react/icon'
3
+ import { ButtonProps } from '@ltht-react/button'
4
+
5
+ import { Menu } from './menu'
6
+ import MenuOption from './action-menu-option'
7
+
8
+ interface IProps<T extends HTMLElement = HTMLElement> extends HTMLAttributes<HTMLButtonElement> {
9
+ actions: ActionMenuOption[]
10
+ menuButtonOptions?: IconButtonMenuProps | ButtonMenuProps
11
+ disabled?: boolean
12
+ root?: React.MutableRefObject<T | null>
13
+ }
14
+
15
+ interface IconButtonMenuProps {
16
+ type: 'icon'
17
+ iconProps: IconProps
18
+ text?: string
19
+ disabled?: boolean
20
+ }
21
+
22
+ interface ButtonMenuProps {
23
+ type: 'button'
24
+ buttonProps: ButtonProps
25
+ text: string
26
+ }
27
+
28
+ export interface ActionMenuOption extends HTMLAttributes<HTMLButtonElement> {
29
+ text: string
30
+ clickHandler?: VoidFunction
31
+ leftIcon?: IconProps
32
+ rightIcon?: IconProps
33
+ disabled?: boolean
34
+ exitFullScreenOnClick?: boolean
35
+ actions?: ActionMenuOption[]
36
+ }
37
+
38
+ export const DefaultTrigger: IconButtonMenuProps = {
39
+ type: 'icon',
40
+ iconProps: {
41
+ type: 'ellipsis-vertical',
42
+ size: 'large',
43
+ },
44
+ }
45
+
46
+ const ActionMenu: FC<IProps> = ({
47
+ id = 'action-menu-button',
48
+ actions,
49
+ disabled,
50
+ menuButtonOptions = { ...DefaultTrigger, disabled },
51
+ ...rest
52
+ }) => {
53
+ const menuItemIdPrefix = id ? `${id}-` : ''
54
+
55
+ return (
56
+ <Menu rootTrigger={menuButtonOptions} data-testid={id} {...rest}>
57
+ {actions.map((action, index) => (
58
+ <MenuOption
59
+ key={`${menuItemIdPrefix}_menu_item_${index}`}
60
+ idPrefix={menuItemIdPrefix}
61
+ index={index}
62
+ {...action}
63
+ />
64
+ ))}
65
+ </Menu>
66
+ )
67
+ }
68
+
69
+ export default ActionMenu
@@ -1,130 +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
- padding: 0.2rem 0.3rem;
74
- `
75
-
76
- const rootStyles = css`
77
- display: flex;
78
- align-items: center;
79
- align-content: center;
80
- gap: 0.25rem;
81
- padding: 6px 10px;
82
- border: 0;
83
- font-size: 0.9rem;
84
- background: none;
85
- border-radius: 6px;
86
- cursor: pointer;
87
-
88
- &[disabled]:hover {
89
- background: none;
90
- cursor: not-allowed;
91
- }
92
-
93
- &[disabled]:hover > div > svg {
94
- color: ${BTN_COLOURS.PRIMARY.DISABLED};
95
- }
96
-
97
- &[data-open],
98
- &:hover {
99
- background: ${BTN_COLOURS.PRIMARY.HOVER};
100
- color: ${BTN_COLOURS.PRIMARY.TEXT};
101
- }
102
-
103
- &[data-open] > div > svg,
104
- &:focus > div > svg,
105
- &:hover > div > svg {
106
- color: ${BTN_COLOURS.PRIMARY.TEXT};
107
- }
108
-
109
- &[data-open] > 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
- `
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
+ padding: 0.2rem 0.3rem;
74
+ `
75
+
76
+ const rootStyles = css`
77
+ display: flex;
78
+ align-items: center;
79
+ align-content: center;
80
+ gap: 0.25rem;
81
+ padding: 6px 10px;
82
+ border: 0;
83
+ font-size: 0.9rem;
84
+ background: none;
85
+ border-radius: 6px;
86
+ cursor: pointer;
87
+
88
+ &[disabled]:hover {
89
+ background: none;
90
+ cursor: not-allowed;
91
+ }
92
+
93
+ &[disabled]:hover > div > svg {
94
+ color: ${BTN_COLOURS.PRIMARY.DISABLED};
95
+ }
96
+
97
+ &[data-open],
98
+ &:hover {
99
+ background: ${BTN_COLOURS.PRIMARY.HOVER};
100
+ color: ${BTN_COLOURS.PRIMARY.TEXT};
101
+ }
102
+
103
+ &[data-open] > div > svg,
104
+ &:focus > div > svg,
105
+ &:hover > div > svg {
106
+ color: ${BTN_COLOURS.PRIMARY.TEXT};
107
+ }
108
+
109
+ &[data-open] > 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
+ `