@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.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +155 -34
- package/dist/index.js.map +1 -1
- package/dist/web-component.js +154 -36
- package/dist/web-component.js.map +1 -1
- package/package.json +1 -1
package/dist/web-component.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
190
|
-
]
|
|
191
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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;
|