@a.nemreen/a11y 0.1.1 → 0.1.3

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' });
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
  }
@@ -676,8 +683,28 @@ var ICONS = {
676
683
  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
684
  colorPicker: '<path d="M12 3a7 7 0 0 1 0 14h-1l-4 4v-5a7 7 0 0 1 5-13Z"/>'
678
685
  };
686
+ var STROKE_ICONS = /* @__PURE__ */ new Set([
687
+ "close",
688
+ "heading",
689
+ "linkAlt",
690
+ "pause",
691
+ "textFont",
692
+ "textAlign",
693
+ "textUnderline",
694
+ "letterSpacing",
695
+ "maximize",
696
+ "minus",
697
+ "plus",
698
+ "glasses",
699
+ "sun",
700
+ "touch",
701
+ "mask"
702
+ ]);
679
703
  function iconSvg(name, size = 18) {
680
704
  const path = ICONS[name] ?? ICONS.access;
705
+ if (STROKE_ICONS.has(name)) {
706
+ 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>`;
707
+ }
681
708
  return `<svg class="a11y-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">${path}</svg>`;
682
709
  }
683
710
 
@@ -714,13 +741,25 @@ var A11yWidget = class {
714
741
  this.resetArmed = false;
715
742
  this.resetTimer = null;
716
743
  this.draggingMask = false;
744
+ this.wasOpen = false;
745
+ this.panelScrollTop = 0;
717
746
  this.onKeyDown = (e) => this.handleKey(e);
718
747
  this.onPointerMove = (e) => {
719
748
  if (!this.draggingMask) return;
720
- this.ctrl.setMaskY(e.clientY);
749
+ this.ctrl.setMaskY(e.clientY, { persist: false });
750
+ this.syncMaskDom();
721
751
  };
722
752
  this.onPointerUp = () => {
753
+ if (!this.draggingMask) return;
723
754
  this.draggingMask = false;
755
+ this.ctrl.setMaskY(this.ctrl.maskY, { persist: true });
756
+ };
757
+ this.onRootClick = (e) => this.handleClick(e);
758
+ this.onRootPointerDown = (e) => {
759
+ const t = e.target?.closest?.('[data-a11y-action="mask-drag"]');
760
+ if (!t || !this.rootEl?.contains(t)) return;
761
+ e.preventDefault();
762
+ this.draggingMask = true;
724
763
  };
725
764
  this.ctrl = ctrl;
726
765
  this.position = options.position ?? "end";
@@ -733,6 +772,8 @@ var A11yWidget = class {
733
772
  this.rootEl.className = "a11y-root";
734
773
  this.rootEl.setAttribute("data-a11y-ui", "");
735
774
  this.mountRoot.appendChild(this.rootEl);
775
+ this.rootEl.addEventListener("click", this.onRootClick);
776
+ this.rootEl.addEventListener("pointerdown", this.onRootPointerDown);
736
777
  this.unsub = this.ctrl.subscribe(() => this.render());
737
778
  document.addEventListener("keydown", this.onKeyDown);
738
779
  document.addEventListener("pointermove", this.onPointerMove);
@@ -742,6 +783,8 @@ var A11yWidget = class {
742
783
  destroy() {
743
784
  this.unsub?.();
744
785
  this.unsub = null;
786
+ this.rootEl?.removeEventListener("click", this.onRootClick);
787
+ this.rootEl?.removeEventListener("pointerdown", this.onRootPointerDown);
745
788
  document.removeEventListener("keydown", this.onKeyDown);
746
789
  document.removeEventListener("pointermove", this.onPointerMove);
747
790
  document.removeEventListener("pointerup", this.onPointerUp);
@@ -814,7 +857,16 @@ var A11yWidget = class {
814
857
  const chips = this.activeChips();
815
858
  const badge = chips.length;
816
859
  const open = this.ctrl.isOpen;
860
+ const opening = open && !this.wasOpen;
861
+ const stayingOpen = open && this.wasOpen;
817
862
  const readingMask = this.ctrl.hasEffectiveMode("reading-mask");
863
+ const dir = this.ctrl.locale === "ar" ? "rtl" : "ltr";
864
+ if (stayingOpen) {
865
+ const body = this.rootEl.querySelector(".a11y-panel__body");
866
+ if (body) this.panelScrollTop = body.scrollTop;
867
+ }
868
+ this.rootEl.lang = this.ctrl.locale;
869
+ this.rootEl.dir = dir;
818
870
  this.rootEl.innerHTML = `
819
871
  <svg class="a11y-deuteranopia-svg" aria-hidden="true" focusable="false">
820
872
  <filter id="a11y-deuteranopia">
@@ -840,23 +892,42 @@ var A11yWidget = class {
840
892
  ${badge > 0 ? `<span class="a11y-fab__badge" aria-hidden="true">${badge > 9 ? "9+" : badge}</span>` : ""}
841
893
  </button>
842
894
 
843
- ${open ? this.renderPanel(c, chips) : ""}
895
+ ${open ? this.renderPanel(c, chips, stayingOpen) : ""}
844
896
  ${readingMask ? this.renderMask(c) : ""}
845
897
  `;
846
- this.bindEvents();
847
- if (open) {
898
+ if (stayingOpen) {
899
+ const body = this.rootEl.querySelector(".a11y-panel__body");
900
+ if (body) body.scrollTop = this.panelScrollTop;
901
+ }
902
+ if (opening) {
848
903
  const closeBtn = this.rootEl.querySelector('[data-a11y-action="close"]');
849
904
  queueMicrotask(() => closeBtn?.focus());
850
905
  }
906
+ this.wasOpen = open;
851
907
  }
852
- renderPanel(c, chips) {
908
+ syncMaskDom() {
909
+ if (!this.rootEl) return;
910
+ const shades = this.rootEl.querySelectorAll(".a11y-mask-shade");
911
+ const toolbar = this.rootEl.querySelector(".a11y-mask-toolbar");
912
+ if (shades[0]) {
913
+ shades[0].style.height = "calc(var(--a11y-mask-y) - var(--a11y-mask-band))";
914
+ }
915
+ if (shades[1]) {
916
+ shades[1].style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band))";
917
+ }
918
+ if (toolbar) {
919
+ toolbar.style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band) + 8px)";
920
+ }
921
+ }
922
+ renderPanel(c, chips, staticPanel) {
853
923
  const profileCount = this.ctrl.activeModeCount("modes");
924
+ const panelClass = staticPanel ? `a11y-panel a11y-panel--${this.position} a11y-panel--static` : `a11y-panel a11y-panel--${this.position}`;
854
925
  return `
855
926
  <div class="a11y-layer" role="presentation">
856
927
  <div class="a11y-backdrop" data-a11y-action="close" aria-hidden="true"></div>
857
928
  <aside
858
929
  id="${PANEL_ID}"
859
- class="a11y-panel a11y-panel--${this.position}"
930
+ class="${panelClass}"
860
931
  role="dialog"
861
932
  aria-modal="true"
862
933
  aria-label="${escapeAttr(c.panelLabel)}"
@@ -1050,67 +1121,65 @@ var A11yWidget = class {
1050
1121
  </button>
1051
1122
  </div>`;
1052
1123
  }
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
- });
1124
+ handleClick(e) {
1125
+ const target = e.target;
1126
+ if (!target || !this.rootEl) return;
1127
+ const actionEl = target.closest("[data-a11y-action]");
1128
+ if (actionEl && this.rootEl.contains(actionEl)) {
1129
+ const action = actionEl.getAttribute("data-a11y-action");
1130
+ if (action === "toggle") {
1131
+ this.ctrl.toggle(actionEl);
1132
+ } else if (action === "close") {
1133
+ this.ctrl.close();
1134
+ } else if (action === "reset") {
1135
+ this.onReset();
1136
+ } else if (action === "mask-smaller") {
1137
+ this.ctrl.adjustMaskBand(-20);
1138
+ } else if (action === "mask-larger") {
1139
+ this.ctrl.adjustMaskBand(20);
1140
+ } else if (action === "mask-close") {
1141
+ this.ctrl.setMode("reading-mask", false);
1142
+ }
1143
+ return;
1144
+ }
1145
+ const bundleEl = target.closest("[data-a11y-bundle]");
1146
+ if (bundleEl && this.rootEl.contains(bundleEl)) {
1147
+ const id = bundleEl.getAttribute("data-a11y-bundle");
1148
+ this.ctrl.setMode(id, !this.ctrl.hasMode(id));
1149
+ return;
1150
+ }
1151
+ const toggleEl = target.closest("[data-a11y-toggle]");
1152
+ if (toggleEl && this.rootEl.contains(toggleEl)) {
1153
+ const mode = toggleEl.getAttribute("data-a11y-toggle");
1154
+ this.ctrl.setMode(mode, !this.ctrl.hasEffectiveMode(mode));
1155
+ return;
1156
+ }
1157
+ const filterEl = target.closest("[data-a11y-filter]");
1158
+ if (filterEl && this.rootEl.contains(filterEl)) {
1159
+ const filter = filterEl.getAttribute("data-a11y-filter");
1160
+ this.ctrl.setVisualFilter(filter);
1161
+ return;
1162
+ }
1163
+ const stepEl = target.closest("[data-a11y-step]");
1164
+ if (stepEl && this.rootEl.contains(stepEl)) {
1165
+ e.preventDefault();
1166
+ const id = stepEl.getAttribute("data-a11y-step");
1167
+ const delta = Number(stepEl.getAttribute("data-delta") || "1");
1168
+ if (id === "font") this.ctrl.stepFontStep(delta);
1169
+ else if (id === "align") this.ctrl.stepTextAlign(delta);
1170
+ else if (id === "line") this.ctrl.stepLineHeight(delta);
1171
+ else if (id === "letter") this.ctrl.stepLetterSpacing(delta);
1172
+ else if (id === "word") this.ctrl.stepWordSpacing(delta);
1173
+ return;
1174
+ }
1175
+ const focusEl = target.closest("[data-a11y-focus]");
1176
+ if (focusEl && this.rootEl.contains(focusEl)) {
1177
+ const id = focusEl.getAttribute("data-a11y-focus");
1178
+ const control = this.rootEl.querySelector(`[data-a11y-control="${id}"]`);
1179
+ control?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1180
+ const focusable = control?.querySelector("button, [tabindex]");
1181
+ focusable?.focus();
1182
+ }
1114
1183
  }
1115
1184
  onReset() {
1116
1185
  if (!this.resetArmed) {
@@ -1182,7 +1251,8 @@ function mount(options = {}) {
1182
1251
  ensureStyles();
1183
1252
  const controller = new A11yController({
1184
1253
  storageKey: options.storageKey,
1185
- locale: options.locale
1254
+ locale: options.locale,
1255
+ syncDocumentLocale: options.syncDocumentLocale
1186
1256
  });
1187
1257
  let widget = null;
1188
1258
  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,9 +135,13 @@ 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;
132
145
  constructor(ctrl: A11yController, options?: A11yWidgetOptions);
133
146
  mount(): void;
134
147
  destroy(): void;
@@ -136,10 +149,11 @@ declare class A11yWidget {
136
149
  private fabBottom;
137
150
  private activeChips;
138
151
  private render;
152
+ private syncMaskDom;
139
153
  private renderPanel;
140
154
  private renderSteppers;
141
155
  private renderMask;
142
- private bindEvents;
156
+ private handleClick;
143
157
  private onReset;
144
158
  private handleKey;
145
159
  }
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,9 +135,13 @@ 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;
132
145
  constructor(ctrl: A11yController, options?: A11yWidgetOptions);
133
146
  mount(): void;
134
147
  destroy(): void;
@@ -136,10 +149,11 @@ declare class A11yWidget {
136
149
  private fabBottom;
137
150
  private activeChips;
138
151
  private render;
152
+ private syncMaskDom;
139
153
  private renderPanel;
140
154
  private renderSteppers;
141
155
  private renderMask;
142
- private bindEvents;
156
+ private handleClick;
143
157
  private onReset;
144
158
  private handleKey;
145
159
  }
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
  }
@@ -674,8 +681,28 @@ var ICONS = {
674
681
  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
682
  colorPicker: '<path d="M12 3a7 7 0 0 1 0 14h-1l-4 4v-5a7 7 0 0 1 5-13Z"/>'
676
683
  };
684
+ var STROKE_ICONS = /* @__PURE__ */ new Set([
685
+ "close",
686
+ "heading",
687
+ "linkAlt",
688
+ "pause",
689
+ "textFont",
690
+ "textAlign",
691
+ "textUnderline",
692
+ "letterSpacing",
693
+ "maximize",
694
+ "minus",
695
+ "plus",
696
+ "glasses",
697
+ "sun",
698
+ "touch",
699
+ "mask"
700
+ ]);
677
701
  function iconSvg(name, size = 18) {
678
702
  const path = ICONS[name] ?? ICONS.access;
703
+ if (STROKE_ICONS.has(name)) {
704
+ 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>`;
705
+ }
679
706
  return `<svg class="a11y-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">${path}</svg>`;
680
707
  }
681
708
 
@@ -712,13 +739,25 @@ var A11yWidget = class {
712
739
  this.resetArmed = false;
713
740
  this.resetTimer = null;
714
741
  this.draggingMask = false;
742
+ this.wasOpen = false;
743
+ this.panelScrollTop = 0;
715
744
  this.onKeyDown = (e) => this.handleKey(e);
716
745
  this.onPointerMove = (e) => {
717
746
  if (!this.draggingMask) return;
718
- this.ctrl.setMaskY(e.clientY);
747
+ this.ctrl.setMaskY(e.clientY, { persist: false });
748
+ this.syncMaskDom();
719
749
  };
720
750
  this.onPointerUp = () => {
751
+ if (!this.draggingMask) return;
721
752
  this.draggingMask = false;
753
+ this.ctrl.setMaskY(this.ctrl.maskY, { persist: true });
754
+ };
755
+ this.onRootClick = (e) => this.handleClick(e);
756
+ this.onRootPointerDown = (e) => {
757
+ const t = e.target?.closest?.('[data-a11y-action="mask-drag"]');
758
+ if (!t || !this.rootEl?.contains(t)) return;
759
+ e.preventDefault();
760
+ this.draggingMask = true;
722
761
  };
723
762
  this.ctrl = ctrl;
724
763
  this.position = options.position ?? "end";
@@ -731,6 +770,8 @@ var A11yWidget = class {
731
770
  this.rootEl.className = "a11y-root";
732
771
  this.rootEl.setAttribute("data-a11y-ui", "");
733
772
  this.mountRoot.appendChild(this.rootEl);
773
+ this.rootEl.addEventListener("click", this.onRootClick);
774
+ this.rootEl.addEventListener("pointerdown", this.onRootPointerDown);
734
775
  this.unsub = this.ctrl.subscribe(() => this.render());
735
776
  document.addEventListener("keydown", this.onKeyDown);
736
777
  document.addEventListener("pointermove", this.onPointerMove);
@@ -740,6 +781,8 @@ var A11yWidget = class {
740
781
  destroy() {
741
782
  this.unsub?.();
742
783
  this.unsub = null;
784
+ this.rootEl?.removeEventListener("click", this.onRootClick);
785
+ this.rootEl?.removeEventListener("pointerdown", this.onRootPointerDown);
743
786
  document.removeEventListener("keydown", this.onKeyDown);
744
787
  document.removeEventListener("pointermove", this.onPointerMove);
745
788
  document.removeEventListener("pointerup", this.onPointerUp);
@@ -812,7 +855,16 @@ var A11yWidget = class {
812
855
  const chips = this.activeChips();
813
856
  const badge = chips.length;
814
857
  const open = this.ctrl.isOpen;
858
+ const opening = open && !this.wasOpen;
859
+ const stayingOpen = open && this.wasOpen;
815
860
  const readingMask = this.ctrl.hasEffectiveMode("reading-mask");
861
+ const dir = this.ctrl.locale === "ar" ? "rtl" : "ltr";
862
+ if (stayingOpen) {
863
+ const body = this.rootEl.querySelector(".a11y-panel__body");
864
+ if (body) this.panelScrollTop = body.scrollTop;
865
+ }
866
+ this.rootEl.lang = this.ctrl.locale;
867
+ this.rootEl.dir = dir;
816
868
  this.rootEl.innerHTML = `
817
869
  <svg class="a11y-deuteranopia-svg" aria-hidden="true" focusable="false">
818
870
  <filter id="a11y-deuteranopia">
@@ -838,23 +890,42 @@ var A11yWidget = class {
838
890
  ${badge > 0 ? `<span class="a11y-fab__badge" aria-hidden="true">${badge > 9 ? "9+" : badge}</span>` : ""}
839
891
  </button>
840
892
 
841
- ${open ? this.renderPanel(c, chips) : ""}
893
+ ${open ? this.renderPanel(c, chips, stayingOpen) : ""}
842
894
  ${readingMask ? this.renderMask(c) : ""}
843
895
  `;
844
- this.bindEvents();
845
- if (open) {
896
+ if (stayingOpen) {
897
+ const body = this.rootEl.querySelector(".a11y-panel__body");
898
+ if (body) body.scrollTop = this.panelScrollTop;
899
+ }
900
+ if (opening) {
846
901
  const closeBtn = this.rootEl.querySelector('[data-a11y-action="close"]');
847
902
  queueMicrotask(() => closeBtn?.focus());
848
903
  }
904
+ this.wasOpen = open;
849
905
  }
850
- renderPanel(c, chips) {
906
+ syncMaskDom() {
907
+ if (!this.rootEl) return;
908
+ const shades = this.rootEl.querySelectorAll(".a11y-mask-shade");
909
+ const toolbar = this.rootEl.querySelector(".a11y-mask-toolbar");
910
+ if (shades[0]) {
911
+ shades[0].style.height = "calc(var(--a11y-mask-y) - var(--a11y-mask-band))";
912
+ }
913
+ if (shades[1]) {
914
+ shades[1].style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band))";
915
+ }
916
+ if (toolbar) {
917
+ toolbar.style.top = "calc(var(--a11y-mask-y) + var(--a11y-mask-band) + 8px)";
918
+ }
919
+ }
920
+ renderPanel(c, chips, staticPanel) {
851
921
  const profileCount = this.ctrl.activeModeCount("modes");
922
+ const panelClass = staticPanel ? `a11y-panel a11y-panel--${this.position} a11y-panel--static` : `a11y-panel a11y-panel--${this.position}`;
852
923
  return `
853
924
  <div class="a11y-layer" role="presentation">
854
925
  <div class="a11y-backdrop" data-a11y-action="close" aria-hidden="true"></div>
855
926
  <aside
856
927
  id="${PANEL_ID}"
857
- class="a11y-panel a11y-panel--${this.position}"
928
+ class="${panelClass}"
858
929
  role="dialog"
859
930
  aria-modal="true"
860
931
  aria-label="${escapeAttr(c.panelLabel)}"
@@ -1048,67 +1119,65 @@ var A11yWidget = class {
1048
1119
  </button>
1049
1120
  </div>`;
1050
1121
  }
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
- });
1122
+ handleClick(e) {
1123
+ const target = e.target;
1124
+ if (!target || !this.rootEl) return;
1125
+ const actionEl = target.closest("[data-a11y-action]");
1126
+ if (actionEl && this.rootEl.contains(actionEl)) {
1127
+ const action = actionEl.getAttribute("data-a11y-action");
1128
+ if (action === "toggle") {
1129
+ this.ctrl.toggle(actionEl);
1130
+ } else if (action === "close") {
1131
+ this.ctrl.close();
1132
+ } else if (action === "reset") {
1133
+ this.onReset();
1134
+ } else if (action === "mask-smaller") {
1135
+ this.ctrl.adjustMaskBand(-20);
1136
+ } else if (action === "mask-larger") {
1137
+ this.ctrl.adjustMaskBand(20);
1138
+ } else if (action === "mask-close") {
1139
+ this.ctrl.setMode("reading-mask", false);
1140
+ }
1141
+ return;
1142
+ }
1143
+ const bundleEl = target.closest("[data-a11y-bundle]");
1144
+ if (bundleEl && this.rootEl.contains(bundleEl)) {
1145
+ const id = bundleEl.getAttribute("data-a11y-bundle");
1146
+ this.ctrl.setMode(id, !this.ctrl.hasMode(id));
1147
+ return;
1148
+ }
1149
+ const toggleEl = target.closest("[data-a11y-toggle]");
1150
+ if (toggleEl && this.rootEl.contains(toggleEl)) {
1151
+ const mode = toggleEl.getAttribute("data-a11y-toggle");
1152
+ this.ctrl.setMode(mode, !this.ctrl.hasEffectiveMode(mode));
1153
+ return;
1154
+ }
1155
+ const filterEl = target.closest("[data-a11y-filter]");
1156
+ if (filterEl && this.rootEl.contains(filterEl)) {
1157
+ const filter = filterEl.getAttribute("data-a11y-filter");
1158
+ this.ctrl.setVisualFilter(filter);
1159
+ return;
1160
+ }
1161
+ const stepEl = target.closest("[data-a11y-step]");
1162
+ if (stepEl && this.rootEl.contains(stepEl)) {
1163
+ e.preventDefault();
1164
+ const id = stepEl.getAttribute("data-a11y-step");
1165
+ const delta = Number(stepEl.getAttribute("data-delta") || "1");
1166
+ if (id === "font") this.ctrl.stepFontStep(delta);
1167
+ else if (id === "align") this.ctrl.stepTextAlign(delta);
1168
+ else if (id === "line") this.ctrl.stepLineHeight(delta);
1169
+ else if (id === "letter") this.ctrl.stepLetterSpacing(delta);
1170
+ else if (id === "word") this.ctrl.stepWordSpacing(delta);
1171
+ return;
1172
+ }
1173
+ const focusEl = target.closest("[data-a11y-focus]");
1174
+ if (focusEl && this.rootEl.contains(focusEl)) {
1175
+ const id = focusEl.getAttribute("data-a11y-focus");
1176
+ const control = this.rootEl.querySelector(`[data-a11y-control="${id}"]`);
1177
+ control?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1178
+ const focusable = control?.querySelector("button, [tabindex]");
1179
+ focusable?.focus();
1180
+ }
1112
1181
  }
1113
1182
  onReset() {
1114
1183
  if (!this.resetArmed) {
@@ -1180,7 +1249,8 @@ function mount(options = {}) {
1180
1249
  ensureStyles();
1181
1250
  const controller = new A11yController({
1182
1251
  storageKey: options.storageKey,
1183
- locale: options.locale
1252
+ locale: options.locale,
1253
+ syncDocumentLocale: options.syncDocumentLocale
1184
1254
  });
1185
1255
  let widget = null;
1186
1256
  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,21 @@ html {
233
279
  --a11y-fab-size: 3.5rem;
234
280
  --a11y-fab-edge: 2rem;
235
281
  --a11y-fab-stack-step: 4.25rem;
282
+ /* Isolate widget chrome from page font-scale / spacing modes */
236
283
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
237
284
  font-size: 14px;
238
285
  line-height: 1.4;
286
+ letter-spacing: normal !important;
287
+ word-spacing: normal !important;
288
+ text-align: start;
239
289
  color: var(--a11y-text);
240
290
  }
241
291
 
292
+ .a11y-root * {
293
+ letter-spacing: normal;
294
+ word-spacing: normal;
295
+ }
296
+
242
297
  .a11y-fab {
243
298
  position: fixed;
244
299
  z-index: 390;
@@ -357,6 +412,10 @@ html {
357
412
  animation-name: a11y-panel-from-start;
358
413
  }
359
414
 
415
+ .a11y-panel--static {
416
+ animation: none !important;
417
+ }
418
+
360
419
  [dir='rtl'] .a11y-panel--end {
361
420
  animation-name: a11y-panel-from-start;
362
421
  }
@@ -365,6 +424,10 @@ html {
365
424
  animation-name: a11y-panel-from-end;
366
425
  }
367
426
 
427
+ [dir='rtl'] .a11y-panel--static {
428
+ animation: none !important;
429
+ }
430
+
368
431
  .a11y-panel__header,
369
432
  .a11y-panel__footer {
370
433
  flex-shrink: 0;
@@ -510,6 +573,20 @@ html {
510
573
  background: var(--a11y-neutral);
511
574
  }
512
575
 
576
+ .a11y-stepper .a11y-btn {
577
+ width: 1.75rem;
578
+ height: 1.75rem;
579
+ min-height: 1.75rem;
580
+ padding: 0;
581
+ flex-shrink: 0;
582
+ color: var(--a11y-text);
583
+ }
584
+
585
+ .a11y-stepper .a11y-icon {
586
+ display: block;
587
+ flex-shrink: 0;
588
+ }
589
+
513
590
  .a11y-stepper__value {
514
591
  min-width: 4.5rem;
515
592
  text-align: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a.nemreen/a11y",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",