@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.
Files changed (39) hide show
  1. package/dist/LayoutOverlayManager.js.map +1 -1
  2. package/dist/components/SelectionList.d.ts.map +1 -1
  3. package/dist/components/SelectionList.js +3 -56
  4. package/dist/components/SelectionList.js.map +1 -1
  5. package/dist/components/menu/MenuContent.d.ts.map +1 -1
  6. package/dist/components/menu/MenuContent.js +9 -3
  7. package/dist/components/menu/MenuContent.js.map +1 -1
  8. package/dist/components/menu/MenuSections.d.ts.map +1 -1
  9. package/dist/components/menu/MenuSections.js +34 -9
  10. package/dist/components/menu/MenuSections.js.map +1 -1
  11. package/dist/components/menu/PageSelector.js +1 -1
  12. package/dist/components/menu/PageSelector.js.map +1 -1
  13. package/dist/components/menu/types.d.ts +8 -0
  14. package/dist/components/menu/types.d.ts.map +1 -1
  15. package/dist/components/menu/{useCheckTextOverflow.d.ts → use-check-text-overflow.d.ts} +1 -1
  16. package/dist/components/menu/use-check-text-overflow.d.ts.map +1 -0
  17. package/dist/components/menu/{useCheckTextOverflow.js → use-check-text-overflow.js} +1 -1
  18. package/dist/components/menu/use-check-text-overflow.js.map +1 -0
  19. package/dist/components/menu/use-keyboard-controls.d.ts +10 -0
  20. package/dist/components/menu/use-keyboard-controls.d.ts.map +1 -0
  21. package/dist/components/menu/use-keyboard-controls.js +74 -0
  22. package/dist/components/menu/use-keyboard-controls.js.map +1 -0
  23. package/dist/layout.css +27 -6
  24. package/dist/utils.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/SelectionList.tsx +3 -59
  27. package/src/components/menu/MenuContent.tsx +9 -2
  28. package/src/components/menu/MenuSections.tsx +95 -35
  29. package/src/components/menu/PageSelector.tsx +1 -1
  30. package/src/components/menu/types.ts +8 -0
  31. package/src/components/menu/{useCheckTextOverflow.tsx → use-check-text-overflow.tsx} +2 -2
  32. package/src/components/menu/use-keyboard-controls.tsx +88 -0
  33. package/src/layout.css +27 -6
  34. package/dist/components/error/ErrorDescriptor.d.ts +0 -12
  35. package/dist/components/error/ErrorDescriptor.d.ts.map +0 -1
  36. package/dist/components/error/ErrorDescriptor.js +0 -17
  37. package/dist/components/error/ErrorDescriptor.js.map +0 -1
  38. package/dist/components/menu/useCheckTextOverflow.d.ts.map +0 -1
  39. 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: 87px;
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: 56px;
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
- #layout.menu-compact .section-label {
151
- display: none;
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: center;
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":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stack-spot/portal-layout",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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, useCallback, useEffect, useMemo, useRef, useState } from 'react'
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 './useCheckTextOverflow'
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: ${theme.color.primary['500']};
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
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- import { IconBox, Text } from '@citric/core'
3
- import { ChevronLeft, Menu as MenuIcon } from '@citric/icons'
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
- }: MenuSection & { id: number, setCurrentOverlay: (id: number | undefined) => void, hasContent: boolean }) => {
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
- <li key={label} title={label} className={active ? 'active' : undefined}>
85
- <a
86
- href={href}
87
- target={target}
88
- onClick={click}
89
- onMouseEnter={showOverlayAndFixArrowPosition}
90
- onMouseLeave={() => shouldShowOverlay() && hideOverlay()}
91
- title={label}
92
- aria-label={label}
93
- onKeyDown={onClick ? e => e.key === 'Enter' && onClick() : undefined}
94
- {...(active ? { 'aria-current': 'page' } : undefined)}
95
- >
96
- {icon}
97
- <Text appearance="microtext1" className="section-label">{label}</Text>
98
- </a>
99
- </li>
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 <div><MenuContent {...data} /></div>
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
- {(!!props.content || !!props.customContent) &&
155
- <button className="toggle" onClick={toggleMenu} title={t.toggle} tabIndex={-1} aria-hidden>
156
- <IconBox>
157
- <MenuIcon className="expand" />
158
- <ChevronLeft className="collapse" />
159
- </IconBox>
160
- </button>}
161
- <div id="menuContentOverlay" onMouseEnter={showOverlay} onMouseLeave={hideOverlay}>
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 './useCheckTextOverflow'
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: 87px;
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: 56px;
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
- #layout.menu-compact .section-label {
151
- display: none;
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: center;
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"}