@stack-spot/portal-layout 0.0.10 → 0.0.12
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/Layout.d.ts +5 -3
- package/dist/Layout.d.ts.map +1 -1
- package/dist/Layout.js +12 -7
- package/dist/Layout.js.map +1 -1
- package/dist/LayoutOverlayManager.d.ts +7 -0
- package/dist/LayoutOverlayManager.d.ts.map +1 -1
- package/dist/LayoutOverlayManager.js +67 -23
- package/dist/LayoutOverlayManager.js.map +1 -1
- package/dist/components/OverlayContent.d.ts +1 -0
- package/dist/components/OverlayContent.d.ts.map +1 -1
- package/dist/components/OverlayContent.js +2 -1
- package/dist/components/OverlayContent.js.map +1 -1
- package/dist/components/SelectionList.d.ts +2 -1
- package/dist/components/SelectionList.d.ts.map +1 -1
- package/dist/components/SelectionList.js +87 -30
- package/dist/components/SelectionList.js.map +1 -1
- package/dist/components/UserMenu.d.ts.map +1 -1
- package/dist/components/UserMenu.js +14 -3
- package/dist/components/UserMenu.js.map +1 -1
- package/dist/components/error/ErrorBoundary.d.ts +1 -1
- package/dist/components/error/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/error/ErrorBoundary.js +3 -2
- package/dist/components/error/ErrorBoundary.js.map +1 -1
- package/dist/components/error/ErrorFeedback.d.ts +1 -1
- package/dist/components/error/ErrorFeedback.d.ts.map +1 -1
- package/dist/components/error/ErrorManager.d.ts +16 -0
- package/dist/components/error/ErrorManager.d.ts.map +1 -0
- package/dist/components/error/ErrorManager.js +23 -0
- package/dist/components/error/ErrorManager.js.map +1 -0
- package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
- package/dist/components/error/SilentErrorBoundary.d.ts.map +1 -1
- package/dist/components/error/SilentErrorBoundary.js +3 -2
- package/dist/components/error/SilentErrorBoundary.js.map +1 -1
- package/dist/components/menu/MenuContent.d.ts.map +1 -1
- package/dist/components/menu/MenuContent.js +10 -6
- 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 +12 -2
- 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 +13 -2
- package/dist/components/menu/PageSelector.js.map +1 -1
- package/dist/elements.d.ts +13 -0
- package/dist/elements.d.ts.map +1 -0
- package/dist/elements.js +13 -0
- package/dist/elements.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.css +6 -2
- package/dist/toaster.d.ts.map +1 -1
- package/dist/toaster.js.map +1 -1
- package/dist/utils.d.ts +45 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +80 -0
- package/dist/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/Layout.tsx +15 -8
- package/src/LayoutOverlayManager.tsx +72 -33
- package/src/components/OverlayContent.tsx +3 -1
- package/src/components/SelectionList.tsx +111 -28
- package/src/components/UserMenu.tsx +23 -3
- package/src/components/error/ErrorBoundary.tsx +3 -2
- package/src/components/error/ErrorFeedback.tsx +1 -1
- package/src/components/error/{ErrorDescriptor.ts → ErrorManager.ts} +11 -1
- package/src/components/error/SilentErrorBoundary.tsx +3 -2
- package/src/components/menu/MenuContent.tsx +18 -7
- package/src/components/menu/MenuSections.tsx +14 -1
- package/src/components/menu/PageSelector.tsx +24 -2
- package/src/elements.ts +19 -0
- package/src/index.ts +2 -1
- package/src/layout.css +6 -2
- package/src/toaster.tsx +1 -2
- package/src/utils.ts +94 -0
|
@@ -2,6 +2,7 @@ import { Flex, IconBox, LinkBox, Text } from '@citric/core'
|
|
|
2
2
|
import { ChevronDown } from '@citric/icons'
|
|
3
3
|
import { Avatar } from '@citric/ui'
|
|
4
4
|
import { theme } from '@stack-spot/portal-theme'
|
|
5
|
+
import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
|
|
5
6
|
import { useState } from 'react'
|
|
6
7
|
import { styled } from 'styled-components'
|
|
7
8
|
import { SelectionList, SelectionListProps } from './SelectionList'
|
|
@@ -12,6 +13,8 @@ interface Props {
|
|
|
12
13
|
options?: SelectionListProps['items'],
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
const USER_MENU_ID = 'userMenu'
|
|
17
|
+
|
|
15
18
|
const UserMenuBox = styled.div`
|
|
16
19
|
.user-menu-header {
|
|
17
20
|
display: flex;
|
|
@@ -38,7 +41,7 @@ const UserMenuBox = styled.div`
|
|
|
38
41
|
margin: 8px 0;
|
|
39
42
|
& > a {
|
|
40
43
|
border-radius: 6px;
|
|
41
|
-
&:hover {
|
|
44
|
+
&:hover, &:focus {
|
|
42
45
|
background: ${theme.color.light['500']};
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -63,13 +66,20 @@ const UserMenuHeader = ({ userName, email }: Omit<Props, 'options'>) => (
|
|
|
63
66
|
)
|
|
64
67
|
|
|
65
68
|
export const UserMenu = ({ userName, email, options }: Props) => {
|
|
69
|
+
const t = useTranslate(dictionary)
|
|
66
70
|
const [visible, setVisible] = useState(false)
|
|
67
71
|
|
|
68
72
|
return (
|
|
69
73
|
<UserMenuBox>
|
|
70
|
-
<LinkBox
|
|
74
|
+
<LinkBox
|
|
75
|
+
as="button"
|
|
76
|
+
onClick={() => setVisible(v => !v)}
|
|
77
|
+
aria-controls={USER_MENU_ID}
|
|
78
|
+
aria-expanded={visible}
|
|
79
|
+
aria-label={interpolate(t.accountMenu, [userName])}
|
|
80
|
+
>
|
|
71
81
|
<Flex alignItems="center">
|
|
72
|
-
<Avatar size="xs">{userName}</Avatar>
|
|
82
|
+
<Avatar size="xs" aria-label={interpolate(t.accountMenu, [userName])}>{userName}</Avatar>
|
|
73
83
|
<IconBox colorScheme="inverse" className="chevron" style={visible ? { transform: 'rotate(180deg)' } : undefined}>
|
|
74
84
|
<ChevronDown />
|
|
75
85
|
</IconBox>
|
|
@@ -78,6 +88,7 @@ export const UserMenu = ({ userName, email, options }: Props) => {
|
|
|
78
88
|
|
|
79
89
|
{options?.length
|
|
80
90
|
? <SelectionList
|
|
91
|
+
id={USER_MENU_ID}
|
|
81
92
|
visible={visible}
|
|
82
93
|
before={<UserMenuHeader userName={userName} email={email} />}
|
|
83
94
|
items={options!}
|
|
@@ -89,3 +100,12 @@ export const UserMenu = ({ userName, email, options }: Props) => {
|
|
|
89
100
|
</UserMenuBox>
|
|
90
101
|
)
|
|
91
102
|
}
|
|
103
|
+
|
|
104
|
+
const dictionary = {
|
|
105
|
+
en: {
|
|
106
|
+
accountMenu: 'Profile menu of $0',
|
|
107
|
+
},
|
|
108
|
+
pt: {
|
|
109
|
+
accountMenu: 'Menu do perfil $0',
|
|
110
|
+
},
|
|
111
|
+
} satisfies Dictionary
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Component } from 'react'
|
|
2
|
-
import { ErrorDescription, ErrorDescriptor } from './ErrorDescriptor'
|
|
3
2
|
import { ErrorFeedback } from './ErrorFeedback'
|
|
3
|
+
import { ErrorDescription, ErrorManager } from './ErrorManager'
|
|
4
4
|
|
|
5
5
|
interface State extends ErrorDescription {
|
|
6
6
|
hasError: boolean,
|
|
@@ -17,12 +17,13 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
static getDerivedStateFromError(error: any) {
|
|
20
|
-
return { hasError: true, ...
|
|
20
|
+
return { hasError: true, ...ErrorManager.describe(error) }
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
componentDidCatch(error: any, errorInfo: any) {
|
|
24
24
|
// eslint-disable-next-line no-console
|
|
25
25
|
console.error(error, errorInfo)
|
|
26
|
+
ErrorManager.runErrorHandler(error)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
componentDidUpdate(prevProps: Readonly<Props>) {
|
|
@@ -6,7 +6,7 @@ import { Logo } from '../../svg/Logo'
|
|
|
6
6
|
import { NotFound } from '../../svg/NotFound'
|
|
7
7
|
import { ServerError } from '../../svg/ServerError'
|
|
8
8
|
import { Unauthenticated } from '../../svg/Unauthenticated'
|
|
9
|
-
import { ErrorDescription } from './
|
|
9
|
+
import { ErrorDescription } from './ErrorManager'
|
|
10
10
|
|
|
11
11
|
const imageStyle: React.CSSProperties = {
|
|
12
12
|
width: '200px',
|
|
@@ -5,17 +5,27 @@ export interface ErrorDescription {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export type DescriptionFn = (error: any) => ErrorDescription
|
|
8
|
+
export type ErrorHandler = (error: any) => void
|
|
8
9
|
|
|
9
|
-
export class
|
|
10
|
+
export class ErrorManager {
|
|
10
11
|
private static descriptionFunction: DescriptionFn = error => ({
|
|
11
12
|
message: error.message || `${error}`,
|
|
12
13
|
})
|
|
14
|
+
private static errorHandler: ErrorHandler | undefined
|
|
13
15
|
|
|
14
16
|
static setDescriptionFunction(fn: DescriptionFn) {
|
|
15
17
|
this.descriptionFunction = fn
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
static setErrorHandler(handler: ErrorHandler) {
|
|
21
|
+
this.errorHandler = handler
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
static describe(error: any) {
|
|
19
25
|
return this.descriptionFunction(error)
|
|
20
26
|
}
|
|
27
|
+
|
|
28
|
+
static runErrorHandler(error: any) {
|
|
29
|
+
return this.errorHandler?.(error)
|
|
30
|
+
}
|
|
21
31
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { theme } from '@stack-spot/portal-theme'
|
|
2
2
|
import { Component } from 'react'
|
|
3
|
-
import { ErrorDescription,
|
|
3
|
+
import { ErrorDescription, ErrorManager } from './ErrorManager'
|
|
4
4
|
|
|
5
5
|
interface State extends ErrorDescription {
|
|
6
6
|
hasError: boolean,
|
|
@@ -18,12 +18,13 @@ export class SilentErrorBoundary extends Component<Props, State> {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
static getDerivedStateFromError(error: any) {
|
|
21
|
-
return { hasError: true, ...
|
|
21
|
+
return { hasError: true, ...ErrorManager.describe(error) }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
componentDidCatch(error: any, errorInfo: any) {
|
|
25
25
|
// eslint-disable-next-line no-console
|
|
26
26
|
console.error(error, errorInfo)
|
|
27
|
+
ErrorManager.runErrorHandler(error)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
componentDidUpdate(prevProps: Readonly<Props>) {
|
|
@@ -23,6 +23,8 @@ const MenuGroup = styled.ul`
|
|
|
23
23
|
padding: 0 0 0 16px;
|
|
24
24
|
display: flex;
|
|
25
25
|
flex-direction: column;
|
|
26
|
+
visibility: hidden;
|
|
27
|
+
transition: visibility 0s 0.3s;
|
|
26
28
|
|
|
27
29
|
&.no-indentation {
|
|
28
30
|
padding: 0;
|
|
@@ -50,12 +52,9 @@ const MenuGroup = styled.ul`
|
|
|
50
52
|
position: relative;
|
|
51
53
|
height: 0;
|
|
52
54
|
overflow: hidden;
|
|
53
|
-
/* display: flex; */
|
|
54
|
-
/* align-items: center; */
|
|
55
55
|
transition: height 0.3s, background-color 0.2s;
|
|
56
56
|
margin: 0 ${PADDING - ITEM_PADDING}px;
|
|
57
57
|
border-radius: 4px;
|
|
58
|
-
/* justify-content: space-between; */
|
|
59
58
|
padding: 0 ${ITEM_PADDING}px;
|
|
60
59
|
|
|
61
60
|
&:hover {
|
|
@@ -95,8 +94,12 @@ const MenuGroup = styled.ul`
|
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
&.open
|
|
99
|
-
|
|
97
|
+
&.open {
|
|
98
|
+
visibility: visible;
|
|
99
|
+
transition: unset;
|
|
100
|
+
& > li > a {
|
|
101
|
+
height: 40px;
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
&:not(.open) &.open > li > a {
|
|
@@ -148,16 +151,24 @@ const ActionItem = ({ label, onClick, href, active, icon, badge, overflow = 'wra
|
|
|
148
151
|
const CollapsibleGroupItem = ({ label, open: initiallyOpened, children, icon, badge, overflow = 'wrap' }: ItemGroup) => {
|
|
149
152
|
const [open, setOpen] = useState(initiallyOpened ?? children?.some(c => 'active' in c && c.active) ?? false)
|
|
150
153
|
const items = useMemo(() => children?.map(renderOption), [children])
|
|
154
|
+
const id = `menuGroup${label}`
|
|
151
155
|
|
|
152
156
|
return (
|
|
153
157
|
<>
|
|
154
|
-
<a
|
|
158
|
+
<a
|
|
159
|
+
onClick={() => setOpen(!open)}
|
|
160
|
+
onKeyDown={e => e.key === 'Enter' && setOpen(!open)}
|
|
161
|
+
className="item-row"
|
|
162
|
+
tabIndex={0}
|
|
163
|
+
aria-controls={id}
|
|
164
|
+
aria-expanded={open}
|
|
165
|
+
>
|
|
155
166
|
{icon}
|
|
156
167
|
<Text appearance="body2" className={`label ${overflow}`}>{label}</Text>
|
|
157
168
|
{badge}
|
|
158
169
|
<IconBox><ChevronDown className={listToClass(['chevron', open ? 'open' : ''])} /></IconBox>
|
|
159
170
|
</a>
|
|
160
|
-
<MenuGroup className={open ? 'open' : undefined}>{items}</MenuGroup>
|
|
171
|
+
<MenuGroup id={id} className={open ? 'open' : undefined} aria-hidden={!open}>{items}</MenuGroup>
|
|
161
172
|
</>
|
|
162
173
|
)
|
|
163
174
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
2
|
import { IconBox, Text } from '@citric/core'
|
|
3
3
|
import { ChevronLeft, Menu as MenuIcon } from '@citric/icons'
|
|
4
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
4
5
|
import { useCallback, useMemo, useState } from 'react'
|
|
5
6
|
import { MenuContent } from './MenuContent'
|
|
6
7
|
import { MenuProps, MenuSection } from './types'
|
|
@@ -84,6 +85,8 @@ const Section = ({
|
|
|
84
85
|
onClick={click}
|
|
85
86
|
onMouseEnter={showOverlayAndFixArrowPosition}
|
|
86
87
|
onMouseLeave={() => shouldShowOverlay() && hideOverlay()}
|
|
88
|
+
title={label}
|
|
89
|
+
aria-label={label}
|
|
87
90
|
>
|
|
88
91
|
{icon}
|
|
89
92
|
<Text appearance="microtext1" className="section-label">{label}</Text>
|
|
@@ -98,6 +101,7 @@ const OverlayRenderer = ({ content }: Pick<MenuSection, 'content'>) => {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
104
|
+
const t = useTranslate(dictionary)
|
|
101
105
|
// this is a mock state only used to force an update on the component.
|
|
102
106
|
const [_, setUpdate] = useState(0)
|
|
103
107
|
const toggleMenu = useCallback(() => {
|
|
@@ -134,7 +138,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
134
138
|
return (
|
|
135
139
|
<>
|
|
136
140
|
<ul>{sectionItems}</ul>
|
|
137
|
-
{!!props.content && <button className="toggle" onClick={toggleMenu} title=
|
|
141
|
+
{!!props.content && <button className="toggle" onClick={toggleMenu} title={t.toggle}>
|
|
138
142
|
<IconBox>
|
|
139
143
|
<MenuIcon className="expand" />
|
|
140
144
|
<ChevronLeft className="collapse" />
|
|
@@ -147,3 +151,12 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
147
151
|
</>
|
|
148
152
|
)
|
|
149
153
|
}
|
|
154
|
+
|
|
155
|
+
const dictionary = {
|
|
156
|
+
en: {
|
|
157
|
+
toggle: 'Show or hide the menu',
|
|
158
|
+
},
|
|
159
|
+
pt: {
|
|
160
|
+
toggle: 'Visualizar ou esconder o menu',
|
|
161
|
+
},
|
|
162
|
+
} satisfies Dictionary
|
|
@@ -2,7 +2,8 @@ import { IconBox, Text } from '@citric/core'
|
|
|
2
2
|
import { ArrowRight, Select } from '@citric/icons'
|
|
3
3
|
import { LoadingCircular } from '@citric/ui'
|
|
4
4
|
import { theme } from '@stack-spot/portal-theme'
|
|
5
|
-
import {
|
|
5
|
+
import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
|
|
6
|
+
import { useMemo, useRef, useState } from 'react'
|
|
6
7
|
import { styled } from 'styled-components'
|
|
7
8
|
import { ListAction, SelectionList } from '../SelectionList'
|
|
8
9
|
import { MENU_CONTENT_PADDING as PADDING } from './constants'
|
|
@@ -76,7 +77,9 @@ const SelectorBox = styled.div`
|
|
|
76
77
|
`
|
|
77
78
|
|
|
78
79
|
export const PageSelector = ({ options, value, button, loading, title }: Selector) => {
|
|
80
|
+
const t = useTranslate(dictionary)
|
|
79
81
|
const [visible, setVisible] = useState(false)
|
|
82
|
+
const id = useRef(`pageSelector${title || Math.random()}`)
|
|
80
83
|
const { optionsWithIcon, selected } = useMemo(
|
|
81
84
|
() => {
|
|
82
85
|
let selected = options[0]
|
|
@@ -100,13 +103,22 @@ export const PageSelector = ({ options, value, button, loading, title }: Selecto
|
|
|
100
103
|
: (
|
|
101
104
|
<>
|
|
102
105
|
{title && <Text colorScheme="light.700" sx={{ mb: 3 }}>{title}</Text>}
|
|
103
|
-
<a
|
|
106
|
+
<a
|
|
107
|
+
onClick={() => setVisible(true)}
|
|
108
|
+
onKeyDown={e => e.key === 'Enter' && setVisible(true)}
|
|
109
|
+
title={value}
|
|
110
|
+
tabIndex={0}
|
|
111
|
+
aria-label={interpolate(t.accessibility, [value])}
|
|
112
|
+
aria-expanded={visible}
|
|
113
|
+
aria-controls={id.current}
|
|
114
|
+
>
|
|
104
115
|
{selected?.icon && <IconBox>{selected?.icon}</IconBox>}
|
|
105
116
|
<Text appearance="body2" className="label">{selected?.label ?? button?.label ?? value}</Text>
|
|
106
117
|
<IconBox size="xs"><Select /></IconBox>
|
|
107
118
|
</a>
|
|
108
119
|
|
|
109
120
|
<SelectionList
|
|
121
|
+
id={id.current}
|
|
110
122
|
visible={visible}
|
|
111
123
|
items={optionsWithIcon}
|
|
112
124
|
onHide={() => setVisible(false)}
|
|
@@ -119,3 +131,13 @@ export const PageSelector = ({ options, value, button, loading, title }: Selecto
|
|
|
119
131
|
</SelectorBox>
|
|
120
132
|
)
|
|
121
133
|
}
|
|
134
|
+
|
|
135
|
+
const dictionary = {
|
|
136
|
+
en: {
|
|
137
|
+
accessibility: 'Current value: $0. Press Enter to change.',
|
|
138
|
+
},
|
|
139
|
+
pt: {
|
|
140
|
+
accessibility: 'Valor atual: $0. Aperte Enter para mudar.',
|
|
141
|
+
},
|
|
142
|
+
} satisfies Dictionary
|
|
143
|
+
|
package/src/elements.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const elementIds = {
|
|
2
|
+
backdrop: 'backdrop',
|
|
3
|
+
modal: 'modal',
|
|
4
|
+
rightPanel: 'rightPanel',
|
|
5
|
+
bottomDialog: 'bottomDialog',
|
|
6
|
+
page: 'page',
|
|
7
|
+
header: 'header',
|
|
8
|
+
menu: 'menu',
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
export type LayoutElement = keyof typeof elementIds
|
|
12
|
+
export type LayoutElements = Record<LayoutElement, HTMLElement | null>
|
|
13
|
+
|
|
14
|
+
export function getLayoutElements() {
|
|
15
|
+
return (Object.keys(elementIds) as LayoutElement[]).reduce<LayoutElements>(
|
|
16
|
+
(result, id) => ({ ...result, [id]: document.getElementById(id) }),
|
|
17
|
+
{} as LayoutElements,
|
|
18
|
+
)
|
|
19
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,8 @@ export { MenuContent } from './components/menu/MenuContent'
|
|
|
8
8
|
export { MenuSections } from './components/menu/MenuSections'
|
|
9
9
|
export * from './components/menu/types'
|
|
10
10
|
export * from './components/types'
|
|
11
|
+
export * from './elements'
|
|
11
12
|
export * from './errors'
|
|
12
13
|
export { Logo as StackspotLogo } from './svg/Logo'
|
|
13
|
-
export { valueOfLayoutVar } from './utils'
|
|
14
|
+
export { focusFirstChild, focusNextIgnoringChildren, valueOfLayoutVar } from './utils'
|
|
14
15
|
|
package/src/layout.css
CHANGED
|
@@ -188,7 +188,9 @@ body {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
#menuSections .toggle:hover,
|
|
191
|
-
#menuSections > ul li a:hover
|
|
191
|
+
#menuSections > ul li a:hover,
|
|
192
|
+
#menuSections .toggle:focus,
|
|
193
|
+
#menuSections > ul li a:focus {
|
|
192
194
|
background: var(--light-500);
|
|
193
195
|
}
|
|
194
196
|
|
|
@@ -308,11 +310,13 @@ body {
|
|
|
308
310
|
position: fixed;
|
|
309
311
|
display: flex;
|
|
310
312
|
flex-direction: column;
|
|
311
|
-
top:
|
|
313
|
+
top: 0;
|
|
312
314
|
bottom: 0;
|
|
313
315
|
transition: right var(--right-panel-animation-duration);
|
|
314
316
|
background-color: var(--light-400);
|
|
315
317
|
right: -800px;
|
|
318
|
+
border-top-left-radius: 1rem;
|
|
319
|
+
border-bottom-left-radius: 1rem;
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
#rightPanel.small {
|
package/src/toaster.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { IconBox } from '@citric/core'
|
|
2
|
-
import { OneOfColorSchemesWithVariants } from '@citric/core/utils/theme.types'
|
|
1
|
+
import { IconBox, OneOfColorSchemesWithVariants } from '@citric/core'
|
|
3
2
|
import { CheckCircleFill, ExclamationTriangleFill, InfoCircleFill, TimesCircleFill } from '@citric/icons'
|
|
4
3
|
import { toast } from 'react-toastify'
|
|
5
4
|
import 'react-toastify/dist/ReactToastify.css'
|
package/src/utils.ts
CHANGED
|
@@ -5,3 +5,97 @@ export function valueOfLayoutVar(varname: string): string {
|
|
|
5
5
|
if (!layout) return ''
|
|
6
6
|
return valueOf(varname, layout)
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Important for accessibility.
|
|
11
|
+
*
|
|
12
|
+
* Makes it so we focus the next focusable element in the DOM hierarchy, disregarding the element passed as parameter and its children.
|
|
13
|
+
*
|
|
14
|
+
* If there's no next focusable element, the first focusable of the page will be focused. If the page doesn't contain any focusable
|
|
15
|
+
* element, nothing happens.
|
|
16
|
+
*
|
|
17
|
+
* @param current the reference element to focus the next. If not provided, will be the currently active element.
|
|
18
|
+
*/
|
|
19
|
+
export function focusNextIgnoringChildren(current?: HTMLElement | null) {
|
|
20
|
+
current = current ?? document.activeElement as HTMLElement
|
|
21
|
+
while (current && !current.nextElementSibling) {
|
|
22
|
+
current = current?.parentElement
|
|
23
|
+
}
|
|
24
|
+
current = current?.nextElementSibling as HTMLElement
|
|
25
|
+
while (current && current.tabIndex < 0) {
|
|
26
|
+
current = (current.children.length ? current.firstChild : current.nextElementSibling) as HTMLElement
|
|
27
|
+
}
|
|
28
|
+
if (current) current?.focus?.()
|
|
29
|
+
else focusFirstChild(document)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other'
|
|
33
|
+
type TagPriorityElement = TagPriority | TagPriority[]
|
|
34
|
+
|
|
35
|
+
interface FocusOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Instead of focusing the first element overall, focus the first according to this list of priorities.
|
|
38
|
+
*/
|
|
39
|
+
priority?: TagPriorityElement[],
|
|
40
|
+
/**
|
|
41
|
+
* Ignores any element that matches this query selector.
|
|
42
|
+
*/
|
|
43
|
+
ignore?: string,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const selectors: Record<TagPriority, string> = {
|
|
47
|
+
a: 'a[href]:not(:disabled)',
|
|
48
|
+
button: 'button:not(:disabled)',
|
|
49
|
+
input: 'input:not(:disabled):not([type="hidden"])',
|
|
50
|
+
select: 'textarea:not(:disabled)',
|
|
51
|
+
textarea: 'select:not(:disabled)',
|
|
52
|
+
other: '[tabindex]:not([tabindex="-1"])',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Focus the first focusable child of the element provided. If the element has no focusable child, nothing happens.
|
|
57
|
+
*
|
|
58
|
+
* A priority list can be passed in the second parameter, as an option. If it's provided, it will focus the first element according to the
|
|
59
|
+
* list.
|
|
60
|
+
*
|
|
61
|
+
* An ignore query selector can also be passed in the options parameter. If the first focusable element matches the query selector, the
|
|
62
|
+
* next element is focused instead.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* Suppose the children of element are: h1, button, p, input, select.
|
|
66
|
+
* 1. We don't pass a priority list. The focused element will be the button.
|
|
67
|
+
* 2. Our priority list is ['button']. The focused element will be the button.
|
|
68
|
+
* 3. Our priority list is ['input', 'button']. The focused element will be the input.
|
|
69
|
+
* 4. Our priority list is ['select', 'input']. The focused element will be the select.
|
|
70
|
+
* 5. Our priority list is [['select', 'input'], 'button']. The focused element will be the input.
|
|
71
|
+
*
|
|
72
|
+
* @param element the element to search a child to focus.
|
|
73
|
+
* @param options optional.
|
|
74
|
+
*/
|
|
75
|
+
export function focusFirstChild(element: HTMLElement | Document | null | undefined, { priority = [], ignore }: FocusOptions = {}) {
|
|
76
|
+
let focusable: NodeListOf<HTMLElement> | null | undefined
|
|
77
|
+
let missing: TagPriority[] = ['a', 'button', 'input', 'other', 'select', 'textarea']
|
|
78
|
+
for (const p of priority) {
|
|
79
|
+
const tags = Array.isArray(p) ? p : [p]
|
|
80
|
+
const querySelectors = tags.map(t => {
|
|
81
|
+
missing = missing.filter(tag => tag != t)
|
|
82
|
+
return selectors[t]
|
|
83
|
+
})
|
|
84
|
+
focusable = element?.querySelectorAll(querySelectors.join(', '))
|
|
85
|
+
if (focusable) break
|
|
86
|
+
}
|
|
87
|
+
if (!focusable) {
|
|
88
|
+
element?.querySelectorAll(missing.map(t => selectors[t]).join(', '))
|
|
89
|
+
}
|
|
90
|
+
let elementToFocus: HTMLElement | undefined
|
|
91
|
+
for (const f of focusable ?? []) {
|
|
92
|
+
if (!ignore || !f.matches(ignore)) {
|
|
93
|
+
const styles = window.getComputedStyle(f)
|
|
94
|
+
if (styles.display != 'none' && styles.visibility != 'hidden') {
|
|
95
|
+
elementToFocus = f
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
elementToFocus?.focus?.()
|
|
101
|
+
}
|