@countermeasure-platform/web-components 1.3.3-dev.33.1 → 1.3.4-dev.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/component-D5sRm1fq.js +389 -0
- package/dist/component-D5sRm1fq.js.map +1 -0
- package/dist/components/index.js +27 -27
- package/dist/icons/index.d.ts +12 -0
- package/dist/icons/index.d.ts.map +1 -1
- package/dist/icons/index.js +12 -0
- package/dist/icons/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +127 -126
- package/dist/layout/app-shell.d.ts +50 -3
- package/dist/layout/app-shell.d.ts.map +1 -1
- package/dist/layout/app-shell.js +142 -13
- package/dist/layout/app-shell.js.map +1 -1
- package/dist/layout/core-app-chrome.d.ts +81 -0
- package/dist/layout/core-app-chrome.d.ts.map +1 -0
- package/dist/layout/core-app-chrome.js +349 -0
- package/dist/layout/core-app-chrome.js.map +1 -0
- package/dist/layout/index.d.ts +4 -2
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +88 -87
- package/dist/layout/index.js.map +1 -1
- package/dist/react/primitives/badge.d.ts +1 -1
- package/dist/react/primitives/button.d.ts +2 -2
- package/dist/react/primitives/copy-button.d.ts +1 -1
- package/dist/react/primitives/stat-card.d.ts +1 -1
- package/dist/react/primitives/toggle.d.ts +1 -1
- package/dist/react/sidebar.d.ts +4 -4
- package/dist/react/sidebar.d.ts.map +1 -1
- package/dist/react/sidebar.js +18 -16
- package/dist/react/sidebar.js.map +1 -1
- package/dist/sidebar/component.d.ts +24 -2
- package/dist/sidebar/component.d.ts.map +1 -1
- package/dist/sidebar/enhance.d.ts +8 -0
- package/dist/sidebar/enhance.d.ts.map +1 -1
- package/dist/sidebar/index.d.ts +3 -0
- package/dist/sidebar/index.d.ts.map +1 -1
- package/dist/sidebar/index.js +81 -28
- package/dist/sidebar/index.js.map +1 -1
- package/dist/sidebar/types.d.ts +126 -4
- package/dist/sidebar/types.d.ts.map +1 -1
- package/dist/styles/layout.css +252 -0
- package/dist/styles/sidebar.css +313 -5
- package/package.json +6 -1
- package/src/icons/icons.test.ts +9 -0
- package/src/icons/index.ts +12 -0
- package/src/index.ts +11 -0
- package/src/layout/app-shell.test.ts +204 -0
- package/src/layout/app-shell.ts +362 -3
- package/src/layout/core-app-chrome.test.ts +507 -0
- package/src/layout/core-app-chrome.ts +662 -0
- package/src/layout/index.ts +36 -2
- package/src/react/sidebar.test.tsx +104 -3
- package/src/react/sidebar.tsx +26 -4
- package/src/sidebar/component.test.ts +395 -1
- package/src/sidebar/component.ts +661 -86
- package/src/sidebar/enhance.ts +118 -0
- package/src/sidebar/index.ts +144 -0
- package/src/sidebar/types.ts +143 -4
- package/src/styles/layout.css +252 -0
- package/src/styles/sidebar.css +313 -5
- package/dist/component-Bxhxf21c.js +0 -167
- package/dist/component-Bxhxf21c.js.map +0 -1
package/src/sidebar/component.ts
CHANGED
|
@@ -2,27 +2,34 @@
|
|
|
2
2
|
* SidebarComponent - Programmatic sidebar with sections, badges, and footer
|
|
3
3
|
* @countermeasure/web-components/sidebar/component
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* import { SidebarComponent } from '@countermeasure/web-components/sidebar/component'
|
|
11
|
-
*
|
|
12
|
-
* const sidebar = new SidebarComponent({
|
|
13
|
-
* container: '#sidebar-root',
|
|
14
|
-
* sections: [{ id: 'main', label: 'Nav', items: [...] }],
|
|
15
|
-
* user: { name: 'Alice' },
|
|
16
|
-
* onNavigate: item => console.log(item.id),
|
|
17
|
-
* })
|
|
18
|
-
* sidebar.setActive('dashboard')
|
|
5
|
+
* Builds the sidebar DOM from a config object and owns the common app-shell
|
|
6
|
+
* behavior expected by CounterMeasure product surfaces: active route state,
|
|
7
|
+
* persisted collapse, mobile drawer state, keyboard navigation, tenant/scope
|
|
8
|
+
* selection, badges, and footer account chrome.
|
|
19
9
|
*/
|
|
20
10
|
|
|
21
11
|
import { createBrandLockupElement } from '../components/brand'
|
|
22
12
|
import { getIconSvgInner, type IconSet } from '../icons/index'
|
|
23
13
|
import { resolveContainer } from '../primitives/utils'
|
|
24
14
|
import { setSafeIconHtml } from '../utils/sanitize'
|
|
25
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type SidebarBadgeConfig,
|
|
17
|
+
type SidebarBadgeTone,
|
|
18
|
+
type SidebarComponentConfig,
|
|
19
|
+
type SidebarNavItem,
|
|
20
|
+
type SidebarSection,
|
|
21
|
+
} from './types'
|
|
22
|
+
|
|
23
|
+
const DEFAULT_MOBILE_BREAKPOINT = 900
|
|
24
|
+
const FALLBACK_URL_BASE = 'http://localhost/'
|
|
25
|
+
|
|
26
|
+
interface NormalizedBadge {
|
|
27
|
+
label: string
|
|
28
|
+
title?: string
|
|
29
|
+
tone: SidebarBadgeTone
|
|
30
|
+
visible: boolean
|
|
31
|
+
dot: boolean
|
|
32
|
+
}
|
|
26
33
|
|
|
27
34
|
function toSidebarVariantClassName(variant: string): string {
|
|
28
35
|
const normalized = variant
|
|
@@ -32,8 +39,72 @@ function toSidebarVariantClassName(variant: string): string {
|
|
|
32
39
|
return `sidebar--${normalized || 'app'}`
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
function toInitials(name: string): string {
|
|
43
|
+
const words = name.trim().split(/\s+/).filter(Boolean)
|
|
44
|
+
|
|
45
|
+
if (words.length >= 2) {
|
|
46
|
+
return `${words[0]?.charAt(0) ?? ''}${words[words.length - 1]?.charAt(0) ?? ''}`.toUpperCase()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return name.slice(0, 2).toUpperCase()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function canUseStorage(): boolean {
|
|
53
|
+
try {
|
|
54
|
+
return typeof window !== 'undefined' && window.localStorage !== undefined
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toPathname(value: string): string {
|
|
61
|
+
try {
|
|
62
|
+
return new URL(value, FALLBACK_URL_BASE).pathname
|
|
63
|
+
} catch {
|
|
64
|
+
return value.split(/[?#]/, 1)[0] ?? value
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isMobileViewport(breakpoint: number): boolean {
|
|
69
|
+
return typeof window !== 'undefined' && window.innerWidth <= breakpoint
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeBadge(
|
|
73
|
+
badge: number | SidebarBadgeConfig | undefined,
|
|
74
|
+
fallbackTone: SidebarBadgeTone | undefined
|
|
75
|
+
): NormalizedBadge | null {
|
|
76
|
+
if (badge === undefined) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof badge === 'number') {
|
|
81
|
+
return {
|
|
82
|
+
label: badge > 0 ? String(badge) : '',
|
|
83
|
+
tone: fallbackTone ?? 'primary',
|
|
84
|
+
visible: badge > 0,
|
|
85
|
+
dot: badge > 0,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const label = badge.label
|
|
90
|
+
const normalized: NormalizedBadge = {
|
|
91
|
+
label,
|
|
92
|
+
tone: badge.tone ?? fallbackTone ?? 'primary',
|
|
93
|
+
visible: label.length > 0 || badge.dot === true,
|
|
94
|
+
dot: badge.dot ?? label.length > 0,
|
|
95
|
+
}
|
|
96
|
+
if (badge.title !== undefined) {
|
|
97
|
+
normalized.title = badge.title
|
|
98
|
+
}
|
|
99
|
+
return normalized
|
|
100
|
+
}
|
|
101
|
+
|
|
35
102
|
/** Create an SVG element populated with icon content from the registry. */
|
|
36
|
-
function createIconSvg(
|
|
103
|
+
function createIconSvg(
|
|
104
|
+
iconSet: IconSet,
|
|
105
|
+
name: string,
|
|
106
|
+
className = 'sidebar__item-icon'
|
|
107
|
+
): SVGElement {
|
|
37
108
|
const svgNs = 'http://www.w3.org/2000/svg'
|
|
38
109
|
const svg = document.createElementNS(svgNs, 'svg')
|
|
39
110
|
svg.setAttribute('xmlns', svgNs)
|
|
@@ -45,7 +116,7 @@ function createIconSvg(iconSet: IconSet, name: string): SVGElement {
|
|
|
45
116
|
svg.setAttribute('stroke-width', '2')
|
|
46
117
|
svg.setAttribute('stroke-linecap', 'round')
|
|
47
118
|
svg.setAttribute('stroke-linejoin', 'round')
|
|
48
|
-
svg.classList.add(
|
|
119
|
+
svg.classList.add(className)
|
|
49
120
|
svg.setAttribute('data-icon', name)
|
|
50
121
|
svg.setAttribute('aria-hidden', 'true')
|
|
51
122
|
|
|
@@ -57,29 +128,57 @@ function createIconSvg(iconSet: IconSet, name: string): SVGElement {
|
|
|
57
128
|
return svg
|
|
58
129
|
}
|
|
59
130
|
|
|
131
|
+
function appendElements(
|
|
132
|
+
parent: HTMLElement,
|
|
133
|
+
elementOrElements?: HTMLElement | HTMLElement[]
|
|
134
|
+
): void {
|
|
135
|
+
if (elementOrElements === undefined) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const elements = Array.isArray(elementOrElements) ? elementOrElements : [elementOrElements]
|
|
140
|
+
for (const element of elements) {
|
|
141
|
+
parent.appendChild(element)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
60
145
|
export class SidebarComponent {
|
|
61
146
|
private readonly container: HTMLElement
|
|
62
147
|
private readonly aside: HTMLElement
|
|
63
148
|
private readonly config: SidebarComponentConfig
|
|
64
149
|
private readonly iconSet: IconSet
|
|
150
|
+
private readonly variant: string
|
|
151
|
+
private readonly cleanupFns: (() => void)[] = []
|
|
65
152
|
private collapseButton: HTMLButtonElement | null = null
|
|
153
|
+
private backdrop: HTMLElement | null = null
|
|
66
154
|
|
|
67
155
|
constructor(config: SidebarComponentConfig) {
|
|
68
156
|
this.config = config
|
|
69
157
|
this.container = resolveContainer(config.container)
|
|
70
158
|
this.iconSet = config.iconSet ?? 'lucide'
|
|
159
|
+
this.variant = config.variant ?? 'app'
|
|
71
160
|
|
|
72
161
|
this.aside = document.createElement('aside')
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
162
|
+
this.aside.classList.add('sidebar', toSidebarVariantClassName(this.variant))
|
|
163
|
+
if (this.variant === 'threat-library' || this.variant === 'cm') {
|
|
164
|
+
this.aside.classList.add('sidebar--product')
|
|
165
|
+
}
|
|
166
|
+
this.aside.setAttribute('data-sidebar', this.variant)
|
|
76
167
|
this.aside.setAttribute('data-open', String(config.open === true))
|
|
77
|
-
|
|
168
|
+
this.aside.setAttribute('aria-label', 'Primary navigation')
|
|
169
|
+
|
|
170
|
+
if (this.resolveInitialCollapsed()) {
|
|
78
171
|
this.aside.classList.add('sidebar--collapsed')
|
|
79
172
|
}
|
|
80
173
|
|
|
81
174
|
this.render()
|
|
82
175
|
this.container.appendChild(this.aside)
|
|
176
|
+
|
|
177
|
+
if (config.mobileBackdrop === true) {
|
|
178
|
+
this.renderBackdrop()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.setupBehavior()
|
|
83
182
|
}
|
|
84
183
|
|
|
85
184
|
private render(): void {
|
|
@@ -88,16 +187,26 @@ export class SidebarComponent {
|
|
|
88
187
|
}
|
|
89
188
|
|
|
90
189
|
this.renderHeader()
|
|
190
|
+
this.renderScope()
|
|
91
191
|
|
|
92
|
-
const body = document.createElement('
|
|
192
|
+
const body = document.createElement('nav')
|
|
93
193
|
body.classList.add('sidebar__body')
|
|
194
|
+
body.setAttribute('aria-label', 'Primary')
|
|
195
|
+
|
|
196
|
+
appendElements(body, this.config.beforeNav)
|
|
94
197
|
|
|
95
198
|
for (const section of this.config.sections) {
|
|
96
199
|
body.appendChild(this.buildSection(section))
|
|
97
200
|
}
|
|
98
201
|
|
|
202
|
+
appendElements(body, this.config.afterNav)
|
|
203
|
+
|
|
99
204
|
this.aside.appendChild(body)
|
|
100
205
|
this.renderFooter()
|
|
206
|
+
|
|
207
|
+
if (this.config.activeItemId !== undefined) {
|
|
208
|
+
this.setActive(this.config.activeItemId)
|
|
209
|
+
}
|
|
101
210
|
}
|
|
102
211
|
|
|
103
212
|
private buildSection(section: SidebarSection): HTMLElement {
|
|
@@ -161,46 +270,88 @@ export class SidebarComponent {
|
|
|
161
270
|
itemsWrap.id = `sidebar-group-${section.id}`
|
|
162
271
|
|
|
163
272
|
for (const item of section.items) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
link.setAttribute('data-item-id', item.id)
|
|
167
|
-
link.setAttribute('title', item.label)
|
|
168
|
-
|
|
169
|
-
if (item.icon) {
|
|
170
|
-
const icon = createIconSvg(this.iconSet, item.icon)
|
|
171
|
-
link.appendChild(icon)
|
|
172
|
-
}
|
|
273
|
+
itemsWrap.appendChild(this.buildItem(item))
|
|
274
|
+
}
|
|
173
275
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
276
|
+
sectionEl.appendChild(itemsWrap)
|
|
277
|
+
return sectionEl
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private buildItem(item: SidebarNavItem): HTMLElement {
|
|
281
|
+
const disabled = item.disabled === true
|
|
282
|
+
const element =
|
|
283
|
+
item.href !== undefined && !disabled
|
|
284
|
+
? document.createElement('a')
|
|
285
|
+
: disabled
|
|
286
|
+
? document.createElement('span')
|
|
287
|
+
: document.createElement('button')
|
|
288
|
+
|
|
289
|
+
element.classList.add('sidebar__item')
|
|
290
|
+
element.setAttribute('data-item-id', item.id)
|
|
291
|
+
element.setAttribute('data-nav-item', '')
|
|
292
|
+
element.setAttribute('title', item.tooltip ?? item.label)
|
|
293
|
+
|
|
294
|
+
if (item.ariaLabel !== undefined) {
|
|
295
|
+
element.setAttribute('aria-label', item.ariaLabel)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (disabled) {
|
|
299
|
+
element.setAttribute('aria-disabled', 'true')
|
|
300
|
+
element.setAttribute('role', 'link')
|
|
301
|
+
element.setAttribute('tabindex', '-1')
|
|
302
|
+
element.classList.add('sidebar__item--disabled')
|
|
303
|
+
} else if (element instanceof HTMLAnchorElement) {
|
|
304
|
+
element.href = item.href ?? '#'
|
|
305
|
+
if (item.target !== undefined) {
|
|
306
|
+
element.target = item.target
|
|
307
|
+
}
|
|
308
|
+
if (item.rel !== undefined) {
|
|
309
|
+
element.rel = item.rel
|
|
310
|
+
} else if (item.target === '_blank') {
|
|
311
|
+
element.rel = 'noreferrer noopener'
|
|
188
312
|
}
|
|
313
|
+
} else if (element instanceof HTMLButtonElement) {
|
|
314
|
+
element.type = 'button'
|
|
315
|
+
}
|
|
189
316
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
317
|
+
if (this.isItemActive(item)) {
|
|
318
|
+
element.classList.add('sidebar__item--active')
|
|
319
|
+
element.setAttribute('aria-current', 'page')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (item.icon) {
|
|
323
|
+
element.appendChild(createIconSvg(this.iconSet, item.icon))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const label = document.createElement('span')
|
|
327
|
+
label.classList.add('sidebar__item-label')
|
|
328
|
+
label.textContent = item.label
|
|
329
|
+
element.appendChild(label)
|
|
330
|
+
|
|
331
|
+
if (item.tag !== undefined) {
|
|
332
|
+
const tag = document.createElement('span')
|
|
333
|
+
tag.classList.add('sidebar__tag')
|
|
334
|
+
tag.textContent = item.tag
|
|
335
|
+
element.appendChild(tag)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const badge = normalizeBadge(item.badge, item.badgeTone)
|
|
339
|
+
if (badge?.visible === true) {
|
|
340
|
+
this.applyBadge(element, item.id, badge)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!disabled) {
|
|
344
|
+
element.addEventListener('click', event => {
|
|
345
|
+
this.config.onNavigate?.(item)
|
|
346
|
+
item.onClick?.(event)
|
|
347
|
+
|
|
348
|
+
if (this.config.closeOnNavigate !== false && this.isOpen()) {
|
|
349
|
+
this.setOpen(false)
|
|
196
350
|
}
|
|
197
351
|
})
|
|
198
|
-
|
|
199
|
-
itemsWrap.appendChild(link)
|
|
200
352
|
}
|
|
201
353
|
|
|
202
|
-
|
|
203
|
-
return sectionEl
|
|
354
|
+
return element
|
|
204
355
|
}
|
|
205
356
|
|
|
206
357
|
private renderHeader(): void {
|
|
@@ -252,6 +403,118 @@ export class SidebarComponent {
|
|
|
252
403
|
this.aside.appendChild(header)
|
|
253
404
|
}
|
|
254
405
|
|
|
406
|
+
private renderScope(): void {
|
|
407
|
+
const scope = this.config.scope
|
|
408
|
+
if (scope === undefined || scope.options.length === 0) {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const activeOption =
|
|
413
|
+
scope.options.find(option => option.id === scope.value) ?? scope.options[0] ?? null
|
|
414
|
+
const root = document.createElement('div')
|
|
415
|
+
root.classList.add('sidebar__scope')
|
|
416
|
+
|
|
417
|
+
const trigger = document.createElement('button')
|
|
418
|
+
trigger.type = 'button'
|
|
419
|
+
trigger.classList.add('sidebar__scope-trigger')
|
|
420
|
+
trigger.setAttribute('aria-haspopup', 'listbox')
|
|
421
|
+
trigger.setAttribute('aria-expanded', 'false')
|
|
422
|
+
trigger.setAttribute('title', activeOption?.label ?? scope.placeholder ?? 'Select scope')
|
|
423
|
+
|
|
424
|
+
if (scope.icon !== undefined) {
|
|
425
|
+
trigger.appendChild(createIconSvg(this.iconSet, scope.icon, 'sidebar__scope-icon'))
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const copy = document.createElement('span')
|
|
429
|
+
copy.classList.add('sidebar__scope-copy')
|
|
430
|
+
|
|
431
|
+
if (scope.label !== undefined) {
|
|
432
|
+
const eyebrow = document.createElement('span')
|
|
433
|
+
eyebrow.classList.add('sidebar__scope-label')
|
|
434
|
+
eyebrow.textContent = scope.label
|
|
435
|
+
copy.appendChild(eyebrow)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const current = document.createElement('span')
|
|
439
|
+
current.classList.add('sidebar__scope-current')
|
|
440
|
+
current.textContent = activeOption?.label ?? scope.placeholder ?? 'Select scope'
|
|
441
|
+
copy.appendChild(current)
|
|
442
|
+
trigger.appendChild(copy)
|
|
443
|
+
|
|
444
|
+
const chevron = createIconSvg(this.iconSet, 'chevron-down', 'sidebar__scope-chevron')
|
|
445
|
+
trigger.appendChild(chevron)
|
|
446
|
+
|
|
447
|
+
const menu = document.createElement('div')
|
|
448
|
+
menu.classList.add('sidebar__scope-menu')
|
|
449
|
+
menu.setAttribute('role', 'listbox')
|
|
450
|
+
menu.hidden = true
|
|
451
|
+
|
|
452
|
+
const setOpen = (open: boolean): void => {
|
|
453
|
+
trigger.setAttribute('aria-expanded', String(open))
|
|
454
|
+
menu.hidden = !open
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const option of scope.options) {
|
|
458
|
+
const optionButton = document.createElement('button')
|
|
459
|
+
optionButton.type = 'button'
|
|
460
|
+
optionButton.classList.add('sidebar__scope-option')
|
|
461
|
+
optionButton.setAttribute('role', 'option')
|
|
462
|
+
optionButton.setAttribute('data-option-id', option.id)
|
|
463
|
+
optionButton.setAttribute('aria-selected', String(option.id === activeOption?.id))
|
|
464
|
+
|
|
465
|
+
if (option.disabled === true) {
|
|
466
|
+
optionButton.disabled = true
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (option.status !== undefined) {
|
|
470
|
+
const dot = document.createElement('span')
|
|
471
|
+
dot.classList.add('sidebar__scope-dot', `sidebar__scope-dot--${option.status}`)
|
|
472
|
+
optionButton.appendChild(dot)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const label = document.createElement('span')
|
|
476
|
+
label.classList.add('sidebar__scope-option-label')
|
|
477
|
+
label.textContent = option.label
|
|
478
|
+
optionButton.appendChild(label)
|
|
479
|
+
|
|
480
|
+
const check = createIconSvg(this.iconSet, 'check', 'sidebar__scope-check')
|
|
481
|
+
optionButton.appendChild(check)
|
|
482
|
+
|
|
483
|
+
optionButton.addEventListener('click', () => {
|
|
484
|
+
current.textContent = option.label
|
|
485
|
+
trigger.setAttribute('title', option.label)
|
|
486
|
+
menu.querySelectorAll<HTMLElement>('[role="option"]').forEach(item => {
|
|
487
|
+
item.setAttribute(
|
|
488
|
+
'aria-selected',
|
|
489
|
+
String(item.getAttribute('data-option-id') === option.id)
|
|
490
|
+
)
|
|
491
|
+
})
|
|
492
|
+
setOpen(false)
|
|
493
|
+
scope.onChange?.(option)
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
menu.appendChild(optionButton)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
trigger.addEventListener('click', () => {
|
|
500
|
+
setOpen(menu.hidden === true)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const closeOnOutsidePointer = (event: PointerEvent): void => {
|
|
504
|
+
if (!root.contains(event.target as Node)) {
|
|
505
|
+
setOpen(false)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
document.addEventListener('pointerdown', closeOnOutsidePointer)
|
|
509
|
+
this.cleanupFns.push(() => {
|
|
510
|
+
document.removeEventListener('pointerdown', closeOnOutsidePointer)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
root.appendChild(trigger)
|
|
514
|
+
root.appendChild(menu)
|
|
515
|
+
this.aside.appendChild(root)
|
|
516
|
+
}
|
|
517
|
+
|
|
255
518
|
private renderCollapseButton(): void {
|
|
256
519
|
const button = document.createElement('button')
|
|
257
520
|
button.type = 'button'
|
|
@@ -300,15 +563,57 @@ export class SidebarComponent {
|
|
|
300
563
|
footerBar.classList.add('sidebar__footer-bar')
|
|
301
564
|
|
|
302
565
|
if (hasUser && this.config.user) {
|
|
303
|
-
const
|
|
304
|
-
|
|
566
|
+
const user = this.config.user
|
|
567
|
+
const userEl =
|
|
568
|
+
user.onClick !== undefined
|
|
569
|
+
? document.createElement('button')
|
|
570
|
+
: document.createElement('span')
|
|
571
|
+
userEl.classList.add('sidebar__user-trigger', 'sidebar__footer-user')
|
|
572
|
+
|
|
573
|
+
const userClick = user.onClick
|
|
574
|
+
if (userEl instanceof HTMLButtonElement && userClick !== undefined) {
|
|
575
|
+
userEl.type = 'button'
|
|
576
|
+
userEl.setAttribute('aria-label', user.label ?? `User menu for ${user.name}`)
|
|
577
|
+
userEl.addEventListener('click', userClick)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const avatar = document.createElement('span')
|
|
581
|
+
avatar.classList.add('sidebar__avatar', 'sidebar__footer-avatar')
|
|
582
|
+
|
|
583
|
+
if (user.avatarUrl !== undefined) {
|
|
584
|
+
const image = document.createElement('img')
|
|
585
|
+
image.src = user.avatarUrl
|
|
586
|
+
image.alt = ''
|
|
587
|
+
avatar.appendChild(image)
|
|
588
|
+
} else {
|
|
589
|
+
avatar.textContent = user.initials ?? toInitials(user.name)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (user.presence !== undefined && user.presence !== 'offline') {
|
|
593
|
+
const presence = document.createElement('span')
|
|
594
|
+
presence.classList.add('sidebar__presence', `sidebar__presence--${user.presence}`)
|
|
595
|
+
presence.setAttribute('aria-hidden', 'true')
|
|
596
|
+
avatar.appendChild(presence)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const details = document.createElement('span')
|
|
600
|
+
details.classList.add('sidebar__user-details')
|
|
305
601
|
|
|
306
602
|
const nameSpan = document.createElement('span')
|
|
307
|
-
nameSpan.classList.add('sidebar__footer-name')
|
|
308
|
-
nameSpan.textContent =
|
|
309
|
-
|
|
603
|
+
nameSpan.classList.add('sidebar__user-name', 'sidebar__footer-name')
|
|
604
|
+
nameSpan.textContent = user.name
|
|
605
|
+
details.appendChild(nameSpan)
|
|
606
|
+
|
|
607
|
+
if (user.detail !== undefined) {
|
|
608
|
+
const detail = document.createElement('span')
|
|
609
|
+
detail.classList.add('sidebar__user-status')
|
|
610
|
+
detail.textContent = user.detail
|
|
611
|
+
details.appendChild(detail)
|
|
612
|
+
}
|
|
310
613
|
|
|
311
|
-
|
|
614
|
+
userEl.appendChild(avatar)
|
|
615
|
+
userEl.appendChild(details)
|
|
616
|
+
footerBar.appendChild(userEl)
|
|
312
617
|
}
|
|
313
618
|
|
|
314
619
|
if (hasActions && this.config.footerActions) {
|
|
@@ -317,11 +622,13 @@ export class SidebarComponent {
|
|
|
317
622
|
|
|
318
623
|
for (const action of this.config.footerActions) {
|
|
319
624
|
const button = document.createElement('button')
|
|
625
|
+
button.type = 'button'
|
|
320
626
|
button.classList.add('sidebar__footer-icon')
|
|
321
627
|
if (action.danger === true) {
|
|
322
628
|
button.classList.add('sidebar__footer-icon--danger')
|
|
323
629
|
}
|
|
324
630
|
button.setAttribute('aria-label', action.label)
|
|
631
|
+
button.setAttribute('title', action.label)
|
|
325
632
|
|
|
326
633
|
const icon = createIconSvg(this.iconSet, action.icon)
|
|
327
634
|
button.appendChild(icon)
|
|
@@ -343,73 +650,341 @@ export class SidebarComponent {
|
|
|
343
650
|
this.aside.appendChild(footer)
|
|
344
651
|
}
|
|
345
652
|
|
|
653
|
+
private renderBackdrop(): void {
|
|
654
|
+
const backdrop = document.createElement('button')
|
|
655
|
+
backdrop.type = 'button'
|
|
656
|
+
backdrop.classList.add('sidebar__backdrop')
|
|
657
|
+
backdrop.setAttribute('data-sidebar-backdrop', this.variant)
|
|
658
|
+
backdrop.setAttribute('data-visible', String(this.isOpen()))
|
|
659
|
+
backdrop.setAttribute('aria-label', 'Close sidebar')
|
|
660
|
+
backdrop.addEventListener('click', () => {
|
|
661
|
+
this.setOpen(false)
|
|
662
|
+
})
|
|
663
|
+
this.backdrop = backdrop
|
|
664
|
+
this.container.appendChild(backdrop)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private setupBehavior(): void {
|
|
668
|
+
this.setupKeyboardNavigation()
|
|
669
|
+
this.setupMobileToggles()
|
|
670
|
+
|
|
671
|
+
const handleEscape = (event: KeyboardEvent): void => {
|
|
672
|
+
if (event.key === 'Escape') {
|
|
673
|
+
if (this.isOpen()) {
|
|
674
|
+
this.setOpen(false)
|
|
675
|
+
}
|
|
676
|
+
this.closeScopeMenus()
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
document.addEventListener('keydown', handleEscape)
|
|
680
|
+
this.cleanupFns.push(() => {
|
|
681
|
+
document.removeEventListener('keydown', handleEscape)
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
const handleResize = (): void => {
|
|
685
|
+
if (isMobileViewport(this.getMobileBreakpoint())) {
|
|
686
|
+
this.setOpen(false)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
window.addEventListener('resize', handleResize)
|
|
690
|
+
this.cleanupFns.push(() => {
|
|
691
|
+
window.removeEventListener('resize', handleResize)
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private setupKeyboardNavigation(): void {
|
|
696
|
+
const handler = (event: KeyboardEvent): void => {
|
|
697
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
|
|
698
|
+
|
|
699
|
+
const items = Array.from(
|
|
700
|
+
this.aside.querySelectorAll<HTMLElement>('[data-nav-item]:not([aria-disabled="true"])')
|
|
701
|
+
).filter(item => !item.hidden && item.offsetParent !== null)
|
|
702
|
+
|
|
703
|
+
if (items.length === 0) return
|
|
704
|
+
|
|
705
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLElement)
|
|
706
|
+
if (currentIndex === -1) return
|
|
707
|
+
|
|
708
|
+
event.preventDefault()
|
|
709
|
+
const nextIndex =
|
|
710
|
+
event.key === 'ArrowDown'
|
|
711
|
+
? (currentIndex + 1) % items.length
|
|
712
|
+
: (currentIndex - 1 + items.length) % items.length
|
|
713
|
+
items[nextIndex]?.focus()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
this.aside.addEventListener('keydown', handler)
|
|
717
|
+
this.cleanupFns.push(() => {
|
|
718
|
+
this.aside.removeEventListener('keydown', handler)
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private setupMobileToggles(): void {
|
|
723
|
+
const toggles = Array.from(
|
|
724
|
+
document.querySelectorAll<HTMLButtonElement>('[data-sidebar-mobile-toggle]')
|
|
725
|
+
).filter(toggle => {
|
|
726
|
+
const target = toggle.getAttribute('data-sidebar-mobile-toggle')
|
|
727
|
+
return target === null || target === '' || target === this.variant
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
for (const toggle of toggles) {
|
|
731
|
+
const handler = (): void => {
|
|
732
|
+
this.setOpen(!this.isOpen())
|
|
733
|
+
}
|
|
734
|
+
toggle.addEventListener('click', handler)
|
|
735
|
+
this.cleanupFns.push(() => {
|
|
736
|
+
toggle.removeEventListener('click', handler)
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
346
741
|
/** Mark a nav item as active, removing active state from all others. */
|
|
347
742
|
setActive(itemId: string): void {
|
|
743
|
+
this.config.activeItemId = itemId
|
|
348
744
|
this.aside.querySelectorAll<HTMLElement>('.sidebar__item').forEach(el => {
|
|
349
745
|
el.classList.remove('sidebar__item--active')
|
|
746
|
+
el.removeAttribute('aria-current')
|
|
350
747
|
})
|
|
351
748
|
|
|
352
|
-
const target = this.aside.
|
|
749
|
+
const target = Array.from(this.aside.querySelectorAll<HTMLElement>('[data-item-id]')).find(
|
|
750
|
+
item => item.getAttribute('data-item-id') === itemId
|
|
751
|
+
)
|
|
353
752
|
if (target) {
|
|
354
753
|
target.classList.add('sidebar__item--active')
|
|
754
|
+
target.setAttribute('aria-current', 'page')
|
|
355
755
|
}
|
|
356
756
|
}
|
|
357
757
|
|
|
758
|
+
/** Recompute active route state from a supplied pathname or the current location. */
|
|
759
|
+
setPathname(pathname: string | undefined): void {
|
|
760
|
+
if (pathname === undefined) {
|
|
761
|
+
delete this.config.pathname
|
|
762
|
+
} else {
|
|
763
|
+
this.config.pathname = pathname
|
|
764
|
+
}
|
|
765
|
+
delete this.config.activeItemId
|
|
766
|
+
this.syncActiveRouteState()
|
|
767
|
+
}
|
|
768
|
+
|
|
358
769
|
/** Update the badge count for a nav item (empty string when count is 0). */
|
|
359
|
-
updateBadge(itemId: string, count: number): void {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
770
|
+
updateBadge(itemId: string, count: number, tone?: SidebarBadgeTone): void {
|
|
771
|
+
const item = Array.from(this.aside.querySelectorAll<HTMLElement>('[data-item-id]')).find(
|
|
772
|
+
element => element.getAttribute('data-item-id') === itemId
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
if (!item) {
|
|
776
|
+
return
|
|
365
777
|
}
|
|
778
|
+
|
|
779
|
+
this.applyBadge(
|
|
780
|
+
item,
|
|
781
|
+
itemId,
|
|
782
|
+
normalizeBadge(
|
|
783
|
+
count,
|
|
784
|
+
tone ?? (item.getAttribute('data-badge-tone') as SidebarBadgeTone | null) ?? undefined
|
|
785
|
+
)
|
|
786
|
+
)
|
|
366
787
|
}
|
|
367
788
|
|
|
368
789
|
/** Toggle the sidebar between collapsed and expanded states. */
|
|
369
790
|
toggleCollapse(): void {
|
|
370
|
-
|
|
371
|
-
this.updateCollapseButton()
|
|
372
|
-
if (this.config.onCollapse) {
|
|
373
|
-
this.config.onCollapse(isCollapsed)
|
|
374
|
-
}
|
|
791
|
+
this.setCollapsed(!this.isCollapsed())
|
|
375
792
|
}
|
|
376
793
|
|
|
377
794
|
/** Collapse the sidebar. */
|
|
378
795
|
collapse(): void {
|
|
379
|
-
|
|
380
|
-
this.aside.classList.add('sidebar--collapsed')
|
|
381
|
-
this.updateCollapseButton()
|
|
382
|
-
if (!wasCollapsed && this.config.onCollapse) {
|
|
383
|
-
this.config.onCollapse(true)
|
|
384
|
-
}
|
|
796
|
+
this.setCollapsed(true)
|
|
385
797
|
}
|
|
386
798
|
|
|
387
799
|
/** Expand the sidebar. */
|
|
388
800
|
expand(): void {
|
|
389
|
-
|
|
390
|
-
this.aside.classList.remove('sidebar--collapsed')
|
|
391
|
-
this.updateCollapseButton()
|
|
392
|
-
if (wasCollapsed && this.config.onCollapse) {
|
|
393
|
-
this.config.onCollapse(false)
|
|
394
|
-
}
|
|
801
|
+
this.setCollapsed(false)
|
|
395
802
|
}
|
|
396
803
|
|
|
397
804
|
/** Set the mobile open state reflected by the shared CSS contract. */
|
|
398
805
|
setOpen(open: boolean): void {
|
|
806
|
+
const wasOpen = this.isOpen()
|
|
399
807
|
this.aside.setAttribute('data-open', String(open))
|
|
808
|
+
this.backdrop?.setAttribute('data-visible', String(open))
|
|
809
|
+
|
|
810
|
+
if (wasOpen !== open) {
|
|
811
|
+
this.config.onOpenChange?.(open)
|
|
812
|
+
}
|
|
400
813
|
}
|
|
401
814
|
|
|
402
815
|
/** Remove the sidebar from the DOM. */
|
|
403
816
|
destroy(): void {
|
|
817
|
+
for (const cleanup of this.cleanupFns.splice(0)) {
|
|
818
|
+
cleanup()
|
|
819
|
+
}
|
|
820
|
+
this.backdrop?.remove()
|
|
404
821
|
this.aside.remove()
|
|
405
822
|
}
|
|
406
823
|
|
|
824
|
+
private applyBadge(item: HTMLElement, itemId: string, badge: NormalizedBadge | null): void {
|
|
825
|
+
if (badge === null) {
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
let badgeEl = item.querySelector<HTMLElement>(`[data-badge="${itemId}"]`)
|
|
830
|
+
if (badgeEl === null) {
|
|
831
|
+
badgeEl = document.createElement('span')
|
|
832
|
+
badgeEl.classList.add('sidebar__badge')
|
|
833
|
+
badgeEl.setAttribute('data-badge', itemId)
|
|
834
|
+
item.appendChild(badgeEl)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
badgeEl.className = `sidebar__badge sidebar__badge--${badge.tone}`
|
|
838
|
+
badgeEl.textContent = badge.label
|
|
839
|
+
if (badge.title !== undefined) {
|
|
840
|
+
badgeEl.setAttribute('title', badge.title)
|
|
841
|
+
} else {
|
|
842
|
+
badgeEl.removeAttribute('title')
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
item.setAttribute('data-badge-visible', String(badge.visible))
|
|
846
|
+
item.setAttribute('data-badge-dot', String(badge.dot))
|
|
847
|
+
item.setAttribute('data-badge-tone', badge.tone)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private setCollapsed(collapsed: boolean): void {
|
|
851
|
+
const wasCollapsed = this.isCollapsed()
|
|
852
|
+
this.aside.classList.toggle('sidebar--collapsed', collapsed)
|
|
853
|
+
this.persistCollapsed(collapsed)
|
|
854
|
+
this.updateCollapseButton()
|
|
855
|
+
|
|
856
|
+
if (wasCollapsed !== collapsed) {
|
|
857
|
+
this.config.onCollapse?.(collapsed)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private isCollapsed(): boolean {
|
|
862
|
+
return this.aside.classList.contains('sidebar--collapsed')
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private isOpen(): boolean {
|
|
866
|
+
return this.aside.getAttribute('data-open') === 'true'
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private getMobileBreakpoint(): number {
|
|
870
|
+
return this.config.mobileBreakpoint ?? DEFAULT_MOBILE_BREAKPOINT
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private resolveInitialCollapsed(): boolean {
|
|
874
|
+
if (this.config.storageKey && this.config.persistCollapsed !== false && canUseStorage()) {
|
|
875
|
+
try {
|
|
876
|
+
if (!isMobileViewport(this.getMobileBreakpoint())) {
|
|
877
|
+
return window.localStorage.getItem(this.config.storageKey) === 'true'
|
|
878
|
+
}
|
|
879
|
+
} catch {
|
|
880
|
+
return this.config.collapsed === true
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return this.config.collapsed === true
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
private persistCollapsed(collapsed: boolean): void {
|
|
888
|
+
if (!this.config.storageKey || this.config.persistCollapsed === false || !canUseStorage()) {
|
|
889
|
+
return
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
try {
|
|
893
|
+
window.localStorage.setItem(this.config.storageKey, String(collapsed))
|
|
894
|
+
} catch {
|
|
895
|
+
// Collapse state remains applied in memory when storage is unavailable.
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private getCurrentPathname(): string | null {
|
|
900
|
+
if (this.config.pathname !== undefined) {
|
|
901
|
+
return toPathname(this.config.pathname)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (typeof window === 'undefined') {
|
|
905
|
+
return null
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return window.location.pathname
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private isItemActive(item: SidebarNavItem): boolean {
|
|
912
|
+
if (item.active === true || item.id === this.config.activeItemId) {
|
|
913
|
+
return true
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const pathname = this.getCurrentPathname()
|
|
917
|
+
if (pathname === null) {
|
|
918
|
+
return false
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const matcher = item.activeMatch
|
|
922
|
+
|
|
923
|
+
if (typeof matcher === 'function') {
|
|
924
|
+
return matcher(pathname, item)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (matcher instanceof RegExp) {
|
|
928
|
+
return matcher.test(pathname)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (typeof matcher === 'string' && matcher !== 'exact' && matcher !== 'prefix') {
|
|
932
|
+
return pathname === matcher || pathname.startsWith(`${matcher.replace(/\/$/, '')}/`)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (item.href === undefined) {
|
|
936
|
+
return false
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
const baseUrl = typeof window === 'undefined' ? FALLBACK_URL_BASE : window.location.href
|
|
941
|
+
const hrefPathname = new URL(item.href, baseUrl).pathname
|
|
942
|
+
if (matcher === 'exact' || item.end === true) {
|
|
943
|
+
return pathname === hrefPathname
|
|
944
|
+
}
|
|
945
|
+
return pathname === hrefPathname || pathname.startsWith(`${hrefPathname.replace(/\/$/, '')}/`)
|
|
946
|
+
} catch {
|
|
947
|
+
return false
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private syncActiveRouteState(): void {
|
|
952
|
+
this.aside.querySelectorAll<HTMLElement>('.sidebar__item').forEach(el => {
|
|
953
|
+
el.classList.remove('sidebar__item--active')
|
|
954
|
+
el.removeAttribute('aria-current')
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
const active = Array.from(this.aside.querySelectorAll<HTMLElement>('[data-item-id]')).find(
|
|
958
|
+
element => {
|
|
959
|
+
const itemId = element.getAttribute('data-item-id')
|
|
960
|
+
const item = this.config.sections
|
|
961
|
+
.flatMap(section => section.items)
|
|
962
|
+
.find(candidate => candidate.id === itemId)
|
|
963
|
+
return item === undefined ? false : this.isItemActive(item)
|
|
964
|
+
}
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
if (active !== undefined) {
|
|
968
|
+
active.classList.add('sidebar__item--active')
|
|
969
|
+
active.setAttribute('aria-current', 'page')
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private closeScopeMenus(): void {
|
|
974
|
+
this.aside.querySelectorAll<HTMLElement>('.sidebar__scope-menu').forEach(menu => {
|
|
975
|
+
menu.hidden = true
|
|
976
|
+
})
|
|
977
|
+
this.aside.querySelectorAll<HTMLElement>('.sidebar__scope-trigger').forEach(trigger => {
|
|
978
|
+
trigger.setAttribute('aria-expanded', 'false')
|
|
979
|
+
})
|
|
980
|
+
}
|
|
981
|
+
|
|
407
982
|
private updateCollapseButton(): void {
|
|
408
983
|
if (this.collapseButton === null) {
|
|
409
984
|
return
|
|
410
985
|
}
|
|
411
986
|
|
|
412
|
-
const isCollapsed = this.
|
|
987
|
+
const isCollapsed = this.isCollapsed()
|
|
413
988
|
const label = isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'
|
|
414
989
|
this.collapseButton.setAttribute('aria-label', label)
|
|
415
990
|
this.collapseButton.setAttribute('aria-expanded', String(!isCollapsed))
|