@cahyo-dimas/freeday 1.52.1 → 1.54.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/dist/freeday.js CHANGED
@@ -1,5 +1,213 @@
1
1
  /* Freeday enhancers — GENERATED by tokens/build.mjs. Do not edit by hand.
2
2
  * Concatenation of every src/*.js enhancer (each a self-contained IIFE). */
3
+ /* Freeday — app shell enhancer (optional, zero-dependency).
4
+ * Opt in with <div class="fdy-app" data-fdy-app>; the markup is otherwise unchanged.
5
+ *
6
+ * The shell has always shipped the state classes and no behaviour, which left every consumer to
7
+ * assemble the toggle, Escape, focus trap, focus restore and `inert` themselves — and the two
8
+ * hand-rolled copies in this repo's own docs already disagreed about which of those exist. Below
9
+ * 721px the sidebar is an OVERLAY (app-shell.css: position:fixed + translateX(-100%)), so it needs
10
+ * the same treatment a modal drawer gets; at or above 721px it is a column that merely collapses.
11
+ *
12
+ * One rule covers both a hidden nav and a hidden page:
13
+ * - `__sidebar` is inert whenever the nav is NOT visible. Off-canvas and collapsed panels stay in
14
+ * the tab order otherwise — translateX(-100%) and width:0 hide a thing from the eye, not from
15
+ * the keyboard, which is how a nav nobody can see still swallows Tab.
16
+ * - `__content` is inert only while the nav is an OPEN OVERLAY, so Tab cannot wander behind the
17
+ * backdrop.
18
+ *
19
+ * Emits a bubbling `fdy-app-nav` CustomEvent (detail {visible}) whenever the nav's visibility
20
+ * changes, and takes `FreedayAppShell.setVisible(root, visible)` from outside. Those two exist for
21
+ * the same reason the other enhancers have them: a host that keeps its own state — the Blazor
22
+ * wrapper binding @bind-NavOpen, an app persisting the collapsed preference — has to be able to
23
+ * hear the change and to drive it, without owning the behaviour twice.
24
+ * FreedayAppShell.init(root) for late-mounted markup.
25
+ */
26
+ (function () {
27
+ 'use strict';
28
+
29
+ /* Must match app-shell.css, which switches at (min-width:721px) / (max-width:720px). */
30
+ var WIDE = '(min-width: 721px)';
31
+
32
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]),'
33
+ + ' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
34
+
35
+ /* getClientRects(), not offsetParent: an overlay sidebar is position:fixed, and a fixed element
36
+ reports no offsetParent at all — filtering on it would call every nav item invisible. */
37
+ function focusables(root) {
38
+ return Array.prototype.filter.call(root.querySelectorAll(FOCUSABLE), function (el) {
39
+ return el.getClientRects().length > 0;
40
+ });
41
+ }
42
+
43
+ function setInert(el, on) {
44
+ if (!el) return;
45
+ if (on) el.setAttribute('inert', '');
46
+ else el.removeAttribute('inert');
47
+ }
48
+
49
+ function initShell(app) {
50
+ if (app.dataset.fdyAppShellReady === '1') return;
51
+ app.dataset.fdyAppShellReady = '1';
52
+
53
+ var toggle = app.querySelector('.fdy-app__navtoggle');
54
+ var sidebar = app.querySelector('.fdy-app__sidebar');
55
+ if (!toggle || !sidebar) return;
56
+ var content = app.querySelector('.fdy-app__content');
57
+ var backdrop = app.querySelector('.fdy-app__backdrop');
58
+
59
+ var mqWide = window.matchMedia(WIDE);
60
+ var restoreTo = null;
61
+ var lastVisible = null;
62
+
63
+ function isOverlayOpen() { return app.classList.contains('fdy-app--nav-open'); }
64
+ function isCollapsed() { return app.classList.contains('fdy-app--nav-collapsed'); }
65
+ function navVisible() { return mqWide.matches ? !isCollapsed() : isOverlayOpen(); }
66
+
67
+ /* aria-expanded answers "is the nav showing?" in BOTH modes — the two state classes are the
68
+ kit's business, not the reader's. */
69
+ function sync() {
70
+ var visible = navVisible();
71
+ toggle.setAttribute('aria-expanded', String(visible));
72
+ setInert(sidebar, !visible);
73
+ setInert(content, !mqWide.matches && visible);
74
+ /* Announce only real changes. The first sync() runs at init to describe the state the markup
75
+ arrived in, which is not something a host asked for and must not look like one. */
76
+ if (lastVisible !== null && visible !== lastVisible) {
77
+ app.dispatchEvent(new CustomEvent('fdy-app-nav', { bubbles: true, detail: { visible: visible } }));
78
+ }
79
+ lastVisible = visible;
80
+ }
81
+
82
+ function open() {
83
+ if (isOverlayOpen()) return;
84
+ restoreTo = document.activeElement;
85
+ app.classList.add('fdy-app--nav-open');
86
+ sync();
87
+ var first = focusables(sidebar)[0];
88
+ if (first) {
89
+ first.focus();
90
+ } else {
91
+ sidebar.setAttribute('tabindex', '-1');
92
+ sidebar.focus();
93
+ }
94
+ }
95
+
96
+ /* restoreFocus is false when the viewport closed it rather than the user — a resize must not
97
+ yank focus out from under whatever the reader was doing. */
98
+ function close(restoreFocus) {
99
+ if (!isOverlayOpen()) return;
100
+ app.classList.remove('fdy-app--nav-open');
101
+ sync();
102
+ if (restoreFocus === false) return;
103
+ /* Back to the control that opened it. document.body is where a keyboard user gets stranded,
104
+ so anything that is gone, detached or the body itself falls back to the toggle. */
105
+ var target = (restoreTo && restoreTo !== document.body && document.contains(restoreTo))
106
+ ? restoreTo : toggle;
107
+ if (typeof target.focus === 'function') target.focus();
108
+ }
109
+
110
+ toggle.addEventListener('click', function () {
111
+ if (mqWide.matches) {
112
+ app.classList.toggle('fdy-app--nav-collapsed');
113
+ sync();
114
+ } else if (isOverlayOpen()) {
115
+ close();
116
+ } else {
117
+ open();
118
+ }
119
+ });
120
+
121
+ if (backdrop) backdrop.addEventListener('click', function () { close(); });
122
+
123
+ /* Following a link inside an overlay nav means "take me there" — leaving the panel open over
124
+ the page you just asked for is the one thing every hand-rolled copy in this repo disagreed on. */
125
+ sidebar.addEventListener('click', function (e) {
126
+ if (!isOverlayOpen()) return;
127
+ if (e.target.closest && e.target.closest('.fdy-nav__item')) close();
128
+ });
129
+
130
+ document.addEventListener('keydown', function (e) {
131
+ if (!isOverlayOpen()) return;
132
+ if (e.key === 'Escape') {
133
+ e.preventDefault();
134
+ close();
135
+ return;
136
+ }
137
+ if (e.key !== 'Tab') return;
138
+ /* The trap. `inert` on the content stops Tab reaching the page, but without this the focus
139
+ would walk out of the document into the browser's own chrome instead of cycling. */
140
+ var items = focusables(sidebar);
141
+ if (items.length === 0) {
142
+ e.preventDefault();
143
+ return;
144
+ }
145
+ var first = items[0], last = items[items.length - 1];
146
+ if (e.shiftKey && document.activeElement === first) {
147
+ e.preventDefault();
148
+ last.focus();
149
+ } else if (!e.shiftKey && document.activeElement === last) {
150
+ e.preventDefault();
151
+ first.focus();
152
+ }
153
+ });
154
+
155
+ /* Crossing the breakpoint with the overlay open would otherwise leave --nav-open set and the
156
+ content inert forever: the panel becomes a static column again, and the page it is covering
157
+ can no longer be clicked or read. */
158
+ mqWide.addEventListener('change', function () {
159
+ if (mqWide.matches && isOverlayOpen()) close(false);
160
+ else sync();
161
+ });
162
+
163
+ sync();
164
+
165
+ /* The same handle the other enhancers expose (`_fdyCombo` and friends): a host that binds its
166
+ own state needs to drive this without reaching for the class names the kit reserves. */
167
+ app._fdyAppShell = {
168
+ isVisible: navVisible,
169
+ setVisible: function (visible) {
170
+ if (visible === navVisible()) return;
171
+ if (mqWide.matches) {
172
+ app.classList.toggle('fdy-app--nav-collapsed', !visible);
173
+ sync();
174
+ } else if (visible) {
175
+ open();
176
+ } else {
177
+ close();
178
+ }
179
+ },
180
+ };
181
+ }
182
+
183
+ function initShells(context) {
184
+ var ctx = context || document;
185
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON
186
+ the widget. */
187
+ if (ctx.matches && ctx.matches('[data-fdy-app]')) initShell(ctx);
188
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-app]'), initShell);
189
+ }
190
+
191
+ if (document.readyState === 'loading') {
192
+ document.addEventListener('DOMContentLoaded', function () { initShells(); });
193
+ } else {
194
+ initShells();
195
+ }
196
+
197
+ window.FreedayAppShell = {
198
+ init: initShells,
199
+ initAll: initShells,
200
+ /* Both take the shell root. A missing or un-initialised root is a no-op rather than a throw:
201
+ a host may race the enhancer on first render, and a crash there is worse than a late sync. */
202
+ setVisible: function (root, visible) {
203
+ if (root && root._fdyAppShell) root._fdyAppShell.setVisible(visible === true);
204
+ },
205
+ isVisible: function (root) {
206
+ return !!(root && root._fdyAppShell && root._fdyAppShell.isVisible());
207
+ },
208
+ };
209
+ })();
210
+
3
211
  /* Freeday — autocomplete enhancer (optional, zero-dependency).
4
212
  * Editable combobox (WAI-ARIA APG): a text input filters a role="listbox" of role="option"
5
213
  * items as you type; select fills the input. Auto-inits [data-fdy-autocomplete]. Options are
@@ -46,7 +46,7 @@ upload, tree, tabs, menu, rating, slider, form validation, carousel, timepicker)
46
46
  wrapper — use the raw markup and hydrate it:
47
47
 
48
48
  - **Vue / React** — `useFreeday(rootRef)` from the same import path, plus `import '@cahyo-dimas/freeday'`
49
- once at app entry to register the enhancers. (The ten typed components do **not** need this: they
49
+ once at app entry to register the enhancers. (The eleven typed components do **not** need this: they
50
50
  are native Vue/React implementations of the same markup, not wrappers over the enhancer.)
51
51
  - **Blazor** — `FreedayBlazor.initAll` interop. (Here the typed components *are* thin wrappers over
52
52
  the enhancers, so the enhancer script is always required.)
@@ -33,7 +33,7 @@
33
33
  Doctrine behind the choices: USAGE.md.
34
34
  ══════════════════════════════════════════════════════════════════════════════════════════ -->
35
35
 
36
- <div class="fdy-app" id="app">
36
+ <div class="fdy-app" id="app" data-fdy-app>
37
37
  <a class="fdy-skip" href="#main">Skip to main content</a>
38
38
 
39
39
  <!-- Sidebar comes FIRST: .fdy-app is a flex row of [sidebar | content]. The brand lives in the
@@ -407,6 +407,7 @@
407
407
  </dialog>
408
408
 
409
409
  <!-- Enhancers: only the ones this screen actually uses. Zero dependencies, load order free. -->
410
+ <script src="../dist/freeday-app-shell.js"></script>
410
411
  <script src="../dist/freeday-popover.js"></script>
411
412
  <script src="../dist/freeday-select.js"></script>
412
413
  <script src="../dist/freeday-datepicker.js"></script>
@@ -417,21 +418,9 @@
417
418
  <script src="../dist/freeday-toast.js"></script>
418
419
 
419
420
  <script>
420
- // The shell's only wiring: the nav toggle. Two state classes, no library.
421
- // Above 720px the sidebar collapses to zero width (--nav-collapsed); at or below it slides in as
422
- // an off-canvas drawer (--nav-open). 720px is where app-shell.css switches — keep them in step.
423
- const app = document.getElementById('app');
424
- const toggle = app.querySelector('.fdy-app__navtoggle');
425
- const wide = () => window.matchMedia('(min-width: 721px)').matches;
426
- toggle.addEventListener('click', () => {
427
- const cls = wide() ? 'fdy-app--nav-collapsed' : 'fdy-app--nav-open';
428
- const open = app.classList.toggle(cls);
429
- toggle.setAttribute('aria-expanded', String(wide() ? !open : open));
430
- });
431
- app.querySelector('.fdy-app__backdrop').addEventListener('click', () => {
432
- app.classList.remove('fdy-app--nav-open');
433
- toggle.setAttribute('aria-expanded', 'false');
434
- });
421
+ // The nav toggle is the kit's now: `data-fdy-app` on the shell is the whole wiring. What used to
422
+ // live here handled the two state classes and nothing else — no Escape, no focus trap, no inert
423
+ // background, no focus restore — which is exactly why the behaviour moved into the kit.
435
424
 
436
425
  // Theme + density live on <html>, set once — never per component.
437
426
  const root = document.documentElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.52.1",
3
+ "version": "1.54.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@
92
92
  "scripts": {
93
93
  "build": "node tokens/build.mjs",
94
94
  "test": "node --test",
95
- "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs",
95
+ "test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs",
96
96
  "prepack": "node tokens/build.mjs",
97
97
  "version": "node tokens/build.mjs && git add dist",
98
98
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"