@cahyo-dimas/freeday 1.52.1 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/COMPONENTS.md +15 -2
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/dist/freeday-app-shell.js +167 -0
- package/dist/freeday.js +168 -0
- package/docs/reference-screen.html +5 -16
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
Semua perubahan penting dicatat di sini. Format longgar mengikuti
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
|
|
5
5
|
|
|
6
|
+
## [1.53.0] — 2026-08-24
|
|
7
|
+
### Added
|
|
8
|
+
- **`freeday-app-shell.js` — the shell finally ships its own behaviour** (`NEXT-UP.md` #8, reported
|
|
9
|
+
twice). `.fdy-app` shipped `__navtoggle`, `__backdrop` and the `--nav-open` / `--nav-collapsed`
|
|
10
|
+
classes with **zero JS**, and `COMPONENTS.md` told every consumer to wire it in two sentences that
|
|
11
|
+
never mentioned Escape, focus, `inert` or focus restore. Follow that exactly and you get an
|
|
12
|
+
off-canvas overlay that cannot be closed from the keyboard and lets Tab wander behind the
|
|
13
|
+
backdrop. Opt in with `data-fdy-app`; the markup is otherwise untouched, so an existing shell
|
|
14
|
+
gains the behaviour by adding one attribute and deleting its own copy.
|
|
15
|
+
Owned now: the toggle in both modes, Escape, backdrop click, closing when a `.fdy-nav__item` is
|
|
16
|
+
followed, focus into the panel and back to `__navtoggle` on close, `inert` on `__content` while
|
|
17
|
+
the overlay is open, and a Tab trap inside the panel.
|
|
18
|
+
### Fixed
|
|
19
|
+
- **A hidden nav was still a tabbable nav**, in both modes and from the beginning. Collapsed at
|
|
20
|
+
≥721px is `width:0;overflow:hidden`; off-canvas at ≤720px is `translateX(-100%)`. Both hide the
|
|
21
|
+
panel from the eye and neither hides it from the keyboard, so a nav nobody could see still
|
|
22
|
+
swallowed every Tab on the way into the page. `__sidebar` is now `inert` whenever the nav is not
|
|
23
|
+
visible — one rule for both modes rather than two patches.
|
|
24
|
+
- **Crossing the breakpoint with the overlay open stranded the page.** Widening the window left
|
|
25
|
+
`--nav-open` set and `__content` `inert`, so the page underneath could never be clicked or read
|
|
26
|
+
again — the same failure that produced `breakpoints.nav` in v1.20.0. The enhancer clears the
|
|
27
|
+
overlay state on the media-query change, and deliberately does *not* move focus while doing it: a
|
|
28
|
+
resize is not a user asking to go somewhere.
|
|
29
|
+
### Changed
|
|
30
|
+
- **`docs/index.html` and `docs/reference-screen.html` stopped hand-rolling it**, which is the
|
|
31
|
+
proof rather than a tidy-up. The two copies had drifted: `index.html` handled Escape and
|
|
32
|
+
close-on-nav-click, `reference-screen.html` handled neither, and *neither* trapped focus, marked
|
|
33
|
+
the content `inert`, or restored focus. Two versions in one repository disagreeing is the whole
|
|
34
|
+
argument for the kit owning this.
|
|
35
|
+
### Added — guards
|
|
36
|
+
- `browser/app-shell.mjs` (5 tests), each verified to fail with the enhancer switched off: collapse
|
|
37
|
+
and its `inert` at ≥721px, the overlay and its `inert` at ≤720px, focus entering the panel and
|
|
38
|
+
cycling inside it under **trusted** Tab presses, Escape returning focus to the toggle, backdrop
|
|
39
|
+
and nav-item dismissal, and the breakpoint crossing. One of the five originally passed against a
|
|
40
|
+
shell with no behaviour at all — the nav never opened, so "it is closed" was true and meaningless
|
|
41
|
+
— and now asserts it opened first.
|
|
42
|
+
- `setViewport()` in the browser harness (`Emulation.setDeviceMetricsOverride`). A responsive
|
|
43
|
+
contract cannot be tested at a fixed window size, and the moment a layout *crosses* a breakpoint
|
|
44
|
+
is exactly where its state gets stranded.
|
|
45
|
+
|
|
6
46
|
## [1.52.1] — 2026-08-24
|
|
7
47
|
### Docs
|
|
8
48
|
- **1.51.1's close-watcher caveat named the wrong condition** (#048). It said a `showModal()` with
|
|
@@ -30,6 +70,29 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
|
|
|
30
70
|
- The fixture records each `cancel` event's `cancelable` flag; nothing prevents it, so the older
|
|
31
71
|
tests measure what they always did.
|
|
32
72
|
|
|
73
|
+
### Fixed — the kit's own suite
|
|
74
|
+
- **The browser guards did not run where the releases are made.** CI ran the 59 unit tests and
|
|
75
|
+
published; the 58 browser tests across 18 specs — pixel paint order for stacked overlays, chart
|
|
76
|
+
accessible subtrees, the user-activation matrix above — ran only when someone remembered to run
|
|
77
|
+
them locally. Automating the release through OIDC in 1.52.0 removed the someone. `ci.yml` now runs
|
|
78
|
+
both suites on every push and is *called* by `publish.yml` (`needs: test`), so the step list that
|
|
79
|
+
guards a release cannot drift from the one that runs during development, and a tag with red guards
|
|
80
|
+
never reaches npm.
|
|
81
|
+
- **A job that merely ran the command would have been green while running nothing.** Every browser
|
|
82
|
+
spec skips itself when no Chrome is found, and a skipped suite still exits 0. So the job checks
|
|
83
|
+
the binary before it starts and then parses its own summary, failing on any skip — "the job was
|
|
84
|
+
green" and "the guards ran" were separate facts, and only one of them was enforceable. Both
|
|
85
|
+
demonstrated on real runs: the assertion green at 58/0, and a deliberately broken `CHROME_BIN`
|
|
86
|
+
red.
|
|
87
|
+
- **The harness can now drive any installed Chrome.** `chrome-headless-shell` is headless by
|
|
88
|
+
construction; an ordinary Chrome binary is not and, on a machine with no display, looks for one
|
|
89
|
+
and dies. It now gets `--headless=new` (and `--no-sandbox` only under `CI`), which is what lets
|
|
90
|
+
`CHROME_BIN=<some other Chrome>` compare engines — the manoeuvre that settled #048. CI runs the
|
|
91
|
+
runner's Chrome stable, local runs Chromium 133, so both engines `COMPONENTS.md` names are
|
|
92
|
+
genuinely exercised.
|
|
93
|
+
- Spec: `docs/superpowers/specs/2026-08-24-browser-guards-in-ci-design.md`. No shipped file changed —
|
|
94
|
+
`browser/` and `.github/` are not in the published tarball, so there is no release for this.
|
|
95
|
+
|
|
33
96
|
## [1.52.0] — 2026-08-24
|
|
34
97
|
### Fixed
|
|
35
98
|
- **A chart's legend, bar values and donut centre were exposed to assistive tech after all** (#047).
|
package/COMPONENTS.md
CHANGED
|
@@ -122,8 +122,21 @@ off-canvas drawer and backdrop are built in.
|
|
|
122
122
|
- Modifiers: `--nav-open` (mobile drawer open, ≤720px) · `--nav-collapsed` (collapse to zero width,
|
|
123
123
|
≥721px) · `--static` (embed the shell in a page instead of filling the viewport)
|
|
124
124
|
- Also: `.fdy-skip` — the skip-to-content link, first child of the shell.
|
|
125
|
-
-
|
|
126
|
-
|
|
125
|
+
- **Behaviour: `freeday-app-shell.js`.** Opt in with `data-fdy-app` on the root — the markup below
|
|
126
|
+
is unchanged. It owns the toggle in both modes plus everything an overlay needs that hand-rolling
|
|
127
|
+
reliably forgets: Escape, backdrop click, closing when a `.fdy-nav__item` is followed, focus moved
|
|
128
|
+
into the panel on open and returned to `__navtoggle` on close, `inert` on `__content` while the
|
|
129
|
+
overlay is up, and a Tab trap inside the panel. `aria-expanded` on `__navtoggle` answers "is the
|
|
130
|
+
nav showing?" in both modes, so you never read the state classes yourself.
|
|
131
|
+
- **A hidden nav is not a tabbable nav.** `__sidebar` is `inert` whenever the nav is not visible —
|
|
132
|
+
collapsed at ≥721px (`width:0`) or off-canvas at ≤720px (`translateX(-100%)`). Both hide it from
|
|
133
|
+
the eye; neither hides it from the keyboard, so without this a nav nobody can see still swallows
|
|
134
|
+
Tab on the way to the page.
|
|
135
|
+
- **Crossing the breakpoint is part of the contract**: open the overlay, then widen the window, and
|
|
136
|
+
`--nav-open` is dropped and `inert` removed. Leaving them set is how a page becomes permanently
|
|
137
|
+
unreachable.
|
|
138
|
+
- Skip the enhancer and nothing breaks: the classes still mean what they always meant and the
|
|
139
|
+
behaviour is yours to write. `FreedayAppShell.init(root)` for markup mounted later.
|
|
127
140
|
|
|
128
141
|
**The nesting is fixed, not free-form:** `.fdy-app` is a flex **row** of `[__sidebar | __content]`,
|
|
129
142
|
and `__content` is the column holding `__topbar` + `__main` — it exists to give the sticky topbar a
|
package/README.id.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.53.0)
|
|
9
9
|
|
|
10
10
|
UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
|
|
11
11
|
spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **More free days for devs — the UI kit is ready to use.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.53.0)
|
|
9
9
|
|
|
10
10
|
A token-driven, framework-agnostic UI kit — one source of truth for color, typography,
|
|
11
11
|
spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* Freeday — app shell enhancer (optional, zero-dependency).
|
|
2
|
+
* Opt in with <div class="fdy-app" data-fdy-app>; the markup is otherwise unchanged.
|
|
3
|
+
*
|
|
4
|
+
* The shell has always shipped the state classes and no behaviour, which left every consumer to
|
|
5
|
+
* assemble the toggle, Escape, focus trap, focus restore and `inert` themselves — and the two
|
|
6
|
+
* hand-rolled copies in this repo's own docs already disagreed about which of those exist. Below
|
|
7
|
+
* 721px the sidebar is an OVERLAY (app-shell.css: position:fixed + translateX(-100%)), so it needs
|
|
8
|
+
* the same treatment a modal drawer gets; at or above 721px it is a column that merely collapses.
|
|
9
|
+
*
|
|
10
|
+
* One rule covers both a hidden nav and a hidden page:
|
|
11
|
+
* - `__sidebar` is inert whenever the nav is NOT visible. Off-canvas and collapsed panels stay in
|
|
12
|
+
* the tab order otherwise — translateX(-100%) and width:0 hide a thing from the eye, not from
|
|
13
|
+
* the keyboard, which is how a nav nobody can see still swallows Tab.
|
|
14
|
+
* - `__content` is inert only while the nav is an OPEN OVERLAY, so Tab cannot wander behind the
|
|
15
|
+
* backdrop.
|
|
16
|
+
*
|
|
17
|
+
* Emits nothing; the classes are the state. FreedayAppShell.init(root) for late-mounted markup.
|
|
18
|
+
*/
|
|
19
|
+
(function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
/* Must match app-shell.css, which switches at (min-width:721px) / (max-width:720px). */
|
|
23
|
+
var WIDE = '(min-width: 721px)';
|
|
24
|
+
|
|
25
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]),'
|
|
26
|
+
+ ' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
27
|
+
|
|
28
|
+
/* getClientRects(), not offsetParent: an overlay sidebar is position:fixed, and a fixed element
|
|
29
|
+
reports no offsetParent at all — filtering on it would call every nav item invisible. */
|
|
30
|
+
function focusables(root) {
|
|
31
|
+
return Array.prototype.filter.call(root.querySelectorAll(FOCUSABLE), function (el) {
|
|
32
|
+
return el.getClientRects().length > 0;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setInert(el, on) {
|
|
37
|
+
if (!el) return;
|
|
38
|
+
if (on) el.setAttribute('inert', '');
|
|
39
|
+
else el.removeAttribute('inert');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function initShell(app) {
|
|
43
|
+
if (app.dataset.fdyAppShellReady === '1') return;
|
|
44
|
+
app.dataset.fdyAppShellReady = '1';
|
|
45
|
+
|
|
46
|
+
var toggle = app.querySelector('.fdy-app__navtoggle');
|
|
47
|
+
var sidebar = app.querySelector('.fdy-app__sidebar');
|
|
48
|
+
if (!toggle || !sidebar) return;
|
|
49
|
+
var content = app.querySelector('.fdy-app__content');
|
|
50
|
+
var backdrop = app.querySelector('.fdy-app__backdrop');
|
|
51
|
+
|
|
52
|
+
var mqWide = window.matchMedia(WIDE);
|
|
53
|
+
var restoreTo = null;
|
|
54
|
+
|
|
55
|
+
function isOverlayOpen() { return app.classList.contains('fdy-app--nav-open'); }
|
|
56
|
+
function isCollapsed() { return app.classList.contains('fdy-app--nav-collapsed'); }
|
|
57
|
+
function navVisible() { return mqWide.matches ? !isCollapsed() : isOverlayOpen(); }
|
|
58
|
+
|
|
59
|
+
/* aria-expanded answers "is the nav showing?" in BOTH modes — the two state classes are the
|
|
60
|
+
kit's business, not the reader's. */
|
|
61
|
+
function sync() {
|
|
62
|
+
var visible = navVisible();
|
|
63
|
+
toggle.setAttribute('aria-expanded', String(visible));
|
|
64
|
+
setInert(sidebar, !visible);
|
|
65
|
+
setInert(content, !mqWide.matches && visible);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function open() {
|
|
69
|
+
if (isOverlayOpen()) return;
|
|
70
|
+
restoreTo = document.activeElement;
|
|
71
|
+
app.classList.add('fdy-app--nav-open');
|
|
72
|
+
sync();
|
|
73
|
+
var first = focusables(sidebar)[0];
|
|
74
|
+
if (first) {
|
|
75
|
+
first.focus();
|
|
76
|
+
} else {
|
|
77
|
+
sidebar.setAttribute('tabindex', '-1');
|
|
78
|
+
sidebar.focus();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* restoreFocus is false when the viewport closed it rather than the user — a resize must not
|
|
83
|
+
yank focus out from under whatever the reader was doing. */
|
|
84
|
+
function close(restoreFocus) {
|
|
85
|
+
if (!isOverlayOpen()) return;
|
|
86
|
+
app.classList.remove('fdy-app--nav-open');
|
|
87
|
+
sync();
|
|
88
|
+
if (restoreFocus === false) return;
|
|
89
|
+
/* Back to the control that opened it. document.body is where a keyboard user gets stranded,
|
|
90
|
+
so anything that is gone, detached or the body itself falls back to the toggle. */
|
|
91
|
+
var target = (restoreTo && restoreTo !== document.body && document.contains(restoreTo))
|
|
92
|
+
? restoreTo : toggle;
|
|
93
|
+
if (typeof target.focus === 'function') target.focus();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
toggle.addEventListener('click', function () {
|
|
97
|
+
if (mqWide.matches) {
|
|
98
|
+
app.classList.toggle('fdy-app--nav-collapsed');
|
|
99
|
+
sync();
|
|
100
|
+
} else if (isOverlayOpen()) {
|
|
101
|
+
close();
|
|
102
|
+
} else {
|
|
103
|
+
open();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (backdrop) backdrop.addEventListener('click', function () { close(); });
|
|
108
|
+
|
|
109
|
+
/* Following a link inside an overlay nav means "take me there" — leaving the panel open over
|
|
110
|
+
the page you just asked for is the one thing every hand-rolled copy in this repo disagreed on. */
|
|
111
|
+
sidebar.addEventListener('click', function (e) {
|
|
112
|
+
if (!isOverlayOpen()) return;
|
|
113
|
+
if (e.target.closest && e.target.closest('.fdy-nav__item')) close();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
document.addEventListener('keydown', function (e) {
|
|
117
|
+
if (!isOverlayOpen()) return;
|
|
118
|
+
if (e.key === 'Escape') {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
close();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (e.key !== 'Tab') return;
|
|
124
|
+
/* The trap. `inert` on the content stops Tab reaching the page, but without this the focus
|
|
125
|
+
would walk out of the document into the browser's own chrome instead of cycling. */
|
|
126
|
+
var items = focusables(sidebar);
|
|
127
|
+
if (items.length === 0) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
var first = items[0], last = items[items.length - 1];
|
|
132
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
last.focus();
|
|
135
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
first.focus();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/* Crossing the breakpoint with the overlay open would otherwise leave --nav-open set and the
|
|
142
|
+
content inert forever: the panel becomes a static column again, and the page it is covering
|
|
143
|
+
can no longer be clicked or read. */
|
|
144
|
+
mqWide.addEventListener('change', function () {
|
|
145
|
+
if (mqWide.matches && isOverlayOpen()) close(false);
|
|
146
|
+
else sync();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
sync();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function initShells(context) {
|
|
153
|
+
var ctx = context || document;
|
|
154
|
+
/* root included: querySelectorAll never matches its own root, and a framework ref often sits ON
|
|
155
|
+
the widget. */
|
|
156
|
+
if (ctx.matches && ctx.matches('[data-fdy-app]')) initShell(ctx);
|
|
157
|
+
Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-app]'), initShell);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (document.readyState === 'loading') {
|
|
161
|
+
document.addEventListener('DOMContentLoaded', function () { initShells(); });
|
|
162
|
+
} else {
|
|
163
|
+
initShells();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
window.FreedayAppShell = { init: initShells, initAll: initShells };
|
|
167
|
+
})();
|
package/dist/freeday.js
CHANGED
|
@@ -1,5 +1,173 @@
|
|
|
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 nothing; the classes are the state. FreedayAppShell.init(root) for late-mounted markup.
|
|
20
|
+
*/
|
|
21
|
+
(function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
/* Must match app-shell.css, which switches at (min-width:721px) / (max-width:720px). */
|
|
25
|
+
var WIDE = '(min-width: 721px)';
|
|
26
|
+
|
|
27
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]),'
|
|
28
|
+
+ ' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
29
|
+
|
|
30
|
+
/* getClientRects(), not offsetParent: an overlay sidebar is position:fixed, and a fixed element
|
|
31
|
+
reports no offsetParent at all — filtering on it would call every nav item invisible. */
|
|
32
|
+
function focusables(root) {
|
|
33
|
+
return Array.prototype.filter.call(root.querySelectorAll(FOCUSABLE), function (el) {
|
|
34
|
+
return el.getClientRects().length > 0;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setInert(el, on) {
|
|
39
|
+
if (!el) return;
|
|
40
|
+
if (on) el.setAttribute('inert', '');
|
|
41
|
+
else el.removeAttribute('inert');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function initShell(app) {
|
|
45
|
+
if (app.dataset.fdyAppShellReady === '1') return;
|
|
46
|
+
app.dataset.fdyAppShellReady = '1';
|
|
47
|
+
|
|
48
|
+
var toggle = app.querySelector('.fdy-app__navtoggle');
|
|
49
|
+
var sidebar = app.querySelector('.fdy-app__sidebar');
|
|
50
|
+
if (!toggle || !sidebar) return;
|
|
51
|
+
var content = app.querySelector('.fdy-app__content');
|
|
52
|
+
var backdrop = app.querySelector('.fdy-app__backdrop');
|
|
53
|
+
|
|
54
|
+
var mqWide = window.matchMedia(WIDE);
|
|
55
|
+
var restoreTo = null;
|
|
56
|
+
|
|
57
|
+
function isOverlayOpen() { return app.classList.contains('fdy-app--nav-open'); }
|
|
58
|
+
function isCollapsed() { return app.classList.contains('fdy-app--nav-collapsed'); }
|
|
59
|
+
function navVisible() { return mqWide.matches ? !isCollapsed() : isOverlayOpen(); }
|
|
60
|
+
|
|
61
|
+
/* aria-expanded answers "is the nav showing?" in BOTH modes — the two state classes are the
|
|
62
|
+
kit's business, not the reader's. */
|
|
63
|
+
function sync() {
|
|
64
|
+
var visible = navVisible();
|
|
65
|
+
toggle.setAttribute('aria-expanded', String(visible));
|
|
66
|
+
setInert(sidebar, !visible);
|
|
67
|
+
setInert(content, !mqWide.matches && visible);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function open() {
|
|
71
|
+
if (isOverlayOpen()) return;
|
|
72
|
+
restoreTo = document.activeElement;
|
|
73
|
+
app.classList.add('fdy-app--nav-open');
|
|
74
|
+
sync();
|
|
75
|
+
var first = focusables(sidebar)[0];
|
|
76
|
+
if (first) {
|
|
77
|
+
first.focus();
|
|
78
|
+
} else {
|
|
79
|
+
sidebar.setAttribute('tabindex', '-1');
|
|
80
|
+
sidebar.focus();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* restoreFocus is false when the viewport closed it rather than the user — a resize must not
|
|
85
|
+
yank focus out from under whatever the reader was doing. */
|
|
86
|
+
function close(restoreFocus) {
|
|
87
|
+
if (!isOverlayOpen()) return;
|
|
88
|
+
app.classList.remove('fdy-app--nav-open');
|
|
89
|
+
sync();
|
|
90
|
+
if (restoreFocus === false) return;
|
|
91
|
+
/* Back to the control that opened it. document.body is where a keyboard user gets stranded,
|
|
92
|
+
so anything that is gone, detached or the body itself falls back to the toggle. */
|
|
93
|
+
var target = (restoreTo && restoreTo !== document.body && document.contains(restoreTo))
|
|
94
|
+
? restoreTo : toggle;
|
|
95
|
+
if (typeof target.focus === 'function') target.focus();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
toggle.addEventListener('click', function () {
|
|
99
|
+
if (mqWide.matches) {
|
|
100
|
+
app.classList.toggle('fdy-app--nav-collapsed');
|
|
101
|
+
sync();
|
|
102
|
+
} else if (isOverlayOpen()) {
|
|
103
|
+
close();
|
|
104
|
+
} else {
|
|
105
|
+
open();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (backdrop) backdrop.addEventListener('click', function () { close(); });
|
|
110
|
+
|
|
111
|
+
/* Following a link inside an overlay nav means "take me there" — leaving the panel open over
|
|
112
|
+
the page you just asked for is the one thing every hand-rolled copy in this repo disagreed on. */
|
|
113
|
+
sidebar.addEventListener('click', function (e) {
|
|
114
|
+
if (!isOverlayOpen()) return;
|
|
115
|
+
if (e.target.closest && e.target.closest('.fdy-nav__item')) close();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
document.addEventListener('keydown', function (e) {
|
|
119
|
+
if (!isOverlayOpen()) return;
|
|
120
|
+
if (e.key === 'Escape') {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
close();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (e.key !== 'Tab') return;
|
|
126
|
+
/* The trap. `inert` on the content stops Tab reaching the page, but without this the focus
|
|
127
|
+
would walk out of the document into the browser's own chrome instead of cycling. */
|
|
128
|
+
var items = focusables(sidebar);
|
|
129
|
+
if (items.length === 0) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
var first = items[0], last = items[items.length - 1];
|
|
134
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
last.focus();
|
|
137
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
first.focus();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/* Crossing the breakpoint with the overlay open would otherwise leave --nav-open set and the
|
|
144
|
+
content inert forever: the panel becomes a static column again, and the page it is covering
|
|
145
|
+
can no longer be clicked or read. */
|
|
146
|
+
mqWide.addEventListener('change', function () {
|
|
147
|
+
if (mqWide.matches && isOverlayOpen()) close(false);
|
|
148
|
+
else sync();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
sync();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function initShells(context) {
|
|
155
|
+
var ctx = context || document;
|
|
156
|
+
/* root included: querySelectorAll never matches its own root, and a framework ref often sits ON
|
|
157
|
+
the widget. */
|
|
158
|
+
if (ctx.matches && ctx.matches('[data-fdy-app]')) initShell(ctx);
|
|
159
|
+
Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-app]'), initShell);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (document.readyState === 'loading') {
|
|
163
|
+
document.addEventListener('DOMContentLoaded', function () { initShells(); });
|
|
164
|
+
} else {
|
|
165
|
+
initShells();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
window.FreedayAppShell = { init: initShells, initAll: initShells };
|
|
169
|
+
})();
|
|
170
|
+
|
|
3
171
|
/* Freeday — autocomplete enhancer (optional, zero-dependency).
|
|
4
172
|
* Editable combobox (WAI-ARIA APG): a text input filters a role="listbox" of role="option"
|
|
5
173
|
* items as you type; select fills the input. Auto-inits [data-fdy-autocomplete]. Options are
|
|
@@ -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
|
|
421
|
-
//
|
|
422
|
-
//
|
|
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.
|
|
3
|
+
"version": "1.53.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 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"
|