@a.nemreen/a11y 0.1.2 → 0.1.6

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 CHANGED
@@ -37,6 +37,7 @@ const a11y = Accessibility.mount({
37
37
  position: 'end', // end | start | left | right
38
38
  stack: 0,
39
39
  locale: 'auto', // auto | ar | en
40
+ // syncDocumentLocale: true, // also flip <html lang/dir> when toggling locale
40
41
  });
41
42
 
42
43
  Accessibility.createSkipLink({ href: '#main' });
@@ -74,6 +75,10 @@ Accessibility.mount();
74
75
 
75
76
  [https://nemreen.info/a11y](https://nemreen.info/a11y)
76
77
 
78
+ ## Architecture
79
+
80
+ [arc42](docs/architecture/README.md) (what to document) and [C4](docs/architecture/c4/README.md) (how to visualize it) — goals, context, building blocks, runtime, decisions, and known risks.
81
+
77
82
  ## License
78
83
 
79
84
  MIT © [Ahmed Nemreen](https://nemreen.info)
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ var A11yController = class {
35
35
  this.mqContrast = null;
36
36
  this.localeObserver = null;
37
37
  this.localeOverride = null;
38
+ this.syncDocumentLocale = false;
38
39
  this.destroyed = false;
39
40
  this.isOpen = false;
40
41
  this.modes = /* @__PURE__ */ new Set();
@@ -55,6 +56,7 @@ var A11yController = class {
55
56
  };
56
57
  this.doc = options.document ?? document;
57
58
  this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
59
+ this.syncDocumentLocale = options.syncDocumentLocale === true;
58
60
  if (options.locale && options.locale !== "auto") {
59
61
  this.localeOverride = options.locale;
60
62
  this.locale = options.locale;
@@ -181,10 +183,13 @@ var A11yController = class {
181
183
  this.maskBand = Math.max(20, Math.min(160, this.maskBand + delta));
182
184
  this.notify();
183
185
  }
184
- setMaskY(y) {
186
+ setMaskY(y, options = {}) {
185
187
  const vh = this.doc.defaultView?.innerHeight ?? 800;
186
188
  this.maskY = Math.max(0, Math.min(vh, y));
187
- this.notify();
189
+ this.doc.documentElement.style.setProperty("--a11y-mask-y", `${this.maskY}px`);
190
+ if (options.persist === false) return;
191
+ this.persist();
192
+ this.emit();
188
193
  }
189
194
  reset() {
190
195
  this.modes = /* @__PURE__ */ new Set();
@@ -208,12 +213,14 @@ var A11yController = class {
208
213
  }
209
214
  toggleLocale() {
210
215
  const next = this.locale === "ar" ? "en" : "ar";
211
- const root = this.doc.documentElement;
212
- root.lang = next;
213
- root.dir = next === "ar" ? "rtl" : "ltr";
214
- root.dataset["locale"] = next;
215
216
  this.localeOverride = next;
216
217
  this.locale = next;
218
+ if (this.syncDocumentLocale) {
219
+ const root = this.doc.documentElement;
220
+ root.lang = next;
221
+ root.dir = next === "ar" ? "rtl" : "ltr";
222
+ root.dataset["locale"] = next;
223
+ }
217
224
  this.emit();
218
225
  return next;
219
226
  }
@@ -468,7 +475,8 @@ function createSkipLink(options = {}) {
468
475
  position: absolute;
469
476
  inset-inline-start: 1rem;
470
477
  top: 1rem;
471
- z-index: 10000;
478
+ z-index: 2147483647;
479
+ pointer-events: auto;
472
480
  padding: 0.5rem 1rem;
473
481
  border-radius: 0.375rem;
474
482
  background: #fff;
@@ -653,7 +661,10 @@ function getCopy(locale) {
653
661
 
654
662
  // src/icons.ts
655
663
  var ICONS = {
656
- access: '<path d="M12 2a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm-1 7h2l1.5 4H16l-1 2h-1.2L12.5 11h-1L10.2 15H9l-1-2h1.5L11 9Zm-5.5 9.5L8 14l1.2 1.6L12 12l2.8 3.6L16 14l2.5 4.5H5.5Z"/>',
664
+ /**
665
+ * GDS access icon (exact markup from the design system).
666
+ */
667
+ access: '<path fill="currentColor" d="M16.853 8.265a.75.75 0 0 1 .294 1.47l-4.397.88v2.24l1.946 4.867a.75.75 0 1 1-1.392.556L12 15.02l-1.304 3.258a.75.75 0 1 1-1.392-.556l1.946-4.867v-2.24l-4.397-.88a.75.75 0 1 1 .294-1.47l4.853.97zM12 5.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5"/><path fill="currentColor" fill-rule="evenodd" d="M12 1.25c5.937 0 10.75 4.813 10.75 10.75S17.937 22.75 12 22.75 1.25 17.937 1.25 12 6.063 1.25 12 1.25m0 1.5a9.25 9.25 0 1 0 0 18.5 9.25 9.25 0 0 0 0-18.5" clip-rule="evenodd"/>',
657
668
  close: '<path d="M6.7 6.7a1 1 0 0 1 1.4 0L12 10.6l3.9-3.9a1 1 0 1 1 1.4 1.4L13.4 12l3.9 3.9a1 1 0 0 1-1.4 1.4L12 13.4l-3.9 3.9a1 1 0 0 1-1.4-1.4L10.6 12 6.7 8.1a1 1 0 0 1 0-1.4Z"/>',
658
669
  flash: '<path d="M13 2 4 14h7l-1 8 10-14h-7l1-6Z"/>',
659
670
  eye: '<path d="M12 5c-5 0-9 4.5-10 7 1 2.5 5 7 10 7s9-4.5 10-7c-1-2.5-5-7-10-7Zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z"/>',
@@ -676,8 +687,31 @@ var ICONS = {
676
687
  sun: '<path d="M12 4V2m0 20v-2m8-8h2M2 12h2m13.7 5.7 1.4 1.4M4.9 4.9l1.4 1.4m0 11.4-1.4 1.4M19.1 4.9l-1.4 1.4M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z"/>',
677
688
  colorPicker: '<path d="M12 3a7 7 0 0 1 0 14h-1l-4 4v-5a7 7 0 0 1 5-13Z"/>'
678
689
  };
690
+ var STROKE_ICONS = /* @__PURE__ */ new Set([
691
+ "close",
692
+ "heading",
693
+ "linkAlt",
694
+ "pause",
695
+ "textFont",
696
+ "textAlign",
697
+ "textUnderline",
698
+ "letterSpacing",
699
+ "maximize",
700
+ "minus",
701
+ "plus",
702
+ "glasses",
703
+ "sun",
704
+ "touch",
705
+ "mask"
706
+ ]);
679
707
  function iconSvg(name, size = 18) {
680
708
  const path = ICONS[name] ?? ICONS.access;
709
+ if (name === "access") {
710
+ return `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="a11y-icon" width="${size}" height="${size}" aria-hidden="true" focusable="false">${path}</svg>`;
711
+ }
712
+ if (STROKE_ICONS.has(name)) {
713
+ return `<svg class="a11y-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${path}</svg>`;
714
+ }
681
715
  return `<svg class="a11y-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">${path}</svg>`;
682
716
  }
683
717
 
@@ -714,14 +748,28 @@ var A11yWidget = class {
714
748
  this.resetArmed = false;
715
749
  this.resetTimer = null;
716
750
  this.draggingMask = false;
751
+ this.wasOpen = false;
752
+ this.panelScrollTop = 0;
717
753
  this.onKeyDown = (e) => this.handleKey(e);
718
754
  this.onPointerMove = (e) => {
719
755
  if (!this.draggingMask) return;
720
- this.ctrl.setMaskY(e.clientY);
756
+ this.ctrl.setMaskY(e.clientY, { persist: false });
757
+ this.syncMaskDom();
721
758
  };
722
759
  this.onPointerUp = () => {
760
+ if (!this.draggingMask) return;
723
761
  this.draggingMask = false;
762
+ this.ctrl.setMaskY(this.ctrl.maskY, { persist: true });
763
+ };
764
+ this.onRootClick = (e) => this.handleClick(e);
765
+ this.onRootPointerDown = (e) => {
766
+ const t = e.target?.closest?.('[data-a11y-action="mask-drag"]');
767
+ if (!t || !this.rootEl?.contains(t)) return;
768
+ e.preventDefault();
769
+ this.draggingMask = true;
724
770
  };
771
+ this.topObserver = null;
772
+ this.topFrame = 0;
725
773
  this.ctrl = ctrl;
726
774
  this.position = options.position ?? "end";
727
775
  this.stack = Math.max(0, options.stack ?? 0);
@@ -733,15 +781,24 @@ var A11yWidget = class {
733
781
  this.rootEl.className = "a11y-root";
734
782
  this.rootEl.setAttribute("data-a11y-ui", "");
735
783
  this.mountRoot.appendChild(this.rootEl);
784
+ this.rootEl.addEventListener("click", this.onRootClick);
785
+ this.rootEl.addEventListener("pointerdown", this.onRootPointerDown);
736
786
  this.unsub = this.ctrl.subscribe(() => this.render());
737
787
  document.addEventListener("keydown", this.onKeyDown);
738
788
  document.addEventListener("pointermove", this.onPointerMove);
739
789
  document.addEventListener("pointerup", this.onPointerUp);
790
+ this.keepOnTop();
740
791
  this.render();
741
792
  }
742
793
  destroy() {
743
794
  this.unsub?.();
744
795
  this.unsub = null;
796
+ this.topObserver?.disconnect();
797
+ this.topObserver = null;
798
+ if (this.topFrame) cancelAnimationFrame(this.topFrame);
799
+ this.topFrame = 0;
800
+ this.rootEl?.removeEventListener("click", this.onRootClick);
801
+ this.rootEl?.removeEventListener("pointerdown", this.onRootPointerDown);
745
802
  document.removeEventListener("keydown", this.onKeyDown);
746
803
  document.removeEventListener("pointermove", this.onPointerMove);
747
804
  document.removeEventListener("pointerup", this.onPointerUp);
@@ -749,6 +806,25 @@ var A11yWidget = class {
749
806
  this.rootEl?.remove();
750
807
  this.rootEl = null;
751
808
  }
809
+ /** Stay the last child so equal-z-index overlays appended later cannot cover us. */
810
+ keepOnTop() {
811
+ const parent = this.rootEl?.parentElement;
812
+ if (!parent || !this.rootEl) return;
813
+ const bump = () => {
814
+ if (!this.rootEl || this.rootEl.parentElement !== parent) return;
815
+ if (parent.lastElementChild === this.rootEl) return;
816
+ parent.appendChild(this.rootEl);
817
+ };
818
+ this.topObserver = new MutationObserver(() => {
819
+ if (this.topFrame) return;
820
+ this.topFrame = requestAnimationFrame(() => {
821
+ this.topFrame = 0;
822
+ bump();
823
+ });
824
+ });
825
+ this.topObserver.observe(parent, { childList: true });
826
+ bump();
827
+ }
752
828
  copy() {
753
829
  return getCopy(this.ctrl.locale);
754
830
  }
@@ -814,7 +890,16 @@ var A11yWidget = class {
814
890
  const chips = this.activeChips();
815
891
  const badge = chips.length;
816
892
  const open = this.ctrl.isOpen;
893
+ const opening = open && !this.wasOpen;
894
+ const stayingOpen = open && this.wasOpen;
817
895
  const readingMask = this.ctrl.hasEffectiveMode("reading-mask");
896
+ const dir = this.ctrl.locale === "ar" ? "rtl" : "ltr";
897
+ if (stayingOpen) {
898
+ const body = this.rootEl.querySelector(".a11y-panel__body");
899
+ if (body) this.panelScrollTop = body.scrollTop;
900
+ }
901
+ this.rootEl.lang = this.ctrl.locale;
902
+ this.rootEl.dir = dir;
818
903
  this.rootEl.innerHTML = `
819
904
  <svg class="a11y-deuteranopia-svg" aria-hidden="true" focusable="false">
820
905
  <filter id="a11y-deuteranopia">
@@ -840,23 +925,42 @@ var A11yWidget = class {
840
925
  ${badge > 0 ? `<span class="a11y-fab__badge" aria-hidden="true">${badge > 9 ? "9+" : badge}</span>` : ""}
841
926
  </button>
842
927
 
843
- ${open ? this.renderPanel(c, chips) : ""}
928
+ ${open ? this.renderPanel(c, chips, stayingOpen) : ""}
844
929
  ${readingMask ? this.renderMask(c) : ""}
845
930
  `;
846
- this.bindEvents();
847
- if (open) {
931
+ if (stayingOpen) {
932
+ const body = this.rootEl.querySelector(".a11y-panel__body");
933
+ if (body) body.scrollTop = this.panelScrollTop;
934
+ }
935
+ if (opening) {
848
936
  const closeBtn = this.rootEl.querySelector('[data-a11y-action="close"]');
849
937
  queueMicrotask(() => closeBtn?.focus());
850
938
  }
939
+ this.wasOpen = open;
940
+ }
941
+ syncMaskDom() {
942
+ if (!this.rootEl) return;
943
+ const shades = this.rootEl.querySelectorAll(".a11y-mask-shade");
944
+ const toolbar = this.rootEl.querySelector(".a11y-mask-toolbar");
945
+ if (shades[0]) {
946
+ shades[0].style.height = "calc(var(--a11y-mask-y) - var(--a11y-mask-band))";
947
+ }
948
+ if (shades[1]) {
949
+ shades[1].style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band))";
950
+ }
951
+ if (toolbar) {
952
+ toolbar.style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band) + 8px)";
953
+ }
851
954
  }
852
- renderPanel(c, chips) {
955
+ renderPanel(c, chips, staticPanel) {
853
956
  const profileCount = this.ctrl.activeModeCount("modes");
957
+ const panelClass = staticPanel ? `a11y-panel a11y-panel--${this.position} a11y-panel--static` : `a11y-panel a11y-panel--${this.position}`;
854
958
  return `
855
959
  <div class="a11y-layer" role="presentation">
856
960
  <div class="a11y-backdrop" data-a11y-action="close" aria-hidden="true"></div>
857
961
  <aside
858
962
  id="${PANEL_ID}"
859
- class="a11y-panel a11y-panel--${this.position}"
963
+ class="${panelClass}"
860
964
  role="dialog"
861
965
  aria-modal="true"
862
966
  aria-label="${escapeAttr(c.panelLabel)}"
@@ -1050,67 +1154,65 @@ var A11yWidget = class {
1050
1154
  </button>
1051
1155
  </div>`;
1052
1156
  }
1053
- bindEvents() {
1054
- if (!this.rootEl) return;
1055
- this.rootEl.querySelectorAll("[data-a11y-action]").forEach((el) => {
1056
- el.addEventListener("click", (e) => {
1057
- const action = el.getAttribute("data-a11y-action");
1058
- if (action === "toggle") {
1059
- this.ctrl.toggle(el);
1060
- } else if (action === "close") {
1061
- this.ctrl.close();
1062
- } else if (action === "reset") {
1063
- this.onReset();
1064
- } else if (action === "mask-smaller") {
1065
- this.ctrl.adjustMaskBand(-20);
1066
- } else if (action === "mask-larger") {
1067
- this.ctrl.adjustMaskBand(20);
1068
- } else if (action === "mask-close") {
1069
- this.ctrl.setMode("reading-mask", false);
1070
- } else if (action === "mask-drag") {
1071
- e.preventDefault();
1072
- this.draggingMask = true;
1073
- }
1074
- });
1075
- });
1076
- this.rootEl.querySelectorAll("[data-a11y-bundle]").forEach((el) => {
1077
- el.addEventListener("click", () => {
1078
- const id = el.getAttribute("data-a11y-bundle");
1079
- this.ctrl.setMode(id, !this.ctrl.hasMode(id));
1080
- });
1081
- });
1082
- this.rootEl.querySelectorAll("[data-a11y-toggle]").forEach((el) => {
1083
- el.addEventListener("click", () => {
1084
- const mode = el.getAttribute("data-a11y-toggle");
1085
- this.ctrl.setMode(mode, !this.ctrl.hasEffectiveMode(mode));
1086
- });
1087
- });
1088
- this.rootEl.querySelectorAll("[data-a11y-filter]").forEach((el) => {
1089
- el.addEventListener("click", () => {
1090
- const filter = el.getAttribute("data-a11y-filter");
1091
- this.ctrl.setVisualFilter(filter);
1092
- });
1093
- });
1094
- this.rootEl.querySelectorAll("[data-a11y-step]").forEach((el) => {
1095
- el.addEventListener("click", () => {
1096
- const id = el.getAttribute("data-a11y-step");
1097
- const delta = Number(el.getAttribute("data-delta") || "1");
1098
- if (id === "font") this.ctrl.stepFontStep(delta);
1099
- else if (id === "align") this.ctrl.stepTextAlign(delta);
1100
- else if (id === "line") this.ctrl.stepLineHeight(delta);
1101
- else if (id === "letter") this.ctrl.stepLetterSpacing(delta);
1102
- else if (id === "word") this.ctrl.stepWordSpacing(delta);
1103
- });
1104
- });
1105
- this.rootEl.querySelectorAll("[data-a11y-focus]").forEach((el) => {
1106
- el.addEventListener("click", () => {
1107
- const id = el.getAttribute("data-a11y-focus");
1108
- const target = this.rootEl?.querySelector(`[data-a11y-control="${id}"]`);
1109
- target?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1110
- const focusable = target?.querySelector("button, [tabindex]");
1111
- focusable?.focus();
1112
- });
1113
- });
1157
+ handleClick(e) {
1158
+ const target = e.target;
1159
+ if (!target || !this.rootEl) return;
1160
+ const actionEl = target.closest("[data-a11y-action]");
1161
+ if (actionEl && this.rootEl.contains(actionEl)) {
1162
+ const action = actionEl.getAttribute("data-a11y-action");
1163
+ if (action === "toggle") {
1164
+ this.ctrl.toggle(actionEl);
1165
+ } else if (action === "close") {
1166
+ this.ctrl.close();
1167
+ } else if (action === "reset") {
1168
+ this.onReset();
1169
+ } else if (action === "mask-smaller") {
1170
+ this.ctrl.adjustMaskBand(-20);
1171
+ } else if (action === "mask-larger") {
1172
+ this.ctrl.adjustMaskBand(20);
1173
+ } else if (action === "mask-close") {
1174
+ this.ctrl.setMode("reading-mask", false);
1175
+ }
1176
+ return;
1177
+ }
1178
+ const bundleEl = target.closest("[data-a11y-bundle]");
1179
+ if (bundleEl && this.rootEl.contains(bundleEl)) {
1180
+ const id = bundleEl.getAttribute("data-a11y-bundle");
1181
+ this.ctrl.setMode(id, !this.ctrl.hasMode(id));
1182
+ return;
1183
+ }
1184
+ const toggleEl = target.closest("[data-a11y-toggle]");
1185
+ if (toggleEl && this.rootEl.contains(toggleEl)) {
1186
+ const mode = toggleEl.getAttribute("data-a11y-toggle");
1187
+ this.ctrl.setMode(mode, !this.ctrl.hasEffectiveMode(mode));
1188
+ return;
1189
+ }
1190
+ const filterEl = target.closest("[data-a11y-filter]");
1191
+ if (filterEl && this.rootEl.contains(filterEl)) {
1192
+ const filter = filterEl.getAttribute("data-a11y-filter");
1193
+ this.ctrl.setVisualFilter(filter);
1194
+ return;
1195
+ }
1196
+ const stepEl = target.closest("[data-a11y-step]");
1197
+ if (stepEl && this.rootEl.contains(stepEl)) {
1198
+ e.preventDefault();
1199
+ const id = stepEl.getAttribute("data-a11y-step");
1200
+ const delta = Number(stepEl.getAttribute("data-delta") || "1");
1201
+ if (id === "font") this.ctrl.stepFontStep(delta);
1202
+ else if (id === "align") this.ctrl.stepTextAlign(delta);
1203
+ else if (id === "line") this.ctrl.stepLineHeight(delta);
1204
+ else if (id === "letter") this.ctrl.stepLetterSpacing(delta);
1205
+ else if (id === "word") this.ctrl.stepWordSpacing(delta);
1206
+ return;
1207
+ }
1208
+ const focusEl = target.closest("[data-a11y-focus]");
1209
+ if (focusEl && this.rootEl.contains(focusEl)) {
1210
+ const id = focusEl.getAttribute("data-a11y-focus");
1211
+ const control = this.rootEl.querySelector(`[data-a11y-control="${id}"]`);
1212
+ control?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1213
+ const focusable = control?.querySelector("button, [tabindex]");
1214
+ focusable?.focus();
1215
+ }
1114
1216
  }
1115
1217
  onReset() {
1116
1218
  if (!this.resetArmed) {
@@ -1182,7 +1284,8 @@ function mount(options = {}) {
1182
1284
  ensureStyles();
1183
1285
  const controller = new A11yController({
1184
1286
  storageKey: options.storageKey,
1185
- locale: options.locale
1287
+ locale: options.locale,
1288
+ syncDocumentLocale: options.syncDocumentLocale
1186
1289
  });
1187
1290
  let widget = null;
1188
1291
  if (!options.headless) {
package/dist/index.d.cts CHANGED
@@ -16,6 +16,11 @@ interface A11yMountOptions {
16
16
  stack?: number;
17
17
  /** UI locale. Default: `auto` (from `<html lang>` / `data-locale`). */
18
18
  locale?: A11yLocale | 'auto';
19
+ /**
20
+ * When true, `toggleLocale()` also updates `<html lang>` / `dir`.
21
+ * Default: `false` (widget UI only — avoids flipping the host page).
22
+ */
23
+ syncDocumentLocale?: boolean;
19
24
  /** localStorage key. Default: `dga-a11y`. */
20
25
  storageKey?: string;
21
26
  /** Mount root. Default: `document.body`. */
@@ -48,6 +53,7 @@ declare class A11yController {
48
53
  private mqContrast;
49
54
  private localeObserver;
50
55
  private localeOverride;
56
+ private syncDocumentLocale;
51
57
  private destroyed;
52
58
  isOpen: boolean;
53
59
  modes: Set<A11yMode>;
@@ -63,6 +69,7 @@ declare class A11yController {
63
69
  constructor(options?: {
64
70
  storageKey?: string;
65
71
  locale?: A11yLocale | 'auto';
72
+ syncDocumentLocale?: boolean;
66
73
  document?: Document;
67
74
  });
68
75
  subscribe(listener: A11yListener): () => void;
@@ -84,7 +91,9 @@ declare class A11yController {
84
91
  stepLetterSpacing(delta: number): void;
85
92
  stepWordSpacing(delta: number): void;
86
93
  adjustMaskBand(delta: number): void;
87
- setMaskY(y: number): void;
94
+ setMaskY(y: number, options?: {
95
+ persist?: boolean;
96
+ }): void;
88
97
  reset(): void;
89
98
  announce(msg: string): void;
90
99
  toggleLocale(): A11yLocale;
@@ -126,20 +135,29 @@ declare class A11yWidget {
126
135
  private resetArmed;
127
136
  private resetTimer;
128
137
  private draggingMask;
138
+ private wasOpen;
139
+ private panelScrollTop;
129
140
  private onKeyDown;
130
141
  private onPointerMove;
131
142
  private onPointerUp;
143
+ private onRootClick;
144
+ private onRootPointerDown;
145
+ private topObserver;
146
+ private topFrame;
132
147
  constructor(ctrl: A11yController, options?: A11yWidgetOptions);
133
148
  mount(): void;
134
149
  destroy(): void;
150
+ /** Stay the last child so equal-z-index overlays appended later cannot cover us. */
151
+ private keepOnTop;
135
152
  private copy;
136
153
  private fabBottom;
137
154
  private activeChips;
138
155
  private render;
156
+ private syncMaskDom;
139
157
  private renderPanel;
140
158
  private renderSteppers;
141
159
  private renderMask;
142
- private bindEvents;
160
+ private handleClick;
143
161
  private onReset;
144
162
  private handleKey;
145
163
  }
package/dist/index.d.ts CHANGED
@@ -16,6 +16,11 @@ interface A11yMountOptions {
16
16
  stack?: number;
17
17
  /** UI locale. Default: `auto` (from `<html lang>` / `data-locale`). */
18
18
  locale?: A11yLocale | 'auto';
19
+ /**
20
+ * When true, `toggleLocale()` also updates `<html lang>` / `dir`.
21
+ * Default: `false` (widget UI only — avoids flipping the host page).
22
+ */
23
+ syncDocumentLocale?: boolean;
19
24
  /** localStorage key. Default: `dga-a11y`. */
20
25
  storageKey?: string;
21
26
  /** Mount root. Default: `document.body`. */
@@ -48,6 +53,7 @@ declare class A11yController {
48
53
  private mqContrast;
49
54
  private localeObserver;
50
55
  private localeOverride;
56
+ private syncDocumentLocale;
51
57
  private destroyed;
52
58
  isOpen: boolean;
53
59
  modes: Set<A11yMode>;
@@ -63,6 +69,7 @@ declare class A11yController {
63
69
  constructor(options?: {
64
70
  storageKey?: string;
65
71
  locale?: A11yLocale | 'auto';
72
+ syncDocumentLocale?: boolean;
66
73
  document?: Document;
67
74
  });
68
75
  subscribe(listener: A11yListener): () => void;
@@ -84,7 +91,9 @@ declare class A11yController {
84
91
  stepLetterSpacing(delta: number): void;
85
92
  stepWordSpacing(delta: number): void;
86
93
  adjustMaskBand(delta: number): void;
87
- setMaskY(y: number): void;
94
+ setMaskY(y: number, options?: {
95
+ persist?: boolean;
96
+ }): void;
88
97
  reset(): void;
89
98
  announce(msg: string): void;
90
99
  toggleLocale(): A11yLocale;
@@ -126,20 +135,29 @@ declare class A11yWidget {
126
135
  private resetArmed;
127
136
  private resetTimer;
128
137
  private draggingMask;
138
+ private wasOpen;
139
+ private panelScrollTop;
129
140
  private onKeyDown;
130
141
  private onPointerMove;
131
142
  private onPointerUp;
143
+ private onRootClick;
144
+ private onRootPointerDown;
145
+ private topObserver;
146
+ private topFrame;
132
147
  constructor(ctrl: A11yController, options?: A11yWidgetOptions);
133
148
  mount(): void;
134
149
  destroy(): void;
150
+ /** Stay the last child so equal-z-index overlays appended later cannot cover us. */
151
+ private keepOnTop;
135
152
  private copy;
136
153
  private fabBottom;
137
154
  private activeChips;
138
155
  private render;
156
+ private syncMaskDom;
139
157
  private renderPanel;
140
158
  private renderSteppers;
141
159
  private renderMask;
142
- private bindEvents;
160
+ private handleClick;
143
161
  private onReset;
144
162
  private handleKey;
145
163
  }
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ var A11yController = class {
33
33
  this.mqContrast = null;
34
34
  this.localeObserver = null;
35
35
  this.localeOverride = null;
36
+ this.syncDocumentLocale = false;
36
37
  this.destroyed = false;
37
38
  this.isOpen = false;
38
39
  this.modes = /* @__PURE__ */ new Set();
@@ -53,6 +54,7 @@ var A11yController = class {
53
54
  };
54
55
  this.doc = options.document ?? document;
55
56
  this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
57
+ this.syncDocumentLocale = options.syncDocumentLocale === true;
56
58
  if (options.locale && options.locale !== "auto") {
57
59
  this.localeOverride = options.locale;
58
60
  this.locale = options.locale;
@@ -179,10 +181,13 @@ var A11yController = class {
179
181
  this.maskBand = Math.max(20, Math.min(160, this.maskBand + delta));
180
182
  this.notify();
181
183
  }
182
- setMaskY(y) {
184
+ setMaskY(y, options = {}) {
183
185
  const vh = this.doc.defaultView?.innerHeight ?? 800;
184
186
  this.maskY = Math.max(0, Math.min(vh, y));
185
- this.notify();
187
+ this.doc.documentElement.style.setProperty("--a11y-mask-y", `${this.maskY}px`);
188
+ if (options.persist === false) return;
189
+ this.persist();
190
+ this.emit();
186
191
  }
187
192
  reset() {
188
193
  this.modes = /* @__PURE__ */ new Set();
@@ -206,12 +211,14 @@ var A11yController = class {
206
211
  }
207
212
  toggleLocale() {
208
213
  const next = this.locale === "ar" ? "en" : "ar";
209
- const root = this.doc.documentElement;
210
- root.lang = next;
211
- root.dir = next === "ar" ? "rtl" : "ltr";
212
- root.dataset["locale"] = next;
213
214
  this.localeOverride = next;
214
215
  this.locale = next;
216
+ if (this.syncDocumentLocale) {
217
+ const root = this.doc.documentElement;
218
+ root.lang = next;
219
+ root.dir = next === "ar" ? "rtl" : "ltr";
220
+ root.dataset["locale"] = next;
221
+ }
215
222
  this.emit();
216
223
  return next;
217
224
  }
@@ -466,7 +473,8 @@ function createSkipLink(options = {}) {
466
473
  position: absolute;
467
474
  inset-inline-start: 1rem;
468
475
  top: 1rem;
469
- z-index: 10000;
476
+ z-index: 2147483647;
477
+ pointer-events: auto;
470
478
  padding: 0.5rem 1rem;
471
479
  border-radius: 0.375rem;
472
480
  background: #fff;
@@ -651,7 +659,10 @@ function getCopy(locale) {
651
659
 
652
660
  // src/icons.ts
653
661
  var ICONS = {
654
- access: '<path d="M12 2a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm-1 7h2l1.5 4H16l-1 2h-1.2L12.5 11h-1L10.2 15H9l-1-2h1.5L11 9Zm-5.5 9.5L8 14l1.2 1.6L12 12l2.8 3.6L16 14l2.5 4.5H5.5Z"/>',
662
+ /**
663
+ * GDS access icon (exact markup from the design system).
664
+ */
665
+ access: '<path fill="currentColor" d="M16.853 8.265a.75.75 0 0 1 .294 1.47l-4.397.88v2.24l1.946 4.867a.75.75 0 1 1-1.392.556L12 15.02l-1.304 3.258a.75.75 0 1 1-1.392-.556l1.946-4.867v-2.24l-4.397-.88a.75.75 0 1 1 .294-1.47l4.853.97zM12 5.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5"/><path fill="currentColor" fill-rule="evenodd" d="M12 1.25c5.937 0 10.75 4.813 10.75 10.75S17.937 22.75 12 22.75 1.25 17.937 1.25 12 6.063 1.25 12 1.25m0 1.5a9.25 9.25 0 1 0 0 18.5 9.25 9.25 0 0 0 0-18.5" clip-rule="evenodd"/>',
655
666
  close: '<path d="M6.7 6.7a1 1 0 0 1 1.4 0L12 10.6l3.9-3.9a1 1 0 1 1 1.4 1.4L13.4 12l3.9 3.9a1 1 0 0 1-1.4 1.4L12 13.4l-3.9 3.9a1 1 0 0 1-1.4-1.4L10.6 12 6.7 8.1a1 1 0 0 1 0-1.4Z"/>',
656
667
  flash: '<path d="M13 2 4 14h7l-1 8 10-14h-7l1-6Z"/>',
657
668
  eye: '<path d="M12 5c-5 0-9 4.5-10 7 1 2.5 5 7 10 7s9-4.5 10-7c-1-2.5-5-7-10-7Zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z"/>',
@@ -674,8 +685,31 @@ var ICONS = {
674
685
  sun: '<path d="M12 4V2m0 20v-2m8-8h2M2 12h2m13.7 5.7 1.4 1.4M4.9 4.9l1.4 1.4m0 11.4-1.4 1.4M19.1 4.9l-1.4 1.4M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z"/>',
675
686
  colorPicker: '<path d="M12 3a7 7 0 0 1 0 14h-1l-4 4v-5a7 7 0 0 1 5-13Z"/>'
676
687
  };
688
+ var STROKE_ICONS = /* @__PURE__ */ new Set([
689
+ "close",
690
+ "heading",
691
+ "linkAlt",
692
+ "pause",
693
+ "textFont",
694
+ "textAlign",
695
+ "textUnderline",
696
+ "letterSpacing",
697
+ "maximize",
698
+ "minus",
699
+ "plus",
700
+ "glasses",
701
+ "sun",
702
+ "touch",
703
+ "mask"
704
+ ]);
677
705
  function iconSvg(name, size = 18) {
678
706
  const path = ICONS[name] ?? ICONS.access;
707
+ if (name === "access") {
708
+ return `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="a11y-icon" width="${size}" height="${size}" aria-hidden="true" focusable="false">${path}</svg>`;
709
+ }
710
+ if (STROKE_ICONS.has(name)) {
711
+ return `<svg class="a11y-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${path}</svg>`;
712
+ }
679
713
  return `<svg class="a11y-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">${path}</svg>`;
680
714
  }
681
715
 
@@ -712,14 +746,28 @@ var A11yWidget = class {
712
746
  this.resetArmed = false;
713
747
  this.resetTimer = null;
714
748
  this.draggingMask = false;
749
+ this.wasOpen = false;
750
+ this.panelScrollTop = 0;
715
751
  this.onKeyDown = (e) => this.handleKey(e);
716
752
  this.onPointerMove = (e) => {
717
753
  if (!this.draggingMask) return;
718
- this.ctrl.setMaskY(e.clientY);
754
+ this.ctrl.setMaskY(e.clientY, { persist: false });
755
+ this.syncMaskDom();
719
756
  };
720
757
  this.onPointerUp = () => {
758
+ if (!this.draggingMask) return;
721
759
  this.draggingMask = false;
760
+ this.ctrl.setMaskY(this.ctrl.maskY, { persist: true });
761
+ };
762
+ this.onRootClick = (e) => this.handleClick(e);
763
+ this.onRootPointerDown = (e) => {
764
+ const t = e.target?.closest?.('[data-a11y-action="mask-drag"]');
765
+ if (!t || !this.rootEl?.contains(t)) return;
766
+ e.preventDefault();
767
+ this.draggingMask = true;
722
768
  };
769
+ this.topObserver = null;
770
+ this.topFrame = 0;
723
771
  this.ctrl = ctrl;
724
772
  this.position = options.position ?? "end";
725
773
  this.stack = Math.max(0, options.stack ?? 0);
@@ -731,15 +779,24 @@ var A11yWidget = class {
731
779
  this.rootEl.className = "a11y-root";
732
780
  this.rootEl.setAttribute("data-a11y-ui", "");
733
781
  this.mountRoot.appendChild(this.rootEl);
782
+ this.rootEl.addEventListener("click", this.onRootClick);
783
+ this.rootEl.addEventListener("pointerdown", this.onRootPointerDown);
734
784
  this.unsub = this.ctrl.subscribe(() => this.render());
735
785
  document.addEventListener("keydown", this.onKeyDown);
736
786
  document.addEventListener("pointermove", this.onPointerMove);
737
787
  document.addEventListener("pointerup", this.onPointerUp);
788
+ this.keepOnTop();
738
789
  this.render();
739
790
  }
740
791
  destroy() {
741
792
  this.unsub?.();
742
793
  this.unsub = null;
794
+ this.topObserver?.disconnect();
795
+ this.topObserver = null;
796
+ if (this.topFrame) cancelAnimationFrame(this.topFrame);
797
+ this.topFrame = 0;
798
+ this.rootEl?.removeEventListener("click", this.onRootClick);
799
+ this.rootEl?.removeEventListener("pointerdown", this.onRootPointerDown);
743
800
  document.removeEventListener("keydown", this.onKeyDown);
744
801
  document.removeEventListener("pointermove", this.onPointerMove);
745
802
  document.removeEventListener("pointerup", this.onPointerUp);
@@ -747,6 +804,25 @@ var A11yWidget = class {
747
804
  this.rootEl?.remove();
748
805
  this.rootEl = null;
749
806
  }
807
+ /** Stay the last child so equal-z-index overlays appended later cannot cover us. */
808
+ keepOnTop() {
809
+ const parent = this.rootEl?.parentElement;
810
+ if (!parent || !this.rootEl) return;
811
+ const bump = () => {
812
+ if (!this.rootEl || this.rootEl.parentElement !== parent) return;
813
+ if (parent.lastElementChild === this.rootEl) return;
814
+ parent.appendChild(this.rootEl);
815
+ };
816
+ this.topObserver = new MutationObserver(() => {
817
+ if (this.topFrame) return;
818
+ this.topFrame = requestAnimationFrame(() => {
819
+ this.topFrame = 0;
820
+ bump();
821
+ });
822
+ });
823
+ this.topObserver.observe(parent, { childList: true });
824
+ bump();
825
+ }
750
826
  copy() {
751
827
  return getCopy(this.ctrl.locale);
752
828
  }
@@ -812,7 +888,16 @@ var A11yWidget = class {
812
888
  const chips = this.activeChips();
813
889
  const badge = chips.length;
814
890
  const open = this.ctrl.isOpen;
891
+ const opening = open && !this.wasOpen;
892
+ const stayingOpen = open && this.wasOpen;
815
893
  const readingMask = this.ctrl.hasEffectiveMode("reading-mask");
894
+ const dir = this.ctrl.locale === "ar" ? "rtl" : "ltr";
895
+ if (stayingOpen) {
896
+ const body = this.rootEl.querySelector(".a11y-panel__body");
897
+ if (body) this.panelScrollTop = body.scrollTop;
898
+ }
899
+ this.rootEl.lang = this.ctrl.locale;
900
+ this.rootEl.dir = dir;
816
901
  this.rootEl.innerHTML = `
817
902
  <svg class="a11y-deuteranopia-svg" aria-hidden="true" focusable="false">
818
903
  <filter id="a11y-deuteranopia">
@@ -838,23 +923,42 @@ var A11yWidget = class {
838
923
  ${badge > 0 ? `<span class="a11y-fab__badge" aria-hidden="true">${badge > 9 ? "9+" : badge}</span>` : ""}
839
924
  </button>
840
925
 
841
- ${open ? this.renderPanel(c, chips) : ""}
926
+ ${open ? this.renderPanel(c, chips, stayingOpen) : ""}
842
927
  ${readingMask ? this.renderMask(c) : ""}
843
928
  `;
844
- this.bindEvents();
845
- if (open) {
929
+ if (stayingOpen) {
930
+ const body = this.rootEl.querySelector(".a11y-panel__body");
931
+ if (body) body.scrollTop = this.panelScrollTop;
932
+ }
933
+ if (opening) {
846
934
  const closeBtn = this.rootEl.querySelector('[data-a11y-action="close"]');
847
935
  queueMicrotask(() => closeBtn?.focus());
848
936
  }
937
+ this.wasOpen = open;
938
+ }
939
+ syncMaskDom() {
940
+ if (!this.rootEl) return;
941
+ const shades = this.rootEl.querySelectorAll(".a11y-mask-shade");
942
+ const toolbar = this.rootEl.querySelector(".a11y-mask-toolbar");
943
+ if (shades[0]) {
944
+ shades[0].style.height = "calc(var(--a11y-mask-y) - var(--a11y-mask-band))";
945
+ }
946
+ if (shades[1]) {
947
+ shades[1].style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band))";
948
+ }
949
+ if (toolbar) {
950
+ toolbar.style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band) + 8px)";
951
+ }
849
952
  }
850
- renderPanel(c, chips) {
953
+ renderPanel(c, chips, staticPanel) {
851
954
  const profileCount = this.ctrl.activeModeCount("modes");
955
+ const panelClass = staticPanel ? `a11y-panel a11y-panel--${this.position} a11y-panel--static` : `a11y-panel a11y-panel--${this.position}`;
852
956
  return `
853
957
  <div class="a11y-layer" role="presentation">
854
958
  <div class="a11y-backdrop" data-a11y-action="close" aria-hidden="true"></div>
855
959
  <aside
856
960
  id="${PANEL_ID}"
857
- class="a11y-panel a11y-panel--${this.position}"
961
+ class="${panelClass}"
858
962
  role="dialog"
859
963
  aria-modal="true"
860
964
  aria-label="${escapeAttr(c.panelLabel)}"
@@ -1048,67 +1152,65 @@ var A11yWidget = class {
1048
1152
  </button>
1049
1153
  </div>`;
1050
1154
  }
1051
- bindEvents() {
1052
- if (!this.rootEl) return;
1053
- this.rootEl.querySelectorAll("[data-a11y-action]").forEach((el) => {
1054
- el.addEventListener("click", (e) => {
1055
- const action = el.getAttribute("data-a11y-action");
1056
- if (action === "toggle") {
1057
- this.ctrl.toggle(el);
1058
- } else if (action === "close") {
1059
- this.ctrl.close();
1060
- } else if (action === "reset") {
1061
- this.onReset();
1062
- } else if (action === "mask-smaller") {
1063
- this.ctrl.adjustMaskBand(-20);
1064
- } else if (action === "mask-larger") {
1065
- this.ctrl.adjustMaskBand(20);
1066
- } else if (action === "mask-close") {
1067
- this.ctrl.setMode("reading-mask", false);
1068
- } else if (action === "mask-drag") {
1069
- e.preventDefault();
1070
- this.draggingMask = true;
1071
- }
1072
- });
1073
- });
1074
- this.rootEl.querySelectorAll("[data-a11y-bundle]").forEach((el) => {
1075
- el.addEventListener("click", () => {
1076
- const id = el.getAttribute("data-a11y-bundle");
1077
- this.ctrl.setMode(id, !this.ctrl.hasMode(id));
1078
- });
1079
- });
1080
- this.rootEl.querySelectorAll("[data-a11y-toggle]").forEach((el) => {
1081
- el.addEventListener("click", () => {
1082
- const mode = el.getAttribute("data-a11y-toggle");
1083
- this.ctrl.setMode(mode, !this.ctrl.hasEffectiveMode(mode));
1084
- });
1085
- });
1086
- this.rootEl.querySelectorAll("[data-a11y-filter]").forEach((el) => {
1087
- el.addEventListener("click", () => {
1088
- const filter = el.getAttribute("data-a11y-filter");
1089
- this.ctrl.setVisualFilter(filter);
1090
- });
1091
- });
1092
- this.rootEl.querySelectorAll("[data-a11y-step]").forEach((el) => {
1093
- el.addEventListener("click", () => {
1094
- const id = el.getAttribute("data-a11y-step");
1095
- const delta = Number(el.getAttribute("data-delta") || "1");
1096
- if (id === "font") this.ctrl.stepFontStep(delta);
1097
- else if (id === "align") this.ctrl.stepTextAlign(delta);
1098
- else if (id === "line") this.ctrl.stepLineHeight(delta);
1099
- else if (id === "letter") this.ctrl.stepLetterSpacing(delta);
1100
- else if (id === "word") this.ctrl.stepWordSpacing(delta);
1101
- });
1102
- });
1103
- this.rootEl.querySelectorAll("[data-a11y-focus]").forEach((el) => {
1104
- el.addEventListener("click", () => {
1105
- const id = el.getAttribute("data-a11y-focus");
1106
- const target = this.rootEl?.querySelector(`[data-a11y-control="${id}"]`);
1107
- target?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1108
- const focusable = target?.querySelector("button, [tabindex]");
1109
- focusable?.focus();
1110
- });
1111
- });
1155
+ handleClick(e) {
1156
+ const target = e.target;
1157
+ if (!target || !this.rootEl) return;
1158
+ const actionEl = target.closest("[data-a11y-action]");
1159
+ if (actionEl && this.rootEl.contains(actionEl)) {
1160
+ const action = actionEl.getAttribute("data-a11y-action");
1161
+ if (action === "toggle") {
1162
+ this.ctrl.toggle(actionEl);
1163
+ } else if (action === "close") {
1164
+ this.ctrl.close();
1165
+ } else if (action === "reset") {
1166
+ this.onReset();
1167
+ } else if (action === "mask-smaller") {
1168
+ this.ctrl.adjustMaskBand(-20);
1169
+ } else if (action === "mask-larger") {
1170
+ this.ctrl.adjustMaskBand(20);
1171
+ } else if (action === "mask-close") {
1172
+ this.ctrl.setMode("reading-mask", false);
1173
+ }
1174
+ return;
1175
+ }
1176
+ const bundleEl = target.closest("[data-a11y-bundle]");
1177
+ if (bundleEl && this.rootEl.contains(bundleEl)) {
1178
+ const id = bundleEl.getAttribute("data-a11y-bundle");
1179
+ this.ctrl.setMode(id, !this.ctrl.hasMode(id));
1180
+ return;
1181
+ }
1182
+ const toggleEl = target.closest("[data-a11y-toggle]");
1183
+ if (toggleEl && this.rootEl.contains(toggleEl)) {
1184
+ const mode = toggleEl.getAttribute("data-a11y-toggle");
1185
+ this.ctrl.setMode(mode, !this.ctrl.hasEffectiveMode(mode));
1186
+ return;
1187
+ }
1188
+ const filterEl = target.closest("[data-a11y-filter]");
1189
+ if (filterEl && this.rootEl.contains(filterEl)) {
1190
+ const filter = filterEl.getAttribute("data-a11y-filter");
1191
+ this.ctrl.setVisualFilter(filter);
1192
+ return;
1193
+ }
1194
+ const stepEl = target.closest("[data-a11y-step]");
1195
+ if (stepEl && this.rootEl.contains(stepEl)) {
1196
+ e.preventDefault();
1197
+ const id = stepEl.getAttribute("data-a11y-step");
1198
+ const delta = Number(stepEl.getAttribute("data-delta") || "1");
1199
+ if (id === "font") this.ctrl.stepFontStep(delta);
1200
+ else if (id === "align") this.ctrl.stepTextAlign(delta);
1201
+ else if (id === "line") this.ctrl.stepLineHeight(delta);
1202
+ else if (id === "letter") this.ctrl.stepLetterSpacing(delta);
1203
+ else if (id === "word") this.ctrl.stepWordSpacing(delta);
1204
+ return;
1205
+ }
1206
+ const focusEl = target.closest("[data-a11y-focus]");
1207
+ if (focusEl && this.rootEl.contains(focusEl)) {
1208
+ const id = focusEl.getAttribute("data-a11y-focus");
1209
+ const control = this.rootEl.querySelector(`[data-a11y-control="${id}"]`);
1210
+ control?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1211
+ const focusable = control?.querySelector("button, [tabindex]");
1212
+ focusable?.focus();
1213
+ }
1112
1214
  }
1113
1215
  onReset() {
1114
1216
  if (!this.resetArmed) {
@@ -1180,7 +1282,8 @@ function mount(options = {}) {
1180
1282
  ensureStyles();
1181
1283
  const controller = new A11yController({
1182
1284
  storageKey: options.storageKey,
1183
- locale: options.locale
1285
+ locale: options.locale,
1286
+ syncDocumentLocale: options.syncDocumentLocale
1184
1287
  });
1185
1288
  let widget = null;
1186
1289
  if (!options.headless) {
package/dist/styles.css CHANGED
@@ -12,11 +12,93 @@
12
12
  border: 0 !important;
13
13
  }
14
14
 
15
- /* Font scale fallback when design tokens are not present */
15
+ /* Font scale rem-based layouts follow html font-size */
16
16
  html {
17
17
  font-size: calc(100% * var(--user-font-scale, var(--dga-font-scale, 1)));
18
18
  }
19
19
 
20
+ /* Make spacing / align apply broadly so host pages notice the change */
21
+ [data-a11y~='has-line-height'] body,
22
+ [data-a11y~='has-line-height'] p,
23
+ [data-a11y~='has-line-height'] li,
24
+ [data-a11y~='has-line-height'] h1,
25
+ [data-a11y~='has-line-height'] h2,
26
+ [data-a11y~='has-line-height'] h3,
27
+ [data-a11y~='has-line-height'] h4,
28
+ [data-a11y~='has-line-height'] h5,
29
+ [data-a11y~='has-line-height'] h6,
30
+ [data-a11y~='has-line-height'] td,
31
+ [data-a11y~='has-line-height'] th,
32
+ [data-a11y~='has-line-height'] label,
33
+ [data-a11y~='has-line-height'] .lede,
34
+ [data-a11y~='has-line-height'] .section-lede {
35
+ line-height: var(--user-line-height, 1.6) !important;
36
+ }
37
+
38
+ [data-a11y~='has-letter-spacing'] body,
39
+ [data-a11y~='has-letter-spacing'] p,
40
+ [data-a11y~='has-letter-spacing'] li,
41
+ [data-a11y~='has-letter-spacing'] h1,
42
+ [data-a11y~='has-letter-spacing'] h2,
43
+ [data-a11y~='has-letter-spacing'] h3,
44
+ [data-a11y~='has-letter-spacing'] h4,
45
+ [data-a11y~='has-letter-spacing'] h5,
46
+ [data-a11y~='has-letter-spacing'] h6,
47
+ [data-a11y~='has-letter-spacing'] label {
48
+ letter-spacing: var(--user-letter-spacing, 0) !important;
49
+ }
50
+
51
+ [data-a11y~='has-word-spacing'] body,
52
+ [data-a11y~='has-word-spacing'] p,
53
+ [data-a11y~='has-word-spacing'] li,
54
+ [data-a11y~='has-word-spacing'] h1,
55
+ [data-a11y~='has-word-spacing'] h2,
56
+ [data-a11y~='has-word-spacing'] h3,
57
+ [data-a11y~='has-word-spacing'] h4,
58
+ [data-a11y~='has-word-spacing'] h5,
59
+ [data-a11y~='has-word-spacing'] h6 {
60
+ word-spacing: var(--user-word-spacing, 0) !important;
61
+ }
62
+
63
+ [data-a11y~='text-align-start'] body,
64
+ [data-a11y~='text-align-start'] main,
65
+ [data-a11y~='text-align-start'] article,
66
+ [data-a11y~='text-align-start'] section,
67
+ [data-a11y~='text-align-start'] p,
68
+ [data-a11y~='text-align-start'] li,
69
+ [data-a11y~='text-align-start'] h1,
70
+ [data-a11y~='text-align-start'] h2,
71
+ [data-a11y~='text-align-start'] h3,
72
+ [data-a11y~='text-align-start'] h4,
73
+ [data-a11y~='text-align-start'] h5,
74
+ [data-a11y~='text-align-start'] h6 {
75
+ text-align: start !important;
76
+ }
77
+
78
+ [data-a11y~='text-align-end'] body,
79
+ [data-a11y~='text-align-end'] main,
80
+ [data-a11y~='text-align-end'] article,
81
+ [data-a11y~='text-align-end'] section,
82
+ [data-a11y~='text-align-end'] p,
83
+ [data-a11y~='text-align-end'] li,
84
+ [data-a11y~='text-align-end'] h1,
85
+ [data-a11y~='text-align-end'] h2,
86
+ [data-a11y~='text-align-end'] h3,
87
+ [data-a11y~='text-align-end'] h4,
88
+ [data-a11y~='text-align-end'] h5,
89
+ [data-a11y~='text-align-end'] h6 {
90
+ text-align: end !important;
91
+ }
92
+
93
+ [data-a11y~='text-align-justify'] body,
94
+ [data-a11y~='text-align-justify'] main,
95
+ [data-a11y~='text-align-justify'] article,
96
+ [data-a11y~='text-align-justify'] section,
97
+ [data-a11y~='text-align-justify'] p,
98
+ [data-a11y~='text-align-justify'] li {
99
+ text-align: justify !important;
100
+ }
101
+
20
102
  /* ——— Document effects ——— */
21
103
 
22
104
  [data-a11y~='reduce-motion'] *,
@@ -152,42 +234,6 @@ html {
152
234
  outline-offset: 4px !important;
153
235
  }
154
236
 
155
- [data-a11y~='has-line-height'] body,
156
- [data-a11y~='has-line-height'] p,
157
- [data-a11y~='has-line-height'] li {
158
- line-height: var(--user-line-height, 1.6) !important;
159
- }
160
-
161
- [data-a11y~='has-letter-spacing'] body,
162
- [data-a11y~='has-letter-spacing'] p,
163
- [data-a11y~='has-letter-spacing'] li {
164
- letter-spacing: var(--user-letter-spacing, 0) !important;
165
- }
166
-
167
- [data-a11y~='has-word-spacing'] body,
168
- [data-a11y~='has-word-spacing'] p,
169
- [data-a11y~='has-word-spacing'] li {
170
- word-spacing: var(--user-word-spacing, 0) !important;
171
- }
172
-
173
- [data-a11y~='text-align-start'] body,
174
- [data-a11y~='text-align-start'] p,
175
- [data-a11y~='text-align-start'] li {
176
- text-align: start !important;
177
- }
178
-
179
- [data-a11y~='text-align-end'] body,
180
- [data-a11y~='text-align-end'] p,
181
- [data-a11y~='text-align-end'] li {
182
- text-align: end !important;
183
- }
184
-
185
- [data-a11y~='text-align-justify'] body,
186
- [data-a11y~='text-align-justify'] p,
187
- [data-a11y~='text-align-justify'] li {
188
- text-align: justify !important;
189
- }
190
-
191
237
  @media not (forced-colors: active) {
192
238
  html[data-a11y-filter='monochrome'] {
193
239
  filter: grayscale(1);
@@ -233,12 +279,34 @@ html {
233
279
  --a11y-fab-size: 3.5rem;
234
280
  --a11y-fab-edge: 2rem;
235
281
  --a11y-fab-stack-step: 4.25rem;
282
+ /* Beat host overlays (modals, CDK, 2147483000 FABs). Override via --a11y-z. */
283
+ --a11y-z: 2147483647;
284
+ position: fixed;
285
+ inset: 0;
286
+ z-index: var(--a11y-z);
287
+ isolation: isolate;
288
+ pointer-events: none;
289
+ /* Isolate widget chrome from page font-scale / spacing modes */
236
290
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
237
291
  font-size: 14px;
238
292
  line-height: 1.4;
293
+ letter-spacing: normal !important;
294
+ word-spacing: normal !important;
295
+ text-align: start;
239
296
  color: var(--a11y-text);
240
297
  }
241
298
 
299
+ .a11y-root * {
300
+ letter-spacing: normal;
301
+ word-spacing: normal;
302
+ }
303
+
304
+ .a11y-fab,
305
+ .a11y-layer,
306
+ .a11y-mask-toolbar {
307
+ pointer-events: auto;
308
+ }
309
+
242
310
  .a11y-fab {
243
311
  position: fixed;
244
312
  z-index: 390;
@@ -357,6 +425,10 @@ html {
357
425
  animation-name: a11y-panel-from-start;
358
426
  }
359
427
 
428
+ .a11y-panel--static {
429
+ animation: none !important;
430
+ }
431
+
360
432
  [dir='rtl'] .a11y-panel--end {
361
433
  animation-name: a11y-panel-from-start;
362
434
  }
@@ -365,6 +437,10 @@ html {
365
437
  animation-name: a11y-panel-from-end;
366
438
  }
367
439
 
440
+ [dir='rtl'] .a11y-panel--static {
441
+ animation: none !important;
442
+ }
443
+
368
444
  .a11y-panel__header,
369
445
  .a11y-panel__footer {
370
446
  flex-shrink: 0;
@@ -510,6 +586,20 @@ html {
510
586
  background: var(--a11y-neutral);
511
587
  }
512
588
 
589
+ .a11y-stepper .a11y-btn {
590
+ width: 1.75rem;
591
+ height: 1.75rem;
592
+ min-height: 1.75rem;
593
+ padding: 0;
594
+ flex-shrink: 0;
595
+ color: var(--a11y-text);
596
+ }
597
+
598
+ .a11y-stepper .a11y-icon {
599
+ display: block;
600
+ flex-shrink: 0;
601
+ }
602
+
513
603
  .a11y-stepper__value {
514
604
  min-width: 4.5rem;
515
605
  text-align: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a.nemreen/a11y",
3
- "version": "0.1.2",
3
+ "version": "0.1.6",
4
4
  "description": "Framework-agnostic accessibility tools widget — profiles, readable experience, visual filters. Works with Angular, React, Vue, and plain HTML.",
5
5
  "author": {
6
6
  "name": "Ahmed Nemreen",
@@ -24,8 +24,15 @@
24
24
  "exports": {
25
25
  ".": {
26
26
  "types": "./dist/index.d.ts",
27
- "import": "./dist/index.js",
28
- "require": "./dist/index.cjs"
27
+ "import": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "require": {
32
+ "types": "./dist/index.d.cts",
33
+ "default": "./dist/index.cjs"
34
+ },
35
+ "default": "./dist/index.js"
29
36
  },
30
37
  "./styles.css": "./dist/styles.css"
31
38
  },