@brightspot/ui 6.7.0 → 6.7.1-pr.405.2026.10.26

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.
@@ -1 +1 @@
1
- {"generatedAt":1792606322020,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":true,"hasStorybookEslint":true,"refCount":0,"knownPackages":{"testPackages":{"@storybook/addon-vitest":"10.2.12","@vitest/browser-playwright":"4.1.11","playwright":"1.58.2","vitest":"4.1.11"},"stylingPackages":{"tailwindcss":"^3.4.13","autoprefixer":"10.4.21"}},"hasRouterPackage":false,"packageManager":{"type":"yarn","agent":"yarn","nodeLinker":"undefined"},"preview":{"usesGlobals":true},"framework":{"name":"@storybook/web-components-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/web-components","portableStoriesFileCount":0,"applicationFileCount":6,"storybookVersion":"10.2.12","storybookVersionSpecifier":"10.2.12","language":"typescript","storybookPackages":{"@storybook/addon-themes":{"version":"10.2.12"},"@storybook/web-components-vite":{"version":"10.2.12"},"@wc-toolkit/storybook-helpers":{"version":"10.0.0"},"eslint-plugin-storybook":{"version":"10.0.7"},"storybook":{}},"addons":{"@storybook/addon-docs":{"version":"10.2.12"},"@storybook/addon-vitest":{"version":"10.2.12"}},"packageJsonType":"module"}
1
+ {"generatedAt":1793014295499,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":true,"hasStorybookEslint":true,"refCount":0,"knownPackages":{"testPackages":{"@storybook/addon-vitest":"10.2.12","@vitest/browser-playwright":"4.1.11","playwright":"1.58.2","vitest":"4.1.11"},"stylingPackages":{"tailwindcss":"^3.4.13","autoprefixer":"10.4.21"}},"hasRouterPackage":false,"packageManager":{"type":"yarn","agent":"yarn","nodeLinker":"undefined"},"preview":{"usesGlobals":true},"framework":{"name":"@storybook/web-components-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/web-components","portableStoriesFileCount":0,"applicationFileCount":6,"storybookVersion":"10.2.12","storybookVersionSpecifier":"10.2.12","language":"typescript","storybookPackages":{"@storybook/addon-themes":{"version":"10.2.12"},"@storybook/web-components-vite":{"version":"10.2.12"},"@wc-toolkit/storybook-helpers":{"version":"10.0.0"},"eslint-plugin-storybook":{"version":"10.0.7"},"storybook":{}},"addons":{"@storybook/addon-docs":{"version":"10.2.12"},"@storybook/addon-vitest":{"version":"10.2.12"}},"packageJsonType":"module"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspot/ui",
3
- "version": "6.7.0",
3
+ "version": "6.7.1-pr.405.2026.10.26",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "description": "A UI library for building Brightspot CMS components.",
@@ -3,7 +3,7 @@
3
3
  }
4
4
 
5
5
  .ListManager-widget {
6
- @apply absolute -top-10 left-16 z-10 flex flex-col overflow-clip rounded-2xl border border-gray-100 bg-white shadow-lg;
6
+ @apply absolute -top-10 start-16 z-10 flex flex-col overflow-clip rounded-2xl border border-gray-100 bg-white shadow-lg;
7
7
 
8
8
  .Widget-title {
9
9
  @apply static !pe-[60px] text-sm;
@@ -25,3 +25,24 @@
25
25
  }
26
26
  }
27
27
  }
28
+
29
+ /* Anchor the menu to the list's first item where supported; browsers without
30
+ anchor positioning drop these declarations and keep the base -top-10/start-16
31
+ placement. anchor-scope keeps the name private to each <ol> so it resolves to
32
+ that list's own first item. The menu opens as a direct child of the <ol> (see
33
+ ListManager.js), making it a sibling of this anchor. inset-inline-start stays
34
+ logical so the RTL build doesn't flip it to a physical right edge. */
35
+ .ProseMirror ol {
36
+ anchor-scope: --list-first-item;
37
+ }
38
+
39
+ .ProseMirror ol > li:first-child {
40
+ anchor-name: --list-first-item;
41
+ }
42
+
43
+ .ListManager-widget {
44
+ position-anchor: --list-first-item;
45
+ top: anchor(top);
46
+ inset-inline-start: anchor(start);
47
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
48
+ }
@@ -7,11 +7,11 @@
7
7
  }
8
8
 
9
9
  .SkipLinks-heading {
10
- @apply btu-heading-5 px-4 py-2 text-gray-900;
10
+ @apply btu-heading-5 flex w-full px-4 py-2 text-gray-900;
11
11
  }
12
12
 
13
13
  .SkipLinks-note {
14
- @apply border-t border-t-gray-200 px-4 pb-2 pt-4 text-sm text-gray-500;
14
+ @apply flex w-full border-t border-t-gray-200 px-4 pb-2 pt-4 text-sm text-gray-500;
15
15
  }
16
16
 
17
17
  .SkipLinks-list {
@@ -19,5 +19,5 @@
19
19
  }
20
20
 
21
21
  .SkipLinks-link {
22
- @apply btu-button btu-button-fill-none btu-button-primary w-full justify-start bg-white;
22
+ @apply btu-button btu-button-fill-none btu-button-primary w-full justify-start bg-white no-underline;
23
23
  }
@@ -1,112 +1,121 @@
1
1
  import './SkipLinks.css'
2
2
  import onFind from './dom/onFind'
3
- import { focusRegionManager } from './FocusRegions'
3
+ import { focusRegionManager, type FocusRegionConfig } from './FocusRegions'
4
4
  import create from './dom/create'
5
- import { localizeShortcut } from './platform'
6
5
 
7
- onFind('.aria--skiptoContent', (originalButton: HTMLElement) => {
8
- let nav: HTMLElement | null = null
9
- let list: HTMLElement | null = null
10
- let note: HTMLElement | null = null
11
- let lastRegionKey = ''
6
+ // The nav (static landmark links) is server-rendered; we enhance activation via the focus
7
+ // region manager and append the other currently-visible regions the server can't know about.
12
8
 
13
- const skipToPrefix = window.BRIGHTSPOT?.ui.tooltips.skipLinks.heading
9
+ // Rendered statically by the backend — skipped when appending dynamic regions.
10
+ const STATIC_REGION_IDS = new Set(['page-header', 'page-nav', 'page-main'])
14
11
 
15
- originalButton.addEventListener('focus', () => {
16
- const layer = focusRegionManager.getTopLayer()
17
- const regions = focusRegionManager.getVisibleRegions(layer)
12
+ onFind('.SkipLinks', (nav: HTMLElement) => {
13
+ const list = nav.querySelector<HTMLElement>('.SkipLinks-list')
14
+ if (!list) return
15
+
16
+ const skipToPrefix =
17
+ nav.querySelector('.SkipLinks-heading')?.textContent?.trim() ?? ''
18
+
19
+ // Signature of the rendered dynamic links, so we only rebuild — and risk disturbing
20
+ // focus — when the visible region set actually changes.
21
+ let lastKey = ''
18
22
 
19
- // Grab the visible regions and filter for those with labels
20
- const labeledRegions = regions
23
+ const refreshDynamicLinks = (): void => {
24
+ const layer = focusRegionManager.getTopLayer()
25
+ const regions = focusRegionManager
26
+ .getVisibleRegions(layer)
27
+ .filter((config) => !STATIC_REGION_IDS.has(config.id))
21
28
  .map((config) => ({
22
29
  config,
23
30
  label: focusRegionManager.getRegionLabel(config),
24
31
  }))
25
32
  .filter(
26
- (entry): entry is { config: typeof entry.config; label: string } =>
27
- entry.label !== null,
33
+ (region): region is { config: FocusRegionConfig; label: string } =>
34
+ region.label !== null,
28
35
  )
29
36
 
30
- // No labeled regions — leave the original button as-is
31
- if (labeledRegions.length === 0) return
32
-
33
- // Skip rebuild if the set of visible labeled regions hasn't changed
34
- const regionKey = labeledRegions
35
- .map((r) => `${r.config.id}:${r.label}`)
36
- .join('|')
37
- const needsRebuild = regionKey !== lastRegionKey
38
-
39
- // Build the panel shell once
40
- if (!nav) {
41
- nav = create('nav', {
42
- className: 'SkipLinks',
43
- 'aria-label': skipToPrefix,
44
- })
45
-
46
- const heading = create('h2', {
47
- className: 'SkipLinks-heading',
48
- textContent: skipToPrefix,
49
- })
37
+ const key = regions.map((r) => `${r.config.id}:${r.label}`).join('|')
38
+ if (key === lastKey) return
39
+ lastKey = key
40
+
41
+ const active = document.activeElement
42
+ const rebuildingFocusedLink =
43
+ active instanceof HTMLElement &&
44
+ list.contains(active) &&
45
+ active.closest('[data-skip-dynamic]') !== null
46
+
47
+ list
48
+ .querySelectorAll('[data-skip-dynamic]')
49
+ .forEach((item) => item.remove())
50
+
51
+ for (const { config, label } of regions) {
52
+ // Match the static links: an <a href="#id">. A hrefless <a> (or a <button>, which
53
+ // Safari omits from Tab order) wouldn't be reliably tab-reachable. Give the region an
54
+ // id to point at when it lacks one.
55
+ if (!config.element.id) {
56
+ config.element.id = `skipRegion-${config.id}`
57
+ }
50
58
 
51
- const rawSkipNote = window.BRIGHTSPOT?.ui.tooltips.skipLinks.note
52
- const skipNote = rawSkipNote ?? ''
53
- note = create('p', {
54
- className: 'SkipLinks-note',
55
- textContent: localizeShortcut(skipNote),
59
+ const link = create('a', {
60
+ className: 'SkipLinks-link',
61
+ href: `#${config.element.id}`,
62
+ 'data-skip-region': config.id,
63
+ 'aria-label': `${skipToPrefix} ${label}`,
64
+ textContent: label,
56
65
  })
57
66
 
58
- list = create('ul', {
59
- className: 'SkipLinks-list',
60
- })
67
+ const item = create('li', { 'data-skip-dynamic': '' })
68
+ item.append(link)
69
+ list.append(item)
70
+ }
61
71
 
62
- nav.append(heading, note, list)
63
- originalButton.insertAdjacentElement('beforebegin', nav)
72
+ // The rebuild removed the link that had focus; keep focus in the menu.
73
+ if (rebuildingFocusedLink) {
74
+ list.querySelector<HTMLElement>('.SkipLinks-link')?.focus()
64
75
  }
76
+ }
65
77
 
66
- // Rebuild links only when regions have changed
67
- if (needsRebuild && list) {
68
- lastRegionKey = regionKey
69
- list.innerHTML = ''
70
-
71
- for (const { config, label } of labeledRegions) {
72
- const li = create('li')
73
- const link = create('button', {
74
- className: 'SkipLinks-link',
75
- type: 'button',
76
- textContent: label,
77
- 'aria-label': `${skipToPrefix} ${label}`,
78
- })
79
- link.addEventListener('click', () => {
80
- if (!config.element.isConnected) return
81
- focusRegionManager.focusRegion(config, document.body)
82
- })
83
- li.append(link)
84
- list.append(li)
85
- }
78
+ nav.addEventListener('click', (e) => {
79
+ // Let modified / non-primary clicks (open-in-new-tab etc.) use the native href.
80
+ if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) {
81
+ return
86
82
  }
87
83
 
88
- // Hide the original button and move focus to the first link
89
- originalButton.hidden = true
90
- nav.removeAttribute('aria-hidden')
91
- const firstLink = nav.querySelector<HTMLElement>('.SkipLinks-link')
92
- firstLink?.focus()
93
- })
84
+ const link = (e.target as HTMLElement).closest<HTMLElement>(
85
+ '[data-skip-region]',
86
+ )
87
+ if (!link) return
94
88
 
95
- // Dismiss with Escape key
96
- document.addEventListener('keydown', (e) => {
97
- if (!nav || e.key !== 'Escape') return
98
- if (nav.contains(document.activeElement)) {
99
- originalButton.hidden = false
100
- originalButton.focus()
89
+ const layer = focusRegionManager.getTopLayer()
90
+ const config = focusRegionManager
91
+ .getVisibleRegions(layer)
92
+ .find((region) => region.id === link.dataset.skipRegion)
93
+
94
+ if (config) {
95
+ e.preventDefault()
96
+ focusRegionManager.focusRegion(config, layer)
97
+ return
101
98
  }
102
- })
103
99
 
104
- // When focus leaves the panel, re-show the original button for next time
105
- document.addEventListener('focusin', (e) => {
106
- if (!nav) return
107
- if (!nav.contains(e.target as Node)) {
108
- originalButton.hidden = false
109
- nav.setAttribute('aria-hidden', 'true')
100
+ // The region is gone. A dynamic link must not fall through to its native href — that
101
+ // would jump to a now-hidden target and drop focus. Move focus to a stable static link
102
+ // first (so removing the clicked link can't drop focus to <body>), then refresh.
103
+ if (link.closest('[data-skip-dynamic]')) {
104
+ e.preventDefault()
105
+ list.querySelector<HTMLElement>('.SkipLinks-link')?.focus()
106
+ refreshDynamicLinks()
110
107
  }
108
+ // A static link with no live region (no-JS edge) falls through to its native href.
109
+ })
110
+
111
+ // Rebuild whenever the menu is opened from outside, so it reflects the regions visible
112
+ // now. The lastKey check makes this a no-op when nothing changed, so a plain re-entry
113
+ // (including Shift+Tab onto a dynamic link) doesn't disturb focus.
114
+ nav.addEventListener('focusin', (e) => {
115
+ if (nav.contains(e.relatedTarget as Node)) return
116
+ refreshDynamicLinks()
111
117
  })
118
+
119
+ // rAF: regions register asynchronously via their own onFind hooks.
120
+ requestAnimationFrame(refreshDynamicLinks)
112
121
  })