@stack-spot/portal-layout 0.0.26 → 0.0.28
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/dist/components/PortalSwitcher.d.ts.map +1 -1
- package/dist/components/PortalSwitcher.js +77 -49
- package/dist/components/PortalSwitcher.js.map +1 -1
- package/dist/components/SelectionList.d.ts.map +1 -1
- package/dist/components/SelectionList.js +5 -58
- package/dist/components/SelectionList.js.map +1 -1
- package/dist/components/menu/MenuContent.d.ts.map +1 -1
- package/dist/components/menu/MenuContent.js +17 -5
- package/dist/components/menu/MenuContent.js.map +1 -1
- package/dist/components/menu/MenuSections.d.ts.map +1 -1
- package/dist/components/menu/MenuSections.js +37 -9
- package/dist/components/menu/MenuSections.js.map +1 -1
- package/dist/components/menu/PageSelector.d.ts.map +1 -1
- package/dist/components/menu/PageSelector.js +7 -2
- package/dist/components/menu/PageSelector.js.map +1 -1
- package/dist/components/menu/types.d.ts +8 -0
- package/dist/components/menu/types.d.ts.map +1 -1
- package/dist/components/menu/use-check-text-overflow.d.ts +6 -0
- package/dist/components/menu/use-check-text-overflow.d.ts.map +1 -0
- package/dist/components/menu/use-check-text-overflow.js +20 -0
- package/dist/components/menu/use-check-text-overflow.js.map +1 -0
- package/dist/components/menu/use-keyboard-controls.d.ts +10 -0
- package/dist/components/menu/use-keyboard-controls.d.ts.map +1 -0
- package/dist/components/menu/use-keyboard-controls.js +74 -0
- package/dist/components/menu/use-keyboard-controls.js.map +1 -0
- package/dist/components/menu/useCheckTextOverflow.js.map +1 -1
- package/dist/components/types.d.ts +7 -2
- package/dist/components/types.d.ts.map +1 -1
- package/dist/layout.css +27 -6
- package/package.json +5 -5
- package/src/components/PortalSwitcher.tsx +84 -69
- package/src/components/SelectionList.tsx +7 -62
- package/src/components/menu/MenuContent.tsx +18 -5
- package/src/components/menu/MenuSections.tsx +99 -35
- package/src/components/menu/PageSelector.tsx +10 -5
- package/src/components/menu/types.ts +8 -0
- package/src/components/menu/{useCheckTextOverflow.tsx → use-check-text-overflow.tsx} +2 -2
- package/src/components/menu/use-keyboard-controls.tsx +88 -0
- package/src/components/types.ts +6 -2
- package/src/layout.css +27 -6
|
@@ -9,7 +9,7 @@ import { hideOverlayImmediately } from './MenuSections'
|
|
|
9
9
|
import { PageSelector } from './PageSelector'
|
|
10
10
|
import { MENU_CONTENT_ITEM_PADDING as ITEM_PADDING, MENU_CONTENT_PADDING as PADDING } from './constants'
|
|
11
11
|
import { ItemGroup, MenuAction, MenuItem, MenuSectionContent } from './types'
|
|
12
|
-
import { useCheckTextOverflow } from './
|
|
12
|
+
import { useCheckTextOverflow } from './use-check-text-overflow'
|
|
13
13
|
|
|
14
14
|
const BackLink = styled.a`
|
|
15
15
|
display: flex;
|
|
@@ -69,7 +69,7 @@ export const MenuGroup = styled.ul`
|
|
|
69
69
|
left: 2px;
|
|
70
70
|
width: 2px;
|
|
71
71
|
height: 0;
|
|
72
|
-
background:
|
|
72
|
+
background: inherit;
|
|
73
73
|
border-radius: 50%;
|
|
74
74
|
transition: height 0.2s;
|
|
75
75
|
}
|
|
@@ -82,9 +82,15 @@ export const MenuGroup = styled.ul`
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
&:before {
|
|
85
|
+
background: ${theme.color.primary['500']};
|
|
85
86
|
height: 24px;
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
|
|
90
|
+
&:not(.active):hover:before {
|
|
91
|
+
background: ${theme.color.light.contrastText};
|
|
92
|
+
height: 24px;
|
|
93
|
+
}
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
.chevron {
|
|
@@ -135,6 +141,7 @@ export const Title = styled.header`
|
|
|
135
141
|
|
|
136
142
|
export const ActionItem = ({ label, onClick, href, active, icon, badge, overflow = 'wrap' }: MenuAction) => {
|
|
137
143
|
const { ref, overflow: textOverflow } = useCheckTextOverflow()
|
|
144
|
+
const isTextLabel = typeof label === 'string'
|
|
138
145
|
return (
|
|
139
146
|
<a
|
|
140
147
|
href={active ? undefined : href}
|
|
@@ -145,9 +152,12 @@ export const ActionItem = ({ label, onClick, href, active, icon, badge, overflow
|
|
|
145
152
|
}}
|
|
146
153
|
className={listToClass(['action', 'item-row', active ? 'active' : undefined])}
|
|
147
154
|
{...(active ? { 'aria-current': 'page' } : undefined)}
|
|
155
|
+
{...(!href ? { 'tabIndex': 0 } : undefined)}
|
|
148
156
|
>
|
|
149
157
|
{icon}
|
|
150
|
-
|
|
158
|
+
{isTextLabel ?
|
|
159
|
+
<Text ref={ref} appearance="body2" className={`label ${overflow}`} title={textOverflow ? label : ''}>{label}</Text> :
|
|
160
|
+
label.element}
|
|
151
161
|
{badge}
|
|
152
162
|
</a>
|
|
153
163
|
)
|
|
@@ -198,7 +208,8 @@ const GroupItem = ({ root, ...item }: ItemGroup & { root?: boolean }) => (
|
|
|
198
208
|
)
|
|
199
209
|
|
|
200
210
|
function renderOption({ root, ...option }: MenuItem & { root?: boolean }) {
|
|
201
|
-
|
|
211
|
+
const labelText = typeof option.label === 'string' ? option.label : option.label.id
|
|
212
|
+
return <li key={labelText}>{'children' in option ? <GroupItem root={root} {...option} /> : <ActionItem {...option} />}</li>
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
export const MenuContent = ({ pageSelector, goBack, title, subtitle, afterTitle, options = [], loading, error }: MenuSectionContent) => {
|
|
@@ -223,7 +234,9 @@ export const MenuContent = ({ pageSelector, goBack, title, subtitle, afterTitle,
|
|
|
223
234
|
<IconBox colorScheme="inverse" size="sm">
|
|
224
235
|
<ArrowLeft />
|
|
225
236
|
</IconBox>
|
|
226
|
-
|
|
237
|
+
{typeof goBack?.label === 'string' ?
|
|
238
|
+
<Text appearance="body2" nowrapEllipsis>{goBack.label}</Text> :
|
|
239
|
+
goBack.label.element}
|
|
227
240
|
</BackLink>
|
|
228
241
|
)}
|
|
229
242
|
{title && (
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
1
|
+
import { Flex, IconBox, Text } from '@citric/core'
|
|
2
|
+
import { ChevronRight, Cog, Collapse, Menu as MenuIcon } from '@citric/icons'
|
|
3
|
+
import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
|
|
5
4
|
import { useCallback, useMemo, useState } from 'react'
|
|
6
5
|
import { MenuContent } from './MenuContent'
|
|
7
6
|
import { MenuProps, MenuSection } from './types'
|
|
7
|
+
import { useKeyboardControls } from './use-keyboard-controls'
|
|
8
8
|
|
|
9
9
|
const ARROW_HEIGHT = 24
|
|
10
10
|
const HIDE_OVERLAY_DELAY_MS = 400
|
|
@@ -50,9 +50,12 @@ const Section = ({
|
|
|
50
50
|
setCurrentOverlay,
|
|
51
51
|
id,
|
|
52
52
|
hasContent,
|
|
53
|
-
|
|
53
|
+
className,
|
|
54
|
+
}: MenuSection & {
|
|
55
|
+
id: number, setCurrentOverlay: (id: number | undefined) => void, hasContent: boolean,
|
|
56
|
+
}) => {
|
|
54
57
|
const contentToRender = typeof content === 'function' ? content() : content
|
|
55
|
-
|
|
58
|
+
const t = useTranslate(dictionary)
|
|
56
59
|
function shouldShowOverlay() {
|
|
57
60
|
/* The overlay should appear if:
|
|
58
61
|
* 1. The section has some content to render OR:
|
|
@@ -63,7 +66,7 @@ const Section = ({
|
|
|
63
66
|
return (!!contentToRender || !!customContent || (hasContent && active)) && (!active || !isMenuContentVisible())
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
function showOverlayAndFixArrowPosition(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
|
|
69
|
+
function showOverlayAndFixArrowPosition(event: React.MouseEvent<HTMLAnchorElement, MouseEvent> | React.KeyboardEvent<any>) {
|
|
67
70
|
if (!shouldShowOverlay()) return
|
|
68
71
|
onOpen?.()
|
|
69
72
|
const rect = (event.target as HTMLElement)?.getBoundingClientRect()
|
|
@@ -80,23 +83,45 @@ const Section = ({
|
|
|
80
83
|
hideOverlayImmediately()
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
const labelText = typeof label === 'string' ? label : label.id
|
|
87
|
+
|
|
83
88
|
return (
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
<>
|
|
90
|
+
<li key={labelText} title={labelText} className={`section-submenu ${className || ''} ${active ? 'active' : ''}`}>
|
|
91
|
+
<a
|
|
92
|
+
href={href}
|
|
93
|
+
target={target}
|
|
94
|
+
onClick={click}
|
|
95
|
+
onMouseEnter={showOverlayAndFixArrowPosition}
|
|
96
|
+
onMouseLeave={() => shouldShowOverlay() && hideOverlay()}
|
|
97
|
+
title={labelText}
|
|
98
|
+
aria-label={labelText}
|
|
99
|
+
onKeyDown={onClick ? e => e.key === 'Enter' && onClick() : undefined}
|
|
100
|
+
{...(active ? { 'aria-current': 'page' } : undefined)}
|
|
101
|
+
{...(!href ? { 'tabIndex': 0 } : undefined)}
|
|
102
|
+
>
|
|
103
|
+
<Flex alignItems="center" justifyContent="center" px={5}>
|
|
104
|
+
{icon}
|
|
105
|
+
{typeof label === 'string' ? <Text appearance="body2" className="section-label" ml={2}>{label}</Text> : label.element}
|
|
106
|
+
</Flex>
|
|
107
|
+
</a>
|
|
108
|
+
{shouldShowOverlay() &&
|
|
109
|
+
<IconBox size="sm" className="section-submenu-icon"
|
|
110
|
+
as="button"
|
|
111
|
+
aria-label={interpolate(t.menuOptions, label)}
|
|
112
|
+
aria-controls={MENU_OVERLAY_ID}
|
|
113
|
+
aria-expanded={document.getElementById(MENU_OVERLAY_ID)?.classList.contains('visible')}
|
|
114
|
+
onKeyDown={(event) => {
|
|
115
|
+
if (event.key === 'Enter') {
|
|
116
|
+
showOverlayAndFixArrowPosition(event)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}>
|
|
120
|
+
<ChevronRight />
|
|
121
|
+
</IconBox>
|
|
122
|
+
}
|
|
123
|
+
</li>
|
|
124
|
+
</>
|
|
100
125
|
)
|
|
101
126
|
}
|
|
102
127
|
|
|
@@ -104,15 +129,17 @@ const OverlayRenderer = ({ content, customContent }: Pick<MenuSection, 'content'
|
|
|
104
129
|
if (customContent) {
|
|
105
130
|
return <> {customContent} </>
|
|
106
131
|
}
|
|
107
|
-
|
|
132
|
+
|
|
108
133
|
const data = typeof content === 'function' ? content() : content
|
|
109
|
-
return <
|
|
134
|
+
return <MenuContent {...data} />
|
|
110
135
|
}
|
|
111
136
|
|
|
112
137
|
export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
113
138
|
const t = useTranslate(dictionary)
|
|
114
139
|
// this is a mock state only used to force an update on the component.
|
|
115
140
|
const [_, setUpdate] = useState(0)
|
|
141
|
+
const onHide = () => hideOverlay()
|
|
142
|
+
|
|
116
143
|
const toggleMenu = useCallback(() => {
|
|
117
144
|
const layout = document.getElementById('layout')
|
|
118
145
|
if (!layout) return
|
|
@@ -132,6 +159,20 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
132
159
|
[sections],
|
|
133
160
|
)
|
|
134
161
|
|
|
162
|
+
function onPressEscape() {
|
|
163
|
+
hideOverlay()
|
|
164
|
+
const items = document.getElementsByClassName('section-submenu')
|
|
165
|
+
if (!!items && !!currentOverlay && items.length > currentOverlay && items[currentOverlay].children.length > 1) {
|
|
166
|
+
(items[currentOverlay].children[1] as HTMLElement).focus()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const wrapper = useKeyboardControls({
|
|
171
|
+
onHide, visible: document.getElementById(MENU_OVERLAY_ID)?.classList.contains('visible') || false,
|
|
172
|
+
querySelectors: 'li a.action, #custom-selectable-item button, #custom-selectable-item input',
|
|
173
|
+
onPressEscape: () => onPressEscape(),
|
|
174
|
+
})
|
|
175
|
+
|
|
135
176
|
/* This function renders the section preview in the overlay in normal circumstances. If the menu is hidden and the section is active,
|
|
136
177
|
instead of rendering the section preview, it will render the actual menu content, which would be invisible otherwise.
|
|
137
178
|
Below, the key is of extreme importance. It ensures React will consider every section content to be an entirely different
|
|
@@ -139,26 +180,43 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
139
180
|
hook, this would cause some serious problems. */
|
|
140
181
|
function renderMenuOverlay() {
|
|
141
182
|
if (currentOverlay === undefined) return null
|
|
142
|
-
const shouldRenderMenuContentInstead = !isMenuContentVisible() && sections[currentOverlay].active &&
|
|
183
|
+
const shouldRenderMenuContentInstead = !isMenuContentVisible() && sections[currentOverlay].active &&
|
|
143
184
|
(!!props.content || !!props.customContent)
|
|
144
185
|
return shouldRenderMenuContentInstead
|
|
145
186
|
? <OverlayRenderer key={'contentKey' in props ? props.contentKey : undefined} content={props.content}
|
|
146
187
|
customContent={props.customContent} />
|
|
147
|
-
: <OverlayRenderer key={currentOverlay} content={sections[currentOverlay].content}
|
|
188
|
+
: <OverlayRenderer key={currentOverlay} content={sections[currentOverlay].content}
|
|
148
189
|
customContent={sections[currentOverlay].customContent} />
|
|
149
190
|
}
|
|
150
191
|
|
|
151
192
|
return (
|
|
152
193
|
<>
|
|
153
194
|
<ul>{sectionItems}</ul>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
195
|
+
|
|
196
|
+
{(!!props.content || !!props.customContent || props.settings?.show) &&
|
|
197
|
+
<Flex mb={7} alignItems="center" justifyContent="center">
|
|
198
|
+
{(!!props.content || !!props.customContent) &&
|
|
199
|
+
<button className="toggle sections-footer" onClick={toggleMenu} title={t.toggle} tabIndex={-1} aria-hidden>
|
|
200
|
+
<IconBox>
|
|
201
|
+
<MenuIcon className="expand" />
|
|
202
|
+
<Collapse className="collapse" />
|
|
203
|
+
</IconBox>
|
|
204
|
+
</button>}
|
|
205
|
+
{(props.settings?.show) &&
|
|
206
|
+
<a href={props.settings?.href} onClick={props.settings?.onClick}
|
|
207
|
+
className="sections-footer"
|
|
208
|
+
{...(props.settings.active ? { 'aria-current': 'page' } : undefined)}>
|
|
209
|
+
<Flex alignItems="center" justifyContent="center">
|
|
210
|
+
<IconBox aria-label={t.settingsIcon}>
|
|
211
|
+
<Cog />
|
|
212
|
+
</IconBox>
|
|
213
|
+
<Text appearance="body2" ml={2}>{t.settings}</Text>
|
|
214
|
+
</Flex>
|
|
215
|
+
</a>
|
|
216
|
+
}
|
|
217
|
+
</Flex>
|
|
218
|
+
}
|
|
219
|
+
<div id={MENU_OVERLAY_ID} onMouseEnter={showOverlay} onMouseLeave={hideOverlay} ref={wrapper}>
|
|
162
220
|
{renderMenuOverlay()}
|
|
163
221
|
<div className="arrow"></div>
|
|
164
222
|
</div>
|
|
@@ -169,8 +227,14 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
169
227
|
const dictionary = {
|
|
170
228
|
en: {
|
|
171
229
|
toggle: 'Show or hide the menu',
|
|
230
|
+
menuOptions: 'View $0 menu options',
|
|
231
|
+
settings: 'Settings',
|
|
232
|
+
settingsIcon: 'Settings icon',
|
|
172
233
|
},
|
|
173
234
|
pt: {
|
|
174
235
|
toggle: 'Visualizar ou esconder o menu',
|
|
236
|
+
menuOptions: 'Visualizar opções do menu $0',
|
|
237
|
+
settings: 'Configurações',
|
|
238
|
+
settingsIcon: 'Icone de configurações',
|
|
175
239
|
},
|
|
176
240
|
} satisfies Dictionary
|
|
@@ -8,7 +8,7 @@ import { styled } from 'styled-components'
|
|
|
8
8
|
import { ListAction, SelectionList } from '../SelectionList'
|
|
9
9
|
import { MENU_CONTENT_PADDING as PADDING } from './constants'
|
|
10
10
|
import { Selector } from './types'
|
|
11
|
-
import { useCheckTextOverflow } from './
|
|
11
|
+
import { useCheckTextOverflow } from './use-check-text-overflow'
|
|
12
12
|
|
|
13
13
|
const SelectorBox = styled.div`
|
|
14
14
|
position: relative;
|
|
@@ -96,13 +96,16 @@ export const PageSelector = ({ options, value, button, loading, title }: Selecto
|
|
|
96
96
|
},
|
|
97
97
|
[options, value, button],
|
|
98
98
|
)
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
const label = selected?.label ?? button?.label ?? value
|
|
101
|
+
const isTextLabel = typeof label == 'string'
|
|
102
|
+
const labelText = typeof label === 'string' ? label : label.id
|
|
103
|
+
const buttonLabelText = typeof button?.label == 'string' ? button?.label : button?.label.id
|
|
101
104
|
|
|
102
105
|
return (
|
|
103
106
|
<SelectorBox>
|
|
104
107
|
{loading
|
|
105
|
-
? <LoadingCircular />
|
|
108
|
+
? <LoadingCircular />
|
|
106
109
|
: (
|
|
107
110
|
<>
|
|
108
111
|
{title && <Text colorScheme="light.700" sx={{ mb: 3 }}>{title}</Text>}
|
|
@@ -116,7 +119,9 @@ export const PageSelector = ({ options, value, button, loading, title }: Selecto
|
|
|
116
119
|
aria-controls={id.current}
|
|
117
120
|
>
|
|
118
121
|
{selected?.icon && <IconBox>{selected?.icon}</IconBox>}
|
|
119
|
-
|
|
122
|
+
{isTextLabel ?
|
|
123
|
+
<Text ref={ref} appearance="body2" className="label" title={overflow ? labelText : ''}>{labelText}</Text> :
|
|
124
|
+
label.element}
|
|
120
125
|
<IconBox size="xs"><Select /></IconBox>
|
|
121
126
|
</a>
|
|
122
127
|
|
|
@@ -125,7 +130,7 @@ export const PageSelector = ({ options, value, button, loading, title }: Selecto
|
|
|
125
130
|
visible={visible}
|
|
126
131
|
items={optionsWithIcon}
|
|
127
132
|
onHide={() => setVisible(false)}
|
|
128
|
-
after={button ? <a className="view-all" href={button.href} onClick={button.onClick}>{
|
|
133
|
+
after={button ? <a className="view-all" href={button.href} onClick={button.onClick}>{buttonLabelText}</a> : undefined}
|
|
129
134
|
scroll
|
|
130
135
|
/>
|
|
131
136
|
</>
|
|
@@ -76,12 +76,20 @@ export interface MenuSection extends Action {
|
|
|
76
76
|
customContent?: ReactNode,
|
|
77
77
|
active?: boolean,
|
|
78
78
|
onOpen?: () => void,
|
|
79
|
+
className?: string,
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
interface BaseMenuProps {
|
|
82
83
|
sections?: MenuSection[],
|
|
83
84
|
compact?: boolean,
|
|
84
85
|
customContent?: ReactNode,
|
|
86
|
+
settings?: {
|
|
87
|
+
show?: boolean,
|
|
88
|
+
onClick?: () => void,
|
|
89
|
+
href?: string,
|
|
90
|
+
active?: boolean,
|
|
91
|
+
className?: string,
|
|
92
|
+
},
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
export interface MenuPropsWithStaticContent extends BaseMenuProps {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react'
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
2
|
|
|
3
3
|
export function useCheckTextOverflow() {
|
|
4
4
|
const [overflow, setOverflow] = useState<boolean>(false)
|
|
@@ -23,4 +23,4 @@ export function useCheckTextOverflow() {
|
|
|
23
23
|
}, [ref.current])
|
|
24
24
|
|
|
25
25
|
return { overflow, ref }
|
|
26
|
-
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
onHide?: () => void,
|
|
5
|
+
visible: boolean,
|
|
6
|
+
querySelectors: string,
|
|
7
|
+
onPressEscape?: () => void,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useKeyboardControls({ onHide, visible, querySelectors, onPressEscape }: Props) {
|
|
11
|
+
const wrapper = useRef<HTMLDivElement>(null)
|
|
12
|
+
|
|
13
|
+
const onRemoveListeners = () =>{
|
|
14
|
+
document.removeEventListener('keydown', keyboardControls)
|
|
15
|
+
document.removeEventListener('click', hide)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const keyboardControls = useCallback((event: KeyboardEvent) => {
|
|
19
|
+
const target = event?.target as HTMLElement | null
|
|
20
|
+
|
|
21
|
+
function getSelectableAnchors() {
|
|
22
|
+
return wrapper.current?.querySelectorAll(querySelectors) ?? []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleArrows(key = event.key) {
|
|
26
|
+
|
|
27
|
+
const anchors = getSelectableAnchors()
|
|
28
|
+
let i = 0
|
|
29
|
+
while (i < anchors.length && document.activeElement !== anchors[i]) i++
|
|
30
|
+
const next: any = key === 'ArrowDown' ? (anchors[i + 1] ?? anchors[0]) : (anchors[i - 1] ?? anchors[anchors.length - 1])
|
|
31
|
+
next?.focus?.()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handlers: Record<string, (() => void) | undefined> = {
|
|
35
|
+
Escape: () => {
|
|
36
|
+
onPressEscape?.()
|
|
37
|
+
onHide?.()
|
|
38
|
+
onRemoveListeners()
|
|
39
|
+
},
|
|
40
|
+
Enter: () => {
|
|
41
|
+
target?.click()
|
|
42
|
+
},
|
|
43
|
+
Tab: () => {
|
|
44
|
+
const anchors = getSelectableAnchors()
|
|
45
|
+
if (document.activeElement === anchors[anchors.length - 1]) onHide?.()
|
|
46
|
+
else {
|
|
47
|
+
handleArrows('ArrowDown')
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
ArrowUp: () => {
|
|
52
|
+
handleArrows()
|
|
53
|
+
},
|
|
54
|
+
ArrowDown: () => {
|
|
55
|
+
handleArrows()
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
handlers[event.key]?.()
|
|
60
|
+
}, [onPressEscape, visible])
|
|
61
|
+
|
|
62
|
+
const hide = useCallback((event: Event) => {
|
|
63
|
+
const target = (event.target as HTMLElement | null)
|
|
64
|
+
// if the element is not in the DOM anymore, we'll consider the click was inside the selection list
|
|
65
|
+
const isClickInsideSelectionList = !target?.isConnected || wrapper.current?.contains(target)
|
|
66
|
+
const isAction = target?.classList?.contains('action') || !!target?.closest('.action')
|
|
67
|
+
if (!isClickInsideSelectionList || isAction) onHide?.()
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (visible) {
|
|
72
|
+
document.addEventListener('keydown', keyboardControls)
|
|
73
|
+
document.addEventListener('keydown', keyboardControls)
|
|
74
|
+
if (onHide) setTimeout(() => document.addEventListener('click', hide), 50)
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
onRemoveListeners()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
// Remove the event listener
|
|
82
|
+
document.removeEventListener('keydown', keyboardControls)
|
|
83
|
+
document.removeEventListener('click', hide)
|
|
84
|
+
}
|
|
85
|
+
}, [visible, keyboardControls])
|
|
86
|
+
|
|
87
|
+
return wrapper
|
|
88
|
+
}
|
package/src/components/types.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { ReactNode } from 'react'
|
|
2
2
|
|
|
3
|
+
interface CustomLabel {
|
|
4
|
+
id: string,
|
|
5
|
+
element: ReactNode,
|
|
6
|
+
}
|
|
3
7
|
export interface Action {
|
|
4
|
-
label: string,
|
|
8
|
+
label: string | CustomLabel,
|
|
5
9
|
onClick?: () => void,
|
|
6
10
|
href?: string,
|
|
7
11
|
target?: React.AnchorHTMLAttributes<HTMLAnchorElement>['target'],
|
package/src/layout.css
CHANGED
|
@@ -39,7 +39,7 @@ body {
|
|
|
39
39
|
|
|
40
40
|
#layout {
|
|
41
41
|
--header-height: 56px;
|
|
42
|
-
--menu-sections-width:
|
|
42
|
+
--menu-sections-width: 135px;
|
|
43
43
|
--menu-content-width: 233px;
|
|
44
44
|
--menu-item-height: 74px;
|
|
45
45
|
--modal-animation-duration: 0.3s;
|
|
@@ -50,7 +50,7 @@ body {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
#layout.menu-compact {
|
|
53
|
-
--menu-sections-width:
|
|
53
|
+
--menu-sections-width: 135px;
|
|
54
54
|
--menu-item-height: 56px;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -147,8 +147,8 @@ body {
|
|
|
147
147
|
position: relative;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
#
|
|
151
|
-
|
|
150
|
+
#menuSections .sections-footer {
|
|
151
|
+
padding: 16px;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
#menuSections .toggle,
|
|
@@ -161,7 +161,7 @@ body {
|
|
|
161
161
|
display: flex;
|
|
162
162
|
flex-direction: column;
|
|
163
163
|
gap: 10px;
|
|
164
|
-
align-items:
|
|
164
|
+
align-items: flex-start;
|
|
165
165
|
justify-content: center;
|
|
166
166
|
transition: background-color 0.2s;
|
|
167
167
|
cursor: pointer;
|
|
@@ -175,7 +175,6 @@ body {
|
|
|
175
175
|
height: 24px;
|
|
176
176
|
transform: scaleY(0);
|
|
177
177
|
transition: transform ease-in 0.2s;
|
|
178
|
-
background-color: var(--primary-500);
|
|
179
178
|
border-radius: 50%;
|
|
180
179
|
left: 0;
|
|
181
180
|
}
|
|
@@ -186,6 +185,7 @@ body {
|
|
|
186
185
|
|
|
187
186
|
#menuSections > ul li.active a:before {
|
|
188
187
|
transform: scaleY(1);
|
|
188
|
+
background-color: var(--primary-500);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
#menuSections .toggle:hover,
|
|
@@ -195,6 +195,11 @@ body {
|
|
|
195
195
|
background: var(--light-500);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
#menuSections > ul li:not(.active) a:hover:before {
|
|
199
|
+
transform: scaleY(1);
|
|
200
|
+
background-color: var(--light-contrastText);
|
|
201
|
+
}
|
|
202
|
+
|
|
198
203
|
#menuSections .toggle i {
|
|
199
204
|
position: relative;
|
|
200
205
|
}
|
|
@@ -430,3 +435,19 @@ i {
|
|
|
430
435
|
height: 0;
|
|
431
436
|
overflow: hidden;
|
|
432
437
|
}
|
|
438
|
+
|
|
439
|
+
#menuSections .section-submenu {
|
|
440
|
+
position: relative;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#menuSections .section-submenu-icon {
|
|
444
|
+
opacity: 0;
|
|
445
|
+
position: absolute;
|
|
446
|
+
top: 27%;
|
|
447
|
+
right: 10px;
|
|
448
|
+
background-color: inherit;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#menuSections .section-submenu-icon:focus-visible {
|
|
452
|
+
opacity: 1;
|
|
453
|
+
}
|