@stack-spot/portal-layout 0.0.26 → 0.0.27
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/LayoutOverlayManager.js.map +1 -1
- package/dist/components/SelectionList.d.ts.map +1 -1
- package/dist/components/SelectionList.js +3 -56
- package/dist/components/SelectionList.js.map +1 -1
- package/dist/components/menu/MenuContent.d.ts.map +1 -1
- package/dist/components/menu/MenuContent.js +9 -3
- 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 +34 -9
- package/dist/components/menu/MenuSections.js.map +1 -1
- package/dist/components/menu/PageSelector.js +1 -1
- 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/{useCheckTextOverflow.d.ts → use-check-text-overflow.d.ts} +1 -1
- package/dist/components/menu/use-check-text-overflow.d.ts.map +1 -0
- package/dist/components/menu/{useCheckTextOverflow.js → use-check-text-overflow.js} +1 -1
- 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/layout.css +27 -6
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/components/SelectionList.tsx +3 -59
- package/src/components/menu/MenuContent.tsx +9 -2
- package/src/components/menu/MenuSections.tsx +95 -35
- package/src/components/menu/PageSelector.tsx +1 -1
- 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/layout.css +27 -6
- package/dist/components/error/ErrorDescriptor.d.ts +0 -12
- package/dist/components/error/ErrorDescriptor.d.ts.map +0 -1
- package/dist/components/error/ErrorDescriptor.js +0 -17
- package/dist/components/error/ErrorDescriptor.js.map +0 -1
- package/dist/components/menu/useCheckTextOverflow.d.ts.map +0 -1
- package/dist/components/menu/useCheckTextOverflow.js.map +0 -1
package/dist/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
|
+
}
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE9C,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAA;IAChD,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAA;IACtB,OAAO,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;AACjC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAA4B;IACpE,OAAO,GAAG,OAAO,IAAI,QAAQ,CAAC,aAA4B,CAAA;IAC1D,OAAO,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE;
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -3,8 +3,9 @@ import { ArrowLeft, Check, ChevronRight } from '@citric/icons'
|
|
|
3
3
|
import { IconButton } from '@citric/ui'
|
|
4
4
|
import { WithStyle, listToClass, theme } from '@stack-spot/portal-theme'
|
|
5
5
|
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
6
|
-
import { ReactElement,
|
|
6
|
+
import { ReactElement, useMemo, useState } from 'react'
|
|
7
7
|
import { styled } from 'styled-components'
|
|
8
|
+
import { useKeyboardControls } from './menu/use-keyboard-controls'
|
|
8
9
|
import { Action } from './types'
|
|
9
10
|
|
|
10
11
|
interface ItemWithIcon {
|
|
@@ -171,7 +172,6 @@ export const SelectionList = ({
|
|
|
171
172
|
id, items, className, style, visible = true, maxHeight = '300px', onHide, before, after, scroll,
|
|
172
173
|
}: SelectionListProps) => {
|
|
173
174
|
const t = useTranslate(dictionary)
|
|
174
|
-
const wrapper = useRef<HTMLDivElement>(null)
|
|
175
175
|
const [current, setCurrent] = useState<CurrentItemList>({ items })
|
|
176
176
|
|
|
177
177
|
const listItems = useMemo(
|
|
@@ -185,63 +185,7 @@ export const SelectionList = ({
|
|
|
185
185
|
)),
|
|
186
186
|
[current],
|
|
187
187
|
)
|
|
188
|
-
|
|
189
|
-
const keyboardControls = useCallback((event: KeyboardEvent) => {
|
|
190
|
-
const target = event?.target as HTMLElement | null
|
|
191
|
-
|
|
192
|
-
function getSelectableAnchors() {
|
|
193
|
-
return wrapper.current?.querySelectorAll('li.action a, li.collapsible a, button') ?? []
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function handleArrows(key = event.key) {
|
|
197
|
-
const anchors = getSelectableAnchors()
|
|
198
|
-
let i = 0
|
|
199
|
-
while (i < anchors.length && document.activeElement !== anchors[i]) i++
|
|
200
|
-
const next: any = key === 'ArrowDown' ? (anchors[i + 1] ?? anchors[0]) : (anchors[i - 1] ?? anchors[anchors.length - 1])
|
|
201
|
-
next?.focus?.()
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const handlers: Record<string, (() => void) | undefined> = {
|
|
205
|
-
Escape: () => {
|
|
206
|
-
onHide?.()
|
|
207
|
-
event.stopPropagation()
|
|
208
|
-
event.preventDefault()
|
|
209
|
-
},
|
|
210
|
-
Enter: () => target?.click(),
|
|
211
|
-
Tab: () => {
|
|
212
|
-
const anchors = getSelectableAnchors()
|
|
213
|
-
if (document.activeElement === anchors[anchors.length - 1]) onHide?.()
|
|
214
|
-
else {
|
|
215
|
-
handleArrows('ArrowDown')
|
|
216
|
-
event.preventDefault()
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
ArrowUp: handleArrows,
|
|
220
|
-
ArrowDown: handleArrows,
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
handlers[event.key]?.()
|
|
224
|
-
}, [])
|
|
225
|
-
|
|
226
|
-
const hide = useCallback((event: Event) => {
|
|
227
|
-
const target = (event.target as HTMLElement | null)
|
|
228
|
-
// if the element is not in the DOM anymore, we'll consider the click was inside the selection list
|
|
229
|
-
const isClickInsideSelectionList = !target?.isConnected || wrapper.current?.contains(target)
|
|
230
|
-
const isAction = target?.classList?.contains('action') || !!target?.closest('.action')
|
|
231
|
-
if (!isClickInsideSelectionList || isAction) onHide?.()
|
|
232
|
-
}, [])
|
|
233
|
-
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
if (visible) {
|
|
236
|
-
setCurrent({ items })
|
|
237
|
-
document.addEventListener('keydown', keyboardControls)
|
|
238
|
-
if (onHide) setTimeout(() => document.addEventListener('click', hide), 50)
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
document.removeEventListener('keydown', keyboardControls)
|
|
242
|
-
document.removeEventListener('click', hide)
|
|
243
|
-
}
|
|
244
|
-
}, [visible])
|
|
188
|
+
const wrapper = useKeyboardControls({ onHide, querySelectors: 'li.action a, li.collapsible a, button', visible })
|
|
245
189
|
|
|
246
190
|
return (
|
|
247
191
|
<SelectionBox
|
|
@@ -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 {
|
|
@@ -145,6 +151,7 @@ export const ActionItem = ({ label, onClick, href, active, icon, badge, overflow
|
|
|
145
151
|
}}
|
|
146
152
|
className={listToClass(['action', 'item-row', active ? 'active' : undefined])}
|
|
147
153
|
{...(active ? { 'aria-current': 'page' } : undefined)}
|
|
154
|
+
{...(!href ? { 'tabIndex': 0 } : undefined)}
|
|
148
155
|
>
|
|
149
156
|
{icon}
|
|
150
157
|
<Text ref={ref} appearance="body2" className={`label ${overflow}`} title={textOverflow ? label : ''}>{label}</Text>
|
|
@@ -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()
|
|
@@ -81,22 +84,42 @@ const Section = ({
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
return (
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
<>
|
|
88
|
+
<li key={label} title={label} className={`section-submenu ${className || ''} ${active ? 'active' : ''}`}>
|
|
89
|
+
<a
|
|
90
|
+
href={href}
|
|
91
|
+
target={target}
|
|
92
|
+
onClick={click}
|
|
93
|
+
onMouseEnter={showOverlayAndFixArrowPosition}
|
|
94
|
+
onMouseLeave={() => shouldShowOverlay() && hideOverlay()}
|
|
95
|
+
title={label}
|
|
96
|
+
aria-label={label}
|
|
97
|
+
onKeyDown={onClick ? e => e.key === 'Enter' && onClick() : undefined}
|
|
98
|
+
{...(active ? { 'aria-current': 'page' } : undefined)}
|
|
99
|
+
{...(!href ? { 'tabIndex': 0 } : undefined)}
|
|
100
|
+
>
|
|
101
|
+
<Flex alignItems="center" justifyContent="center" px={5}>
|
|
102
|
+
{icon}
|
|
103
|
+
<Text appearance="body2" className="section-label" ml={2}>{label}</Text>
|
|
104
|
+
</Flex>
|
|
105
|
+
</a>
|
|
106
|
+
{shouldShowOverlay() &&
|
|
107
|
+
<IconBox size="sm" className="section-submenu-icon"
|
|
108
|
+
as="button"
|
|
109
|
+
aria-label={interpolate(t.menuOptions, label)}
|
|
110
|
+
aria-controls={MENU_OVERLAY_ID}
|
|
111
|
+
aria-expanded={document.getElementById(MENU_OVERLAY_ID)?.classList.contains('visible')}
|
|
112
|
+
onKeyDown={(event) => {
|
|
113
|
+
if (event.key === 'Enter') {
|
|
114
|
+
showOverlayAndFixArrowPosition(event)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}>
|
|
118
|
+
<ChevronRight />
|
|
119
|
+
</IconBox>
|
|
120
|
+
}
|
|
121
|
+
</li>
|
|
122
|
+
</>
|
|
100
123
|
)
|
|
101
124
|
}
|
|
102
125
|
|
|
@@ -104,15 +127,17 @@ const OverlayRenderer = ({ content, customContent }: Pick<MenuSection, 'content'
|
|
|
104
127
|
if (customContent) {
|
|
105
128
|
return <> {customContent} </>
|
|
106
129
|
}
|
|
107
|
-
|
|
130
|
+
|
|
108
131
|
const data = typeof content === 'function' ? content() : content
|
|
109
|
-
return <
|
|
132
|
+
return <MenuContent {...data} />
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
113
136
|
const t = useTranslate(dictionary)
|
|
114
137
|
// this is a mock state only used to force an update on the component.
|
|
115
138
|
const [_, setUpdate] = useState(0)
|
|
139
|
+
const onHide = () => hideOverlay()
|
|
140
|
+
|
|
116
141
|
const toggleMenu = useCallback(() => {
|
|
117
142
|
const layout = document.getElementById('layout')
|
|
118
143
|
if (!layout) return
|
|
@@ -132,6 +157,18 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
132
157
|
[sections],
|
|
133
158
|
)
|
|
134
159
|
|
|
160
|
+
function onPressEscape() {
|
|
161
|
+
hideOverlay()
|
|
162
|
+
const items = document.getElementsByClassName('section-submenu')
|
|
163
|
+
if (!!items && !!currentOverlay && items.length > currentOverlay && items[currentOverlay].children.length > 1) {
|
|
164
|
+
(items[currentOverlay].children[1] as HTMLElement).focus()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const wrapper = useKeyboardControls({ onHide, visible: document.getElementById(MENU_OVERLAY_ID)?.classList.contains('visible') || false,
|
|
169
|
+
querySelectors: 'li a.action, #custom-selectable-item button, #custom-selectable-item input',
|
|
170
|
+
onPressEscape: () => onPressEscape() })
|
|
171
|
+
|
|
135
172
|
/* This function renders the section preview in the overlay in normal circumstances. If the menu is hidden and the section is active,
|
|
136
173
|
instead of rendering the section preview, it will render the actual menu content, which would be invisible otherwise.
|
|
137
174
|
Below, the key is of extreme importance. It ensures React will consider every section content to be an entirely different
|
|
@@ -139,26 +176,43 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
139
176
|
hook, this would cause some serious problems. */
|
|
140
177
|
function renderMenuOverlay() {
|
|
141
178
|
if (currentOverlay === undefined) return null
|
|
142
|
-
const shouldRenderMenuContentInstead = !isMenuContentVisible() && sections[currentOverlay].active &&
|
|
179
|
+
const shouldRenderMenuContentInstead = !isMenuContentVisible() && sections[currentOverlay].active &&
|
|
143
180
|
(!!props.content || !!props.customContent)
|
|
144
181
|
return shouldRenderMenuContentInstead
|
|
145
182
|
? <OverlayRenderer key={'contentKey' in props ? props.contentKey : undefined} content={props.content}
|
|
146
183
|
customContent={props.customContent} />
|
|
147
|
-
: <OverlayRenderer key={currentOverlay} content={sections[currentOverlay].content}
|
|
184
|
+
: <OverlayRenderer key={currentOverlay} content={sections[currentOverlay].content}
|
|
148
185
|
customContent={sections[currentOverlay].customContent} />
|
|
149
186
|
}
|
|
150
187
|
|
|
151
188
|
return (
|
|
152
189
|
<>
|
|
153
190
|
<ul>{sectionItems}</ul>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
191
|
+
|
|
192
|
+
{(!!props.content || !!props.customContent || props.settings?.show) &&
|
|
193
|
+
<Flex mb={7} alignItems="center" justifyContent="center">
|
|
194
|
+
{(!!props.content || !!props.customContent) &&
|
|
195
|
+
<button className="toggle sections-footer" onClick={toggleMenu} title={t.toggle} tabIndex={-1} aria-hidden>
|
|
196
|
+
<IconBox>
|
|
197
|
+
<MenuIcon className="expand" />
|
|
198
|
+
<Collapse className="collapse" />
|
|
199
|
+
</IconBox>
|
|
200
|
+
</button>}
|
|
201
|
+
{(props.settings?.show) &&
|
|
202
|
+
<a href={props.settings?.href} onClick={props.settings?.onClick}
|
|
203
|
+
className="sections-footer"
|
|
204
|
+
{...(props.settings.active ? { 'aria-current': 'page' } : undefined)}>
|
|
205
|
+
<Flex alignItems="center" justifyContent="center">
|
|
206
|
+
<IconBox aria-label={t.settingsIcon}>
|
|
207
|
+
<Cog />
|
|
208
|
+
</IconBox>
|
|
209
|
+
<Text appearance="body2" ml={2}>{t.settings}</Text>
|
|
210
|
+
</Flex>
|
|
211
|
+
</a>
|
|
212
|
+
}
|
|
213
|
+
</Flex>
|
|
214
|
+
}
|
|
215
|
+
<div id={MENU_OVERLAY_ID} onMouseEnter={showOverlay} onMouseLeave={hideOverlay} ref={wrapper}>
|
|
162
216
|
{renderMenuOverlay()}
|
|
163
217
|
<div className="arrow"></div>
|
|
164
218
|
</div>
|
|
@@ -169,8 +223,14 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
169
223
|
const dictionary = {
|
|
170
224
|
en: {
|
|
171
225
|
toggle: 'Show or hide the menu',
|
|
226
|
+
menuOptions: 'View $0 menu options',
|
|
227
|
+
settings: 'Settings',
|
|
228
|
+
settingsIcon: 'Settings icon',
|
|
172
229
|
},
|
|
173
230
|
pt: {
|
|
174
231
|
toggle: 'Visualizar ou esconder o menu',
|
|
232
|
+
menuOptions: 'Visualizar opções do menu $0',
|
|
233
|
+
settings: 'Configurações',
|
|
234
|
+
settingsIcon: 'Icone de configurações',
|
|
175
235
|
},
|
|
176
236
|
} 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;
|
|
@@ -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/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
|
+
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface ErrorDescription {
|
|
2
|
-
code?: number;
|
|
3
|
-
message?: string;
|
|
4
|
-
debug?: boolean;
|
|
5
|
-
}
|
|
6
|
-
export type DescriptionFn = (error: any) => ErrorDescription;
|
|
7
|
-
export declare class ErrorDescriptor {
|
|
8
|
-
private static descriptionFunction;
|
|
9
|
-
static setDescriptionFunction(fn: DescriptionFn): void;
|
|
10
|
-
static describe(error: any): ErrorDescription;
|
|
11
|
-
}
|
|
12
|
-
//# sourceMappingURL=ErrorDescriptor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorDescriptor.d.ts","sourceRoot":"","sources":["../../../src/components/error/ErrorDescriptor.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,gBAAgB,CAAA;AAE5D,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAEhC;IAEF,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,aAAa;IAI/C,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG;CAG3B"}
|