@oksigenia/access-panel 0.6.1 → 0.7.0

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.
@@ -60,13 +60,7 @@ function resolveEnabledControls(controlsAttr, excludeAttr) {
60
60
  }
61
61
  return enabled;
62
62
  }
63
- var NON_SCOPABLE_CONTROLS = [
64
- "grayscale",
65
- "reading-guide",
66
- "reading-mask",
67
- "big-cursor",
68
- "colorblind"
69
- ];
63
+ var NON_SCOPABLE_CONTROLS = ["grayscale", "colorblind"];
70
64
  function scopedControls(enabled) {
71
65
  const out = new Set(enabled);
72
66
  for (const id of NON_SCOPABLE_CONTROLS) out.delete(id);
@@ -124,6 +118,7 @@ function buildPanelHtml(opts) {
124
118
  const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
125
119
  const showTrigger = opts.showTrigger ?? true;
126
120
  const showPresets = opts.showPresets ?? true;
121
+ const flatLayout = opts.flatLayout ?? false;
127
122
  const trig = TRIGGER_ICONS[triggerIcon];
128
123
  const multi = (id, prefix, levels, label, icon, full = false) => {
129
124
  if (!enabled.has(id)) return "";
@@ -158,44 +153,61 @@ function buildPanelHtml(opts) {
158
153
  };
159
154
  const presetButtons = showPresets ? PRESET_IDS.filter((id) => presetIsAvailable(id, enabled)).map((id) => preset(id, presetLabels[id])) : [];
160
155
  const presetsBlock = presetButtons.length ? `<h4 class="oks-access-title">${escapeHtml(t.presets)}</h4><div class="oks-access-presets">${presetButtons.join("")}</div>` : "";
161
- const triggerBlock = showTrigger ? `
162
- <div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
163
- <button class="oks-access-btn" id="oks-trigger" part="trigger" data-trigger-icon="${triggerIcon}" aria-label="${escapeAttr(t.title)}" aria-expanded="false" aria-controls="oks-panel" type="button">
164
- ${trig}
165
- </button>
166
- <span class="oks-active-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg></span>
167
- </div>` : "";
168
- return `${triggerBlock}
169
- <div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
170
- <div class="oks-access-header">
171
- <h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
172
- <button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
173
- </div>
174
- <div class="oks-access-content">
175
- ${presetsBlock}
176
- ${section(t.txt, [
156
+ const textBtns = [
177
157
  multi("text-size", "oks-zoom", 4, t.size, ICON_TXT),
178
158
  multi("line-height", "oks-lh", 3, t.lh, ICON_LH),
179
159
  multi("text-align", "oks-align", 3, t.align, ICON_ALIGN),
180
160
  toggle("readable-font", "oks-a11y-font", t.font, ICON_FONT),
181
161
  toggle("dyslexia-font", "oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
182
162
  multi("letter-spacing", "oks-ls", 3, t.ls, ICON_LS)
183
- ])}
184
- ${section(t.vis, [
163
+ ];
164
+ const visBtns = [
185
165
  toggle("contrast", "oks-a11y-contrast", t.contrast, ICON_CONTRAST),
186
166
  overlay("grayscale", "oks-overlay-gray", t.gray, ICON_GRAY),
187
167
  toggle("hide-images", "oks-a11y-hide", t.hide, ICON_HIDE),
188
168
  toggle("highlight-links", "oks-a11y-links", t.links, ICON_LINK),
189
- multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, true)
190
- ])}
191
- ${section(t.ori, [
169
+ multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, !flatLayout)
170
+ ];
171
+ const oriBtns = [
192
172
  simple("reading-guide", "guide", t.guide, ICON_GUIDE),
193
173
  simple("reading-mask", "mask", t.mask, ICON_MASK),
194
174
  toggle("big-cursor", "oks-big-cursor", t.cursor, ICON_CURSOR),
195
175
  toggle("big-targets", "oks-a11y-bigtargets", t.targets, ICON_TARGETS),
196
176
  toggle("pause-anim", "oks-a11y-pause", t.pause, ICON_PAUSE),
197
177
  toggle("focus", "oks-a11y-focus", t.focus, ICON_FOCUS)
198
- ])}
178
+ ];
179
+ const addFullWidth = (btn) => btn.replace('class="oks-access-opt', 'class="oks-access-opt full-width');
180
+ const flatContent = () => {
181
+ const groups = [
182
+ { title: t.txt, items: textBtns.filter(Boolean) },
183
+ { title: t.vis, items: visBtns.filter(Boolean) },
184
+ { title: t.ori, items: oriBtns.filter(Boolean) }
185
+ ].filter((g) => g.items.length > 0);
186
+ const total = groups.reduce((n, g) => n + g.items.length, 0);
187
+ if (total % 2 === 1 && groups.length > 0) {
188
+ const last = groups[groups.length - 1];
189
+ last.items[last.items.length - 1] = addFullWidth(last.items[last.items.length - 1]);
190
+ }
191
+ const inner = groups.map((g) => `<h4 class="oks-access-title oks-sr-only">${escapeHtml(g.title)}</h4>${g.items.join("")}`).join("");
192
+ return `<div class="oks-access-grid oks-flat">${inner}</div>`;
193
+ };
194
+ const controlsHtml = flatLayout ? flatContent() : section(t.txt, textBtns) + section(t.vis, visBtns) + section(t.ori, oriBtns);
195
+ const triggerBlock = showTrigger ? `
196
+ <div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
197
+ <button class="oks-access-btn" id="oks-trigger" part="trigger" data-trigger-icon="${triggerIcon}" aria-label="${escapeAttr(t.title)}" aria-expanded="false" aria-controls="oks-panel" type="button">
198
+ ${trig}
199
+ </button>
200
+ <span class="oks-active-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg></span>
201
+ </div>` : "";
202
+ return `${triggerBlock}
203
+ <div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
204
+ <div class="oks-access-header">
205
+ <h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
206
+ <button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
207
+ </div>
208
+ <div class="oks-access-content">
209
+ ${presetsBlock}
210
+ ${controlsHtml}
199
211
  </div>
200
212
  <div class="oks-access-footer">
201
213
  <button class="oks-access-reset" id="oks-reset" type="button">${escapeHtml(t.reset)}</button>
@@ -371,8 +383,69 @@ function bindPanelBehavior(root, opts = {}) {
371
383
  return noopController();
372
384
  }
373
385
  let state = loadState(storageKey);
386
+ const scoped = opts.scopeEl !== void 0;
387
+ const GLOBAL_KEY = "oksiac::global";
388
+ const GLOBAL_MAP = [
389
+ { flag: "bigCursor", cls: "oks-big-cursor", match: (b) => b.getAttribute("data-class") === "oks-big-cursor" },
390
+ { flag: "readingGuide", cls: "oks-a11y-guide", match: (b) => b.getAttribute("data-action") === "guide" },
391
+ { flag: "readingMask", cls: "oks-a11y-mask", match: (b) => b.getAttribute("data-action") === "mask" }
392
+ ];
393
+ const globalOfferableOf = (b) => GLOBAL_MAP.find((x) => x.match(b));
394
+ const loadGlobal = () => {
395
+ try {
396
+ const r = JSON.parse(localStorage.getItem(GLOBAL_KEY) ?? "{}");
397
+ return { bigCursor: !!r.bigCursor, readingGuide: !!r.readingGuide, readingMask: !!r.readingMask };
398
+ } catch {
399
+ return { bigCursor: false, readingGuide: false, readingMask: false };
400
+ }
401
+ };
402
+ const saveGlobal = (g) => {
403
+ try {
404
+ if (g.bigCursor || g.readingGuide || g.readingMask) localStorage.setItem(GLOBAL_KEY, JSON.stringify(g));
405
+ else localStorage.removeItem(GLOBAL_KEY);
406
+ } catch {
407
+ }
408
+ };
409
+ const applyGlobal = (g) => {
410
+ document.body.classList.toggle("oks-big-cursor", g.bigCursor);
411
+ document.body.classList.toggle("oks-a11y-guide", g.readingGuide);
412
+ document.body.classList.toggle("oks-a11y-mask", g.readingMask);
413
+ for (const btn of opts$) {
414
+ const m = globalOfferableOf(btn);
415
+ if (!m) continue;
416
+ btn.classList.toggle("is-active", g[m.flag]);
417
+ btn.setAttribute("aria-pressed", g[m.flag] ? "true" : "false");
418
+ }
419
+ };
420
+ const refreshGlobal = () => {
421
+ if (scoped) applyGlobal(loadGlobal());
422
+ };
423
+ const syncGlobalFromState = () => {
424
+ if (!scoped) return;
425
+ const g = loadGlobal();
426
+ let changed = false;
427
+ if (state.bigCursor) {
428
+ g.bigCursor = true;
429
+ state.bigCursor = false;
430
+ changed = true;
431
+ }
432
+ if (state.readingGuide) {
433
+ g.readingGuide = true;
434
+ state.readingGuide = false;
435
+ changed = true;
436
+ }
437
+ if (state.readingMask) {
438
+ g.readingMask = true;
439
+ state.readingMask = false;
440
+ changed = true;
441
+ }
442
+ if (changed) {
443
+ saveGlobal(g);
444
+ applyGlobal(g);
445
+ document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
446
+ }
447
+ };
374
448
  function applyState() {
375
- const scoped = opts.scopeEl !== void 0;
376
449
  const fxRoot = scoped ? opts.scopeEl ?? null : document.body;
377
450
  const rootEl = document.documentElement;
378
451
  if (fxRoot) {
@@ -407,6 +480,7 @@ function bindPanelBehavior(root, opts = {}) {
407
480
  }
408
481
  function syncButtonsFromState() {
409
482
  for (const btn of opts$) {
483
+ if (scoped && globalOfferableOf(btn)) continue;
410
484
  const action = btn.getAttribute("data-action");
411
485
  if (action === "multi" || action === "colorblind") {
412
486
  const prefix = btn.getAttribute("data-prefix") ?? "";
@@ -441,6 +515,17 @@ function bindPanelBehavior(root, opts = {}) {
441
515
  }
442
516
  const onOptClick = (e) => {
443
517
  const btn = e.currentTarget;
518
+ if (scoped) {
519
+ const go = globalOfferableOf(btn);
520
+ if (go) {
521
+ const g = loadGlobal();
522
+ g[go.flag] = !g[go.flag];
523
+ saveGlobal(g);
524
+ applyGlobal(g);
525
+ document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
526
+ return;
527
+ }
528
+ }
444
529
  const action = btn.getAttribute("data-action");
445
530
  if (action === "multi") {
446
531
  const prefix = btn.getAttribute("data-prefix") ?? "";
@@ -469,6 +554,7 @@ function bindPanelBehavior(root, opts = {}) {
469
554
  if (id && PRESETS[id]) Object.assign(state, filterPresetForEnabled(id, enabled));
470
555
  btn.classList.add("is-flashing");
471
556
  setTimeout(() => btn.classList.remove("is-flashing"), 250);
557
+ syncGlobalFromState();
472
558
  }
473
559
  applyState();
474
560
  saveState(storageKey, state);
@@ -477,6 +563,12 @@ function bindPanelBehavior(root, opts = {}) {
477
563
  state = { ...DEFAULT_STATE };
478
564
  applyState();
479
565
  saveState(storageKey, state);
566
+ if (scoped) {
567
+ const cleared = { bigCursor: false, readingGuide: false, readingMask: false };
568
+ saveGlobal(cleared);
569
+ applyGlobal(cleared);
570
+ document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
571
+ }
480
572
  };
481
573
  let opener = null;
482
574
  let ignoreDocClose = false;
@@ -644,14 +736,16 @@ function bindPanelBehavior(root, opts = {}) {
644
736
  saveNudge();
645
737
  };
646
738
  const onMove = (e) => {
647
- if (!state.readingGuide && !state.readingMask) return;
739
+ const guideOn = document.body.classList.contains("oks-a11y-guide");
740
+ const maskOn = document.body.classList.contains("oks-a11y-mask");
741
+ if (!guideOn && !maskOn) return;
648
742
  const y = e.touches?.[0]?.clientY ?? e.clientY;
649
743
  if (typeof y !== "number") return;
650
- if (state.readingGuide) {
744
+ if (guideOn) {
651
745
  const guide = document.getElementById("oks-reading-guide");
652
746
  if (guide) guide.style.top = `${y}px`;
653
747
  }
654
- if (state.readingMask) {
748
+ if (maskOn) {
655
749
  const mask = document.getElementById("oks-reading-mask");
656
750
  if (mask) mask.style.setProperty("--oks-mask-y", `${y}px`);
657
751
  }
@@ -667,9 +761,15 @@ function bindPanelBehavior(root, opts = {}) {
667
761
  const onReposition = () => {
668
762
  if (opts.scopeEl && panel.classList.contains("is-open")) positionScopedDialog();
669
763
  };
670
- if (opts.scopeEl !== void 0) {
764
+ const onGlobalChange = () => refreshGlobal();
765
+ const onStorage = (e) => {
766
+ if (e.key === GLOBAL_KEY) refreshGlobal();
767
+ };
768
+ if (scoped) {
671
769
  window.addEventListener("scroll", onReposition, true);
672
770
  window.addEventListener("resize", onReposition);
771
+ document.addEventListener("oksiac:globalchange", onGlobalChange);
772
+ window.addEventListener("storage", onStorage);
673
773
  }
674
774
  if (nudgeMax > 0 && trigger) {
675
775
  loadNudge();
@@ -682,6 +782,7 @@ function bindPanelBehavior(root, opts = {}) {
682
782
  trigger.addEventListener("keydown", onTriggerKey);
683
783
  }
684
784
  applyState();
785
+ refreshGlobal();
685
786
  const dispose = () => {
686
787
  trigger?.removeEventListener("click", onTriggerClick);
687
788
  closeBtn.removeEventListener("click", closePanel);
@@ -691,9 +792,11 @@ function bindPanelBehavior(root, opts = {}) {
691
792
  document.removeEventListener("keydown", onKeyDown);
692
793
  document.removeEventListener("mousemove", onMove);
693
794
  document.removeEventListener("touchmove", onMove);
694
- if (opts.scopeEl !== void 0) {
795
+ if (scoped) {
695
796
  window.removeEventListener("scroll", onReposition, true);
696
797
  window.removeEventListener("resize", onReposition);
798
+ document.removeEventListener("oksiac:globalchange", onGlobalChange);
799
+ window.removeEventListener("storage", onStorage);
697
800
  }
698
801
  trigger?.removeEventListener("pointerdown", onPointerDown);
699
802
  document.removeEventListener("pointermove", onPointerMove);
@@ -830,6 +933,19 @@ var PANEL_CSS = `
830
933
  text-transform: uppercase;
831
934
  color: #888;
832
935
  }
936
+ /* Screen-reader-only: keeps the category headings in the accessibility tree
937
+ (flat scoped layout) while taking them out of the grid flow so they don't
938
+ force a visual row break or occupy a cell. */
939
+ .oks-sr-only {
940
+ position: absolute !important;
941
+ width: 1px; height: 1px;
942
+ margin: 0; padding: 0;
943
+ overflow: hidden;
944
+ clip: rect(0 0 0 0);
945
+ clip-path: inset(50%);
946
+ white-space: nowrap;
947
+ border: 0;
948
+ }
833
949
  .oks-access-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
834
950
  .oks-access-presets { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
835
951
  .oks-preset {
@@ -1364,7 +1480,9 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1364
1480
  position,
1365
1481
  enabled,
1366
1482
  showTrigger,
1367
- showPresets
1483
+ showPresets,
1484
+ flatLayout: scope != null
1485
+ // scoped panels use the flat (no-category) layout
1368
1486
  });
1369
1487
  shadow.innerHTML = `<style>${PANEL_CSS}${positionCss(position, positionMobile)}</style>${html}`;
1370
1488
  const scopeEl = scope ? document.querySelector(scope) : void 0;