@stealthscale/component-a11y 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stealth Scale B.V.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @stealthscale/component-a11y
2
+
3
+ Draws what a keyboard and a screen reader need: words read out and drawn nowhere, a way past the
4
+ navigation, and one tab stop over a set of controls. Every component binds a recipe and draws
5
+ nothing of its own, so a theme restyles all of them by extending the recipe. The preset under
6
+ `./theme` registers the recipes with an application's compiler.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pnpm add @stealthscale/component-a11y
12
+ ```
13
+
14
+ The package peers on `react`, `@stealthscale/hooks` and `@stealthscale/theme`. An application lists
15
+ the preset under `./theme` among the presets its compiler installs.
16
+
17
+ ## VisuallyHidden
18
+
19
+ Exposes its words to a screen reader and draws them nowhere. The words stay in the accessibility
20
+ tree, which `display: none` and `visibility: hidden` both take them out of. A control a keyboard can
21
+ reach is `focusable`, so it comes into view while focus is on it and a sighted reader tabbing
22
+ through the page keeps their place.
23
+
24
+ ```tsx
25
+ import { VisuallyHidden } from "@stealthscale/component-a11y";
26
+
27
+ <button type="button">
28
+ <Icon viewBox="0 0 24 24">…</Icon>
29
+ <VisuallyHidden>Close</VisuallyHidden>
30
+ </button>;
31
+ <VisuallyHidden as="h2">Sections</VisuallyHidden>;
32
+ ```
33
+
34
+ | Axis | Values | Default |
35
+ | ----------- | ------ | ------- |
36
+ | `focusable` | `true` | off |
37
+
38
+ ## SkipNav
39
+
40
+ Carries a keyboard past the navigation, composed as `SkipNav.Link` at the top of the document and
41
+ `SkipNav.Target` where the content starts. The link is hidden until focus reaches it, so a reader
42
+ who tabs meets it first and a reader who never tabs never sees it. The target takes a tab index of
43
+ minus one, because a browser moves focus to a fragment only where the target can hold focus.
44
+
45
+ ```tsx
46
+ import { SkipNav } from "@stealthscale/component-a11y";
47
+
48
+ <SkipNav.Link>Skip to content</SkipNav.Link>;
49
+ <SkipNav.Target as="main">…</SkipNav.Target>;
50
+ ```
51
+
52
+ The link points at `#content` and the target carries that id, so neither states the other. A page
53
+ with more than one landing place names its own: `<SkipNav.Link href="#search">` beside
54
+ `<SkipNav.Target id="search">`.
55
+
56
+ ## RovingFocus
57
+
58
+ Holds one tab stop for a set of controls, composed as `RovingFocus.Root` holding `RovingFocus.Item`.
59
+ A reader reaches the set with Tab and moves inside it with the arrows, which is what the toolbar,
60
+ tablist and menubar patterns ask for. Home and End go to the ends, a disabled item is passed over,
61
+ and the arrows run the other way where the line runs right to left.
62
+
63
+ ```tsx
64
+ import { RovingFocus } from "@stealthscale/component-a11y";
65
+ import { Button } from "@stealthscale/component-actions";
66
+
67
+ <RovingFocus.Root role="toolbar">
68
+ <RovingFocus.Item as={Button} variant="ghost">
69
+ Cut
70
+ </RovingFocus.Item>
71
+ <RovingFocus.Item as={Button} variant="ghost">
72
+ Copy
73
+ </RovingFocus.Item>
74
+ </RovingFocus.Root>;
75
+ ```
76
+
77
+ The root carries no role of its own, because naming a set of controls is the caller's decision. It
78
+ reports the orientation to a screen reader only where the caller gave it a role, since
79
+ `aria-orientation` means nothing on a plain element.
80
+
81
+ | Axis | Values | Default |
82
+ | ------------- | -------------------------------- | ------------ |
83
+ | `orientation` | `horizontal`, `vertical`, `both` | `horizontal` |
84
+
85
+ `wrap` joins the ends together. `activeId` sets which item holds the stop from outside, and
86
+ `onActiveIdChange` reports the stop moving.
87
+
88
+ ## Licence
89
+
90
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ {"$schema":"http://cyclonedx.org/schema/bom-1.7.schema.json","bomFormat":"CycloneDX","specVersion":"1.7","version":1,"metadata":{"lifecycles":[{"phase":"build"}],"tools":{"components":[{"type":"application","name":"rolldown","version":"1.2.7"},{"type":"application","name":"vite","version":"8.2.2"},{"type":"library","name":"core","group":"@arethetypeswrong","version":"0.18.5","bom-ref":"pkg:npm/%40arethetypeswrong/core@0.18.5","author":"Andrew Branch","description":"The API behind arethetypeswrong.github.io","hashes":[{"alg":"SHA-512","content":"f72b63cc6c318e6f54cfb23d6af7dbb79be542deae93db914444b3263aabce797a58abc8e9dc184e8faf277534d96aeafe64778aa97f3f386f1e129d2480230d"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40arethetypeswrong/core@0.18.5","externalReferences":[{"url":"https://github.com/arethetypeswrong/arethetypeswrong.github.io.git#packages/core","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=20"}]},{"type":"library","name":"hooks","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/hooks@0.1.0","description":"The React hooks a component uses to read the page it draws into: how the page is read, what it measures, and what a value was on the previous render.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/hooks@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/config/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/config.git#foundations/hooks","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/config/tree/main/foundations/hooks#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"theme","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/theme@0.3.0","description":"Publishes the foundation every recipe is written against, the runtime every component binds its recipe with, and the vocabulary a theme is written in.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/theme@0.3.0","externalReferences":[{"url":"https://github.com/stealth-scale/config/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/config.git#foundations/theme","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/config/tree/main/foundations/theme#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-react","group":"@stealthscale","version":"0.8.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-react@0.8.0","description":"Adds the JSX transform, the React lint rules, a test document and an MDX compiler to a package that renders.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-react@0.8.0","externalReferences":[{"url":"https://github.com/stealth-scale/config/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/config.git#packages/vite-config-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/config/tree/main/packages/vite-config-react#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-theme","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-theme@0.1.0","description":"Adds the runtime generator to a design-system package and the stylesheet compiler to an application.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-theme@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/config/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/config.git#packages/vite-config-theme","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/config/tree/main/packages/vite-config-theme#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-typescript","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","description":"Sets the TypeScript options every stealthscale package compiles with.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","externalReferences":[{"url":"https://github.com/stealth-scale/config/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/config.git#packages/vite-config-typescript","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/config/tree/main/packages/vite-config-typescript#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config","group":"@stealthscale","version":"0.6.0","bom-ref":"pkg:npm/%40stealthscale/vite-config@0.6.0","description":"Configures a package from five Vite+ tiers and a named layer for every block.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config@0.6.0","externalReferences":[{"url":"https://github.com/stealth-scale/config/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/config.git#packages/vite-config","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/config/tree/main/packages/vite-config#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"react","group":"@testing-library","version":"16.3.3","bom-ref":"pkg:npm/%40testing-library/react@16.3.3","author":"Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)","description":"Simple and complete React DOM testing utilities that encourage good testing practices.","hashes":[{"alg":"SHA-512","content":"528d7ddcd8106cf333ea5aeb86d45040570c0ba45efc42cb15b6ee54bdf4583959c65a597fdfe53074c05713d12f0d6ad62bbd389991d5cd812e210d46cb5d06"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40testing-library/react@16.3.3","externalReferences":[{"url":"https://github.com/testing-library/react-testing-library/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/testing-library/react-testing-library","type":"vcs","comment":"as detected from PackageJson property \"repository.url\""},{"url":"https://github.com/testing-library/react-testing-library#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=18"}]},{"type":"library","name":"react-dom","group":"@types","version":"19.3.0","bom-ref":"pkg:npm/%40types/react-dom@19.3.0","description":"TypeScript definitions for react-dom","hashes":[{"alg":"SHA-512","content":"648edb538da66575ca1e7fea34b130d88adb888354ed7e7ebdf81d8b10479023696165e32a07d0fcffaec866f90a338b07d51c9d37a0debca542d5768729b8e1"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40types/react-dom@19.3.0","externalReferences":[{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/react-dom","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-dom","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"react","group":"@types","version":"19.3.0","bom-ref":"pkg:npm/%40types/react@19.3.0","description":"TypeScript definitions for react","hashes":[{"alg":"SHA-512","content":"374ac50ae1fd628c46f7f9bad65f4c7e924a7e62ce554d1e9bb8a9233e9346049292f45e2c1f6f2fce4607ecabf01b3995eaa9be0f7a252c05e194b5b38be242"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40types/react@19.3.0","externalReferences":[{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"babel-plugin-react-compiler","version":"1.0.0","bom-ref":"pkg:npm/babel-plugin-react-compiler@1.0.0","description":"Babel plugin for React Compiler.","hashes":[{"alg":"SHA-512","content":"2319bcb457e828a20f61d0822984ecaaff85778209d0342a3321229a8fa9c5438c511f5c54f9704eb16df40beefb771be99a77980ce5fadd4cac6d9fc712acc7"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/babel-plugin-react-compiler@1.0.0","externalReferences":[{"url":"git+https://github.com/facebook/react.git#compiler/packages/babel-plugin-react-compiler","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""}]},{"type":"library","name":"happy-dom","version":"20.14.5","bom-ref":"pkg:npm/happy-dom@20.14.5","author":"David Ortner","description":"Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.","hashes":[{"alg":"SHA-512","content":"c7f47392958ee346d38c8a13df4890b6deb814b2e61ff89171408dd97fff6cbc7b1f789f91d7cf5f2aacae8fce62dab32008622cc300ee69a2391f42dcd38a8d"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/happy-dom@20.14.5","externalReferences":[{"url":"https://github.com/capricorn86/happy-dom","type":"vcs","comment":"as detected from PackageJson property \"repository\""},{"url":"https://github.com/capricorn86/happy-dom","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=20.0.0"}]},{"type":"library","name":"oxc-transform-react","version":"0.149.0","bom-ref":"pkg:npm/oxc-transform-react@0.149.0","author":"Boshen and oxc contributors","description":"Oxc React Compiler Node API","hashes":[{"alg":"SHA-512","content":"69bdddc5c3ed90349f211f6d35dc4752d9a974bd258479ddb010fd214014e41c6d26dc061bff39367473b367f62b0a2e3641e5edc692b3030b41e52fc3714447"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/oxc-transform-react@0.149.0","externalReferences":[{"url":"https://github.com/oxc-project/oxc/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/oxc-project/oxc.git#napi/transform-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://oxc.rs","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^20.19.0 || >=22.12.0"}]},{"type":"library","name":"react-dom","version":"19.3.0","bom-ref":"pkg:npm/react-dom@19.3.0","description":"React package for working with the DOM.","hashes":[{"alg":"SHA-512","content":"24393c76089fe753a31680c4ef4f8e4fd202c98afeebd1e59a284dc29d7e36c7db9dadede6c22209af59264b43990e3fd5bfeb9f6ea1200476b985c332be6bd1"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/react-dom@19.3.0","externalReferences":[{"url":"https://github.com/react/react/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/react/react.git#packages/react-dom","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://react.dev/","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"react","version":"19.3.0","bom-ref":"pkg:npm/react@19.3.0","description":"React is a JavaScript library for building user interfaces.","hashes":[{"alg":"SHA-512","content":"13c2d471bb415addb46db9766281dfc7864305dc554df38ab42667f5c0d227897afe7ba8029729201723e3bb76c19a155fc836647b8c1db892860091d53e52a2"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/react@19.3.0","externalReferences":[{"url":"https://github.com/react/react/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"https://github.com/react/react.git#packages/react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://react.dev/","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=0.10.0"}]},{"type":"library","name":"typescript","version":"7.0.2","bom-ref":"pkg:npm/typescript@7.0.2","author":"Microsoft Corp.","description":"TypeScript is a language for application scale JavaScript development","hashes":[{"alg":"SHA-512","content":"f0561abbdea8dcd28e85b8ca8bfa8dbc6fd6e63873c646dd9b9b23f406d9ff94f9b16aa7de12602df1b1dbbb11299593bf2cc23fc74b4414dfe6d05349155434"}],"licenses":[{"license":{"id":"Apache-2.0"}}],"purl":"pkg:npm/typescript@7.0.2","externalReferences":[{"url":"https://github.com/microsoft/TypeScript/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/microsoft/TypeScript.git","type":"vcs","comment":"as detected from PackageJson property \"repository.url\""},{"url":"https://www.typescriptlang.org/","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=16.20.0"}]},{"type":"library","name":"vitest","version":"4.1.11","bom-ref":"pkg:npm/vitest@4.1.11","author":"Anthony Fu <anthonyfu117@hotmail.com>","description":"Next generation testing framework powered by Vite","hashes":[{"alg":"SHA-512","content":"7e1002acd5d489d21b19206be4595bb8190eed5582d59c8b9740cee025360eb42800fc57f3862cc6cf877864298a2e656565754388016534eee3d985f80e8477"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/vitest@4.1.11","externalReferences":[{"url":"https://github.com/vitest-dev/vitest/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"git+https://github.com/vitest-dev/vitest.git#packages/vitest","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://vitest.dev","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^20.0.0 || ^22.0.0 || >=24.0.0"}]}]},"component":{"type":"library","name":"component-a11y","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/component-a11y@0.1.0","description":"Draws what a keyboard and a screen reader need, and an eye does not.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/component-a11y@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/config/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/config.git#components/a11y","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/config/tree/main/components/a11y#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}],"evidence":{"licenses":[{"license":{"name":"file: LICENSE","text":{"content":"TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAyNiBTdGVhbHRoIFNjYWxlIEIuVi4KClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUuCg==","contentType":"text/plain","encoding":"base64"}}}]}},"supplier":{"name":"Stealth Scale B.V.","url":["https://stealthscale.io"]}},"components":[],"dependencies":[{"ref":"pkg:npm/%40stealthscale/component-a11y@0.1.0"}]}
@@ -0,0 +1,141 @@
1
+ import { ComponentProps, ReactElement, ReactNode, Ref } from "react";
2
+ //#region src/roving-focus/item.d.ts
3
+ /**
4
+ * Draws the element an item is laid out on.
5
+ */
6
+ declare const Shell$1: import("@stealthscale/theme").StyledComponent<"div", {}>;
7
+ /**
8
+ * Describes what an item takes beside everything a styled element takes.
9
+ */
10
+ interface ItemProps extends ComponentProps<typeof Shell$1> {
11
+ /**
12
+ * Whether the arrows pass over the item.
13
+ */
14
+ disabled?: boolean | undefined;
15
+ /**
16
+ * The id the item answers to, which the group generates where a caller states none.
17
+ */
18
+ id?: string | undefined;
19
+ /**
20
+ * Where a caller wants the element.
21
+ */
22
+ ref?: Ref<HTMLDivElement> | undefined;
23
+ }
24
+ /**
25
+ * Takes the tab stop when the arrows reach it, and leaves the tab order otherwise.
26
+ *
27
+ * @throws {@link Error} When it is drawn outside a group.
28
+ */
29
+ declare function Item(props: ItemProps): ReactElement;
30
+ //#endregion
31
+ //#region src/roving-focus/use-roving-focus.d.ts
32
+ /**
33
+ * Selects which arrows move focus through the group.
34
+ */
35
+ type Orientation = "both" | "horizontal" | "vertical";
36
+ //#endregion
37
+ //#region src/roving-focus/root.d.ts
38
+ /**
39
+ * Draws the element the group is laid out on.
40
+ */
41
+ declare const Shell: import("@stealthscale/theme").StyledComponent<"div", {
42
+ readonly orientation?: "both" | "horizontal" | "vertical";
43
+ }>;
44
+ /**
45
+ * Describes what a group takes beside the variants its recipe offers.
46
+ */
47
+ interface RootProps extends Omit<ComponentProps<typeof Shell>, "orientation"> {
48
+ /**
49
+ * Which item holds the tab stop, where something above decides it.
50
+ */
51
+ activeId?: string | undefined;
52
+ /**
53
+ * The items the group lays out.
54
+ */
55
+ children?: ReactNode | undefined;
56
+ /**
57
+ * Which item Tab first enters, where the group decides for itself.
58
+ */
59
+ defaultActiveId?: string | undefined;
60
+ /**
61
+ * Hears that the tab stop moved.
62
+ */
63
+ onActiveIdChange?: ((activeId: string | undefined) => void) | undefined;
64
+ /**
65
+ * Which arrows move focus, which is along the line unless a caller says otherwise.
66
+ */
67
+ orientation?: Orientation | undefined;
68
+ /**
69
+ * Whether the ends join up.
70
+ */
71
+ wrap?: boolean | undefined;
72
+ }
73
+ /**
74
+ * Keeps one tab stop for the items inside it, and moves it under the arrow keys.
75
+ */
76
+ declare function Root(props: RootProps): ReactElement;
77
+ declare namespace index_d_exports {
78
+ export { Item, ItemProps, Orientation, Root, RootProps };
79
+ }
80
+ //#endregion
81
+ //#region src/skip-nav/link.d.ts
82
+ /**
83
+ * The fragment the link points at and the target answers to, where neither states one.
84
+ */
85
+ declare const SKIP_NAV_TARGET = "content";
86
+ /**
87
+ * Jumps a keyboard past the navigation to the page's content.
88
+ */
89
+ declare const Link: import("@stealthscale/theme").StyledComponent<"a", {}>;
90
+ /**
91
+ * Describes what the link takes: everything a styled anchor element takes.
92
+ */
93
+ type LinkProps = ComponentProps<typeof Link>;
94
+ //#endregion
95
+ //#region src/skip-nav/target.d.ts
96
+ /**
97
+ * Takes the focus the skip link sends, and draws nothing.
98
+ */
99
+ declare const Target: import("@stealthscale/theme").StyledComponent<"div", {}>;
100
+ /**
101
+ * Describes what the target takes: everything a styled div element takes.
102
+ */
103
+ type TargetProps = ComponentProps<typeof Target>;
104
+ declare namespace index_d_exports$1 {
105
+ export { Link, LinkProps, SKIP_NAV_TARGET, Target, TargetProps };
106
+ }
107
+ //#endregion
108
+ //#region src/visually-hidden/context.d.ts
109
+ /**
110
+ * Binds the hidden text's recipe to the element that draws it.
111
+ *
112
+ * @remarks
113
+ * Apart from the recipe, because an application's compiler reads the recipe at build time and
114
+ * the binding needs the runtime. A recipe file that also bound one would put the runtime behind
115
+ * every compiler configuration that reads it.
116
+ */
117
+ /**
118
+ * Binds the recipe once, for the hidden text and for whatever sets its variants from above.
119
+ */
120
+ declare const PropsProvider: import("react").Provider<import("@stealthscale/theme").DataAttrs & Partial<{
121
+ readonly focusable?: boolean;
122
+ }>>, withContext: <Tag extends import("react").ElementType>(Component: Tag, options?: import("@stealthscale/theme").JsxFactoryOptions<import("@stealthscale/theme").ComponentProps<Tag> & Partial<{
123
+ readonly focusable?: boolean;
124
+ }>> | undefined) => import("@stealthscale/theme").StyledComponent<Tag, {
125
+ readonly focusable?: boolean;
126
+ }>;
127
+ //#endregion
128
+ //#region src/visually-hidden/visually-hidden.d.ts
129
+ /**
130
+ * Reads its words to a screen reader and draws them nowhere.
131
+ */
132
+ export declare const VisuallyHidden: import("@stealthscale/theme").StyledComponent<"span", {
133
+ readonly focusable?: boolean;
134
+ }>;
135
+ /**
136
+ * Describes what hidden text takes: the variants its recipe offers, and everything a styled span
137
+ * element takes.
138
+ */
139
+ type VisuallyHiddenProps = ComponentProps<typeof VisuallyHidden>;
140
+ //#endregion
141
+ export { index_d_exports as RovingFocus, index_d_exports$1 as SkipNav, type VisuallyHiddenProps, PropsProvider as VisuallyHiddenPropsProvider };
package/dist/index.js ADDED
@@ -0,0 +1,598 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { n as recipe$1, r as recipe, t as recipe$2 } from "./recipe-BKojQp6r.js";
3
+ import { c } from "react/compiler-runtime";
4
+ import { createContext, use, useEffect, useId, useRef, useState } from "react";
5
+ import { useCallbackRef, useControllableState } from "@stealthscale/hooks";
6
+ import { createRecipeContext, createSlotRecipeContext } from "@stealthscale/theme";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/roving-focus/context.ts
9
+ /**
10
+ * Binds the group's recipe to the parts that draw it.
11
+ *
12
+ * @remarks
13
+ * Apart from the recipe, because an application's compiler reads the recipe at build time and
14
+ * the binding needs the runtime. The root carries the orientation, and every item below it reads
15
+ * its classes from the root through a context.
16
+ */
17
+ /**
18
+ * Binds the recipe once: the root takes the orientation, and each item draws its slot in it.
19
+ */
20
+ const { withContext: withContext$1, withProvider: withProvider$1 } = createSlotRecipeContext(recipe);
21
+ //#endregion
22
+ //#region src/roving-focus/use-roving-focus.ts
23
+ /**
24
+ * Moves one tab stop through a set of controls, so a reader reaches the set with Tab and moves
25
+ * inside it with the arrows.
26
+ *
27
+ * @remarks
28
+ * A toolbar, a row of tabs and a menu bar are all one stop in the tab order, and the arrows move
29
+ * which control that stop is on. The items register themselves rather than being counted by the
30
+ * root, because a root cannot see through whatever composes its children. Registration order is
31
+ * the order the effects ran, which is source order until something is conditional, so the list
32
+ * is sorted by where the items sit in the document: what the arrows follow is what the reader
33
+ * sees.
34
+ */
35
+ /**
36
+ * Maps the arrows that run down the page to the step each takes.
37
+ */
38
+ const DOWN_THE_PAGE = {
39
+ ArrowDown: 1,
40
+ ArrowUp: -1
41
+ };
42
+ /**
43
+ * Maps the arrows that run along a line to the step each takes.
44
+ */
45
+ const ALONG_THE_LINE = {
46
+ ArrowLeft: -1,
47
+ ArrowRight: 1
48
+ };
49
+ /**
50
+ * Carries the group down to its items.
51
+ */
52
+ const RovingFocusContext = createContext(void 0);
53
+ /**
54
+ * Orders two items by where they sit in the document.
55
+ */
56
+ function byDocumentPosition(first, second) {
57
+ return (first.element.compareDocumentPosition(second.element) & Node.DOCUMENT_POSITION_FOLLOWING) === 0 ? 1 : -1;
58
+ }
59
+ /**
60
+ * Reads what a key asks for, given the arrows the group binds and which way the line runs.
61
+ *
62
+ * @param key - The key pressed.
63
+ * @param orientation - Which arrows the group moves on.
64
+ * @param forward - One where the line runs left to right, minus one where it runs the other way.
65
+ * @returns The step or the edge to move to, or nothing where the group does not claim the key.
66
+ */
67
+ function intentOf(key, orientation, forward) {
68
+ if (key === "Home") return "start";
69
+ if (key === "End") return "end";
70
+ const block = DOWN_THE_PAGE[key];
71
+ if (block !== void 0) return orientation === "horizontal" ? void 0 : block;
72
+ const inline = ALONG_THE_LINE[key];
73
+ if (inline !== void 0) return orientation === "vertical" ? void 0 : inline * forward;
74
+ }
75
+ /**
76
+ * Marks out the one item the group moves to, as a slice of the list it is in.
77
+ *
78
+ * @remarks
79
+ * A slice rather than an index, because the list is read at an index that is always inside it
80
+ * and a lookup would still be typed as possibly absent. A slice of one is the item, and a slice
81
+ * of an empty list is empty, so the caller writes no test for a case that cannot arise.
82
+ * @param list - The items, in the order the reader sees them.
83
+ * @param intent - The edge or the step the key asked for.
84
+ * @param from - The item that holds the tab stop.
85
+ * @param wrap - Whether the ends join up.
86
+ * @returns The first and last index of the slice holding the item to move to.
87
+ */
88
+ function spanOf(list, intent, from, wrap) {
89
+ const last = list.length - 1;
90
+ const at = list.findIndex((item) => item.id === from);
91
+ const here = at === -1 ? 0 : at;
92
+ const to = intent === "start" ? 0 : intent === "end" ? last : wrap ? (here + intent + list.length) % list.length : Math.min(last, Math.max(0, here + intent));
93
+ return [to, to + 1];
94
+ }
95
+ /**
96
+ * Keeps the items in a ref rather than in state.
97
+ *
98
+ * @remarks
99
+ * Nothing drawn depends on the list, only on which item holds the stop, so putting it in state
100
+ * would draw the whole group again every time one item mounted, for a change nobody could see.
101
+ */
102
+ function useRegistry() {
103
+ const $ = c(6);
104
+ if ($[0] !== "6879b11696c2d2a5e563183941bb99f80ab3ad4b04bbc8a7ef4c43a80f829e8d") {
105
+ for (let $i = 0; $i < 6; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
106
+ $[0] = "6879b11696c2d2a5e563183941bb99f80ab3ad4b04bbc8a7ef4c43a80f829e8d";
107
+ }
108
+ let t0;
109
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
110
+ t0 = [];
111
+ $[1] = t0;
112
+ } else t0 = $[1];
113
+ const items = useRef(t0);
114
+ let t1;
115
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
116
+ t1 = (registration) => {
117
+ items.current = [...items.current, registration];
118
+ };
119
+ $[2] = t1;
120
+ } else t1 = $[2];
121
+ const add = t1;
122
+ let t2;
123
+ if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
124
+ t2 = (node) => items.current.some((item) => item.element.contains(node));
125
+ $[3] = t2;
126
+ } else t2 = $[3];
127
+ const holds = t2;
128
+ let t3;
129
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
130
+ t3 = () => items.current.toSorted(byDocumentPosition);
131
+ $[4] = t3;
132
+ } else t3 = $[4];
133
+ const ordered = t3;
134
+ let t4;
135
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
136
+ const remove = (id) => {
137
+ items.current = items.current.filter((item_0) => item_0.id !== id);
138
+ };
139
+ t4 = {
140
+ add,
141
+ holds,
142
+ ordered,
143
+ remove
144
+ };
145
+ $[5] = t4;
146
+ } else t4 = $[5];
147
+ return t4;
148
+ }
149
+ /**
150
+ * Keeps which item holds the tab stop, driven from above where a caller drives it.
151
+ */
152
+ function useTabStop(props) {
153
+ const $ = c(13);
154
+ if ($[0] !== "6879b11696c2d2a5e563183941bb99f80ab3ad4b04bbc8a7ef4c43a80f829e8d") {
155
+ for (let $i = 0; $i < 13; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
156
+ $[0] = "6879b11696c2d2a5e563183941bb99f80ab3ad4b04bbc8a7ef4c43a80f829e8d";
157
+ }
158
+ const { activeId: driven, defaultActiveId, onActiveIdChange } = props;
159
+ let t0;
160
+ if ($[1] !== defaultActiveId || $[2] !== driven || $[3] !== onActiveIdChange) {
161
+ t0 = {
162
+ defaultValue: defaultActiveId,
163
+ onChange: onActiveIdChange,
164
+ value: driven
165
+ };
166
+ $[1] = defaultActiveId;
167
+ $[2] = driven;
168
+ $[3] = onActiveIdChange;
169
+ $[4] = t0;
170
+ } else t0 = $[4];
171
+ const [activeId, setActiveId] = useControllableState(t0);
172
+ const stop = useRef(activeId);
173
+ let t1;
174
+ let t2;
175
+ if ($[5] !== activeId) {
176
+ t1 = () => {
177
+ stop.current = activeId;
178
+ };
179
+ t2 = [activeId];
180
+ $[5] = activeId;
181
+ $[6] = t1;
182
+ $[7] = t2;
183
+ } else {
184
+ t1 = $[6];
185
+ t2 = $[7];
186
+ }
187
+ useEffect(t1, t2);
188
+ let t3;
189
+ if ($[8] !== setActiveId) {
190
+ t3 = (next) => {
191
+ stop.current = next;
192
+ setActiveId(next);
193
+ };
194
+ $[8] = setActiveId;
195
+ $[9] = t3;
196
+ } else t3 = $[9];
197
+ const claim = useCallbackRef(t3);
198
+ let t4;
199
+ if ($[10] !== activeId || $[11] !== claim) {
200
+ t4 = {
201
+ activeId,
202
+ claim,
203
+ stop
204
+ };
205
+ $[10] = activeId;
206
+ $[11] = claim;
207
+ $[12] = t4;
208
+ } else t4 = $[12];
209
+ return t4;
210
+ }
211
+ /**
212
+ * Moves one tab stop through the items that register with it.
213
+ *
214
+ * @remarks
215
+ * The first item to register takes the stop, so a group is reachable by Tab from its first
216
+ * render. An item that leaves while it holds the stop hands it to whatever is first, so the
217
+ * group never drops out of the tab order.
218
+ * @returns The group the items read, and the handler the root binds.
219
+ */
220
+ function useRovingFocus(props) {
221
+ const $ = c(23);
222
+ if ($[0] !== "6879b11696c2d2a5e563183941bb99f80ab3ad4b04bbc8a7ef4c43a80f829e8d") {
223
+ for (let $i = 0; $i < 23; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
224
+ $[0] = "6879b11696c2d2a5e563183941bb99f80ab3ad4b04bbc8a7ef4c43a80f829e8d";
225
+ }
226
+ const { orientation, wrap } = props;
227
+ const { add, holds, ordered, remove } = useRegistry();
228
+ const { activeId, claim, stop } = useTabStop(props);
229
+ let t0;
230
+ if ($[1] !== add || $[2] !== claim || $[3] !== ordered || $[4] !== remove || $[5] !== stop.current) {
231
+ t0 = (registration) => {
232
+ add(registration);
233
+ if (stop.current === void 0) claim(registration.id);
234
+ return () => {
235
+ remove(registration.id);
236
+ if (stop.current === registration.id) claim(ordered()[0]?.id);
237
+ };
238
+ };
239
+ $[1] = add;
240
+ $[2] = claim;
241
+ $[3] = ordered;
242
+ $[4] = remove;
243
+ $[5] = stop.current;
244
+ $[6] = t0;
245
+ } else t0 = $[6];
246
+ const register = useCallbackRef(t0);
247
+ let t1;
248
+ if ($[7] !== claim || $[8] !== holds || $[9] !== ordered || $[10] !== orientation || $[11] !== stop.current || $[12] !== wrap) {
249
+ t1 = (event) => {
250
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
251
+ const { target } = event;
252
+ if (!(target instanceof Node) || !holds(target)) return;
253
+ const rightToLeft = globalThis.getComputedStyle(event.currentTarget).direction === "rtl";
254
+ const intent = intentOf(event.key, orientation, rightToLeft ? -1 : 1);
255
+ if (intent === void 0) return;
256
+ for (const moving of ordered().slice(...spanOf(ordered(), intent, stop.current, wrap))) {
257
+ claim(moving.id);
258
+ moving.element.focus();
259
+ }
260
+ event.preventDefault();
261
+ };
262
+ $[7] = claim;
263
+ $[8] = holds;
264
+ $[9] = ordered;
265
+ $[10] = orientation;
266
+ $[11] = stop.current;
267
+ $[12] = wrap;
268
+ $[13] = t1;
269
+ } else t1 = $[13];
270
+ const onKeyDown = useCallbackRef(t1);
271
+ let t2;
272
+ if ($[14] !== claim) {
273
+ t2 = (id) => {
274
+ claim(id);
275
+ };
276
+ $[14] = claim;
277
+ $[15] = t2;
278
+ } else t2 = $[15];
279
+ const onFocus = useCallbackRef(t2);
280
+ let t3;
281
+ if ($[16] !== activeId || $[17] !== onFocus || $[18] !== register) {
282
+ t3 = {
283
+ activeId,
284
+ onFocus,
285
+ register
286
+ };
287
+ $[16] = activeId;
288
+ $[17] = onFocus;
289
+ $[18] = register;
290
+ $[19] = t3;
291
+ } else t3 = $[19];
292
+ const t4 = t3;
293
+ let t5;
294
+ if ($[20] !== onKeyDown || $[21] !== t4) {
295
+ t5 = {
296
+ group: t4,
297
+ onKeyDown
298
+ };
299
+ $[20] = onKeyDown;
300
+ $[21] = t4;
301
+ $[22] = t5;
302
+ } else t5 = $[22];
303
+ return t5;
304
+ }
305
+ //#endregion
306
+ //#region src/roving-focus/item.tsx
307
+ /**
308
+ * Draws one item of the group, which holds the tab stop when the arrows reach it.
309
+ *
310
+ * @remarks
311
+ * Exactly one item is reachable by Tab and every other is at minus one, which is what makes the
312
+ * whole group one stop in the tab order. A disabled item registers with nothing, so the arrows
313
+ * pass over it, and it does not claim the stop when a pointer focuses it: an item that claimed
314
+ * the stop without registering would leave the stop pointing at nothing and take the group out
315
+ * of the tab order. The element is kept in state rather than in a ref, because the effect that
316
+ * registers it has to run again once the element is there, and a ref changing starts no effect.
317
+ */
318
+ /**
319
+ * Draws the element an item is laid out on.
320
+ */
321
+ const Shell$1 = withContext$1("div", "item");
322
+ /**
323
+ * Takes the tab stop when the arrows reach it, and leaves the tab order otherwise.
324
+ *
325
+ * @throws {@link Error} When it is drawn outside a group.
326
+ */
327
+ function Item(props) {
328
+ const $ = c(27);
329
+ if ($[0] !== "906ba9f383c3499089f80a6d094880677f9a2e49a204ba65b1f93dfa19d6065b") {
330
+ for (let $i = 0; $i < 27; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
331
+ $[0] = "906ba9f383c3499089f80a6d094880677f9a2e49a204ba65b1f93dfa19d6065b";
332
+ }
333
+ let handed;
334
+ let named;
335
+ let rest;
336
+ let t0;
337
+ if ($[1] !== props) {
338
+ ({disabled: t0, id: named, ref: handed, ...rest} = props);
339
+ $[1] = props;
340
+ $[2] = handed;
341
+ $[3] = named;
342
+ $[4] = rest;
343
+ $[5] = t0;
344
+ } else {
345
+ handed = $[2];
346
+ named = $[3];
347
+ rest = $[4];
348
+ t0 = $[5];
349
+ }
350
+ const disabled = t0 === void 0 ? false : t0;
351
+ const group = use(RovingFocusContext);
352
+ if (group === void 0) throw new Error("RovingFocus.Item is drawn inside RovingFocus.Root and nowhere else.");
353
+ const generated = useId();
354
+ const id = named ?? generated;
355
+ const { activeId, onFocus, register } = group;
356
+ const [element, setElement] = useState(null);
357
+ let t1;
358
+ if ($[6] !== handed) {
359
+ t1 = (node) => {
360
+ setElement(node);
361
+ if (typeof handed === "function") handed(node);
362
+ else if (handed !== null && handed !== void 0) handed.current = node;
363
+ };
364
+ $[6] = handed;
365
+ $[7] = t1;
366
+ } else t1 = $[7];
367
+ const attach = useCallbackRef(t1);
368
+ let t2;
369
+ if ($[8] !== disabled || $[9] !== id || $[10] !== onFocus) {
370
+ t2 = () => {
371
+ if (!disabled) onFocus(id);
372
+ };
373
+ $[8] = disabled;
374
+ $[9] = id;
375
+ $[10] = onFocus;
376
+ $[11] = t2;
377
+ } else t2 = $[11];
378
+ const claim = useCallbackRef(t2);
379
+ let t3;
380
+ let t4;
381
+ if ($[12] !== disabled || $[13] !== element || $[14] !== id || $[15] !== register) {
382
+ t3 = () => {
383
+ if (element === null || disabled) return;
384
+ return register({
385
+ element,
386
+ id
387
+ });
388
+ };
389
+ t4 = [
390
+ disabled,
391
+ element,
392
+ id,
393
+ register
394
+ ];
395
+ $[12] = disabled;
396
+ $[13] = element;
397
+ $[14] = id;
398
+ $[15] = register;
399
+ $[16] = t3;
400
+ $[17] = t4;
401
+ } else {
402
+ t3 = $[16];
403
+ t4 = $[17];
404
+ }
405
+ useEffect(t3, t4);
406
+ const t5 = disabled || void 0;
407
+ const t6 = activeId === id ? "" : void 0;
408
+ const t7 = disabled ? "" : void 0;
409
+ const t8 = !disabled && activeId === id ? 0 : -1;
410
+ let t9;
411
+ if ($[18] !== attach || $[19] !== claim || $[20] !== id || $[21] !== rest || $[22] !== t5 || $[23] !== t6 || $[24] !== t7 || $[25] !== t8) {
412
+ t9 = /* @__PURE__ */ jsx(Shell$1, {
413
+ "aria-disabled": t5,
414
+ "data-active": t6,
415
+ "data-disabled": t7,
416
+ id,
417
+ onFocus: claim,
418
+ ref: attach,
419
+ tabIndex: t8,
420
+ ...rest
421
+ });
422
+ $[18] = attach;
423
+ $[19] = claim;
424
+ $[20] = id;
425
+ $[21] = rest;
426
+ $[22] = t5;
427
+ $[23] = t6;
428
+ $[24] = t7;
429
+ $[25] = t8;
430
+ $[26] = t9;
431
+ } else t9 = $[26];
432
+ return t9;
433
+ }
434
+ //#endregion
435
+ //#region src/roving-focus/root.tsx
436
+ /**
437
+ * Draws the element the group is laid out on.
438
+ */
439
+ const Shell = withProvider$1("div", "root");
440
+ /**
441
+ * Keeps one tab stop for the items inside it, and moves it under the arrow keys.
442
+ */
443
+ function Root(props) {
444
+ const $ = c(27);
445
+ if ($[0] !== "16a674b835753bf9fd806b5c17b8426cf42995a3c3e2732778987a1e4a0700e7") {
446
+ for (let $i = 0; $i < 27; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
447
+ $[0] = "16a674b835753bf9fd806b5c17b8426cf42995a3c3e2732778987a1e4a0700e7";
448
+ }
449
+ let activeId;
450
+ let children;
451
+ let defaultActiveId;
452
+ let onActiveIdChange;
453
+ let rest;
454
+ let t0;
455
+ let t1;
456
+ if ($[1] !== props) {
457
+ ({activeId, children, defaultActiveId, onActiveIdChange, orientation: t0, wrap: t1, ...rest} = props);
458
+ $[1] = props;
459
+ $[2] = activeId;
460
+ $[3] = children;
461
+ $[4] = defaultActiveId;
462
+ $[5] = onActiveIdChange;
463
+ $[6] = rest;
464
+ $[7] = t0;
465
+ $[8] = t1;
466
+ } else {
467
+ activeId = $[2];
468
+ children = $[3];
469
+ defaultActiveId = $[4];
470
+ onActiveIdChange = $[5];
471
+ rest = $[6];
472
+ t0 = $[7];
473
+ t1 = $[8];
474
+ }
475
+ const orientation = t0 === void 0 ? "horizontal" : t0;
476
+ const wrap = t1 === void 0 ? false : t1;
477
+ let t2;
478
+ if ($[9] !== activeId || $[10] !== defaultActiveId || $[11] !== onActiveIdChange || $[12] !== orientation || $[13] !== wrap) {
479
+ t2 = {
480
+ activeId,
481
+ defaultActiveId,
482
+ onActiveIdChange,
483
+ orientation,
484
+ wrap
485
+ };
486
+ $[9] = activeId;
487
+ $[10] = defaultActiveId;
488
+ $[11] = onActiveIdChange;
489
+ $[12] = orientation;
490
+ $[13] = wrap;
491
+ $[14] = t2;
492
+ } else t2 = $[14];
493
+ const { group, onKeyDown } = useRovingFocus(t2);
494
+ let t3;
495
+ if ($[15] !== orientation || $[16] !== rest.role) {
496
+ t3 = rest.role !== void 0 && orientation !== "both" ? { "aria-orientation": orientation } : void 0;
497
+ $[15] = orientation;
498
+ $[16] = rest.role;
499
+ $[17] = t3;
500
+ } else t3 = $[17];
501
+ const told = t3;
502
+ let t4;
503
+ if ($[18] !== children || $[19] !== onKeyDown || $[20] !== orientation || $[21] !== rest || $[22] !== told) {
504
+ t4 = /* @__PURE__ */ jsx(Shell, {
505
+ onKeyDown,
506
+ orientation,
507
+ ...told,
508
+ ...rest,
509
+ children
510
+ });
511
+ $[18] = children;
512
+ $[19] = onKeyDown;
513
+ $[20] = orientation;
514
+ $[21] = rest;
515
+ $[22] = told;
516
+ $[23] = t4;
517
+ } else t4 = $[23];
518
+ let t5;
519
+ if ($[24] !== group || $[25] !== t4) {
520
+ t5 = /* @__PURE__ */ jsx(RovingFocusContext, {
521
+ value: group,
522
+ children: t4
523
+ });
524
+ $[24] = group;
525
+ $[25] = t4;
526
+ $[26] = t5;
527
+ } else t5 = $[26];
528
+ return t5;
529
+ }
530
+ //#endregion
531
+ //#region src/roving-focus/index.ts
532
+ var roving_focus_exports = /* @__PURE__ */ __exportAll({
533
+ Item: () => Item,
534
+ Root: () => Root
535
+ });
536
+ //#endregion
537
+ //#region src/skip-nav/context.ts
538
+ /**
539
+ * Binds the skip link's recipe to the parts that draw it.
540
+ *
541
+ * @remarks
542
+ * Apart from the recipe, because an application's compiler reads the recipe at build time and
543
+ * the binding needs the runtime. Neither part takes a variant, so each is bound on its own
544
+ * rather than one providing for the other: a page puts the link at the top and the target
545
+ * further down, and the two are never nested.
546
+ */
547
+ /**
548
+ * Binds the recipe once, for the link and for the target it jumps to.
549
+ */
550
+ const { withProvider } = createSlotRecipeContext(recipe$1);
551
+ //#endregion
552
+ //#region src/skip-nav/link.ts
553
+ /**
554
+ * The fragment the link points at and the target answers to, where neither states one.
555
+ */
556
+ const SKIP_NAV_TARGET = "content";
557
+ /**
558
+ * Jumps a keyboard past the navigation to the page's content.
559
+ */
560
+ const Link = withProvider("a", "link", { defaultProps: { href: `#${SKIP_NAV_TARGET}` } });
561
+ //#endregion
562
+ //#region src/skip-nav/target.ts
563
+ /**
564
+ * Takes the focus the skip link sends, and draws nothing.
565
+ */
566
+ const Target = withProvider("div", "target", { defaultProps: {
567
+ id: SKIP_NAV_TARGET,
568
+ tabIndex: -1
569
+ } });
570
+ //#endregion
571
+ //#region src/skip-nav/index.ts
572
+ var skip_nav_exports = /* @__PURE__ */ __exportAll({
573
+ Link: () => Link,
574
+ SKIP_NAV_TARGET: () => SKIP_NAV_TARGET,
575
+ Target: () => Target
576
+ });
577
+ //#endregion
578
+ //#region src/visually-hidden/context.ts
579
+ /**
580
+ * Binds the hidden text's recipe to the element that draws it.
581
+ *
582
+ * @remarks
583
+ * Apart from the recipe, because an application's compiler reads the recipe at build time and
584
+ * the binding needs the runtime. A recipe file that also bound one would put the runtime behind
585
+ * every compiler configuration that reads it.
586
+ */
587
+ /**
588
+ * Binds the recipe once, for the hidden text and for whatever sets its variants from above.
589
+ */
590
+ const { PropsProvider, withContext } = createRecipeContext(recipe$2);
591
+ //#endregion
592
+ //#region src/visually-hidden/visually-hidden.ts
593
+ /**
594
+ * Reads its words to a screen reader and draws them nowhere.
595
+ */
596
+ const VisuallyHidden = withContext("span");
597
+ //#endregion
598
+ export { roving_focus_exports as RovingFocus, skip_nav_exports as SkipNav, VisuallyHidden, PropsProvider as VisuallyHiddenPropsProvider };
@@ -0,0 +1,106 @@
1
+ import { defineRecipe, defineSlotRecipe } from "@stealthscale/theme/authoring";
2
+ //#region src/roving-focus/recipe.ts
3
+ /**
4
+ * States what a set of controls under one tab stop is: a row or a column of items, laid out the
5
+ * way the arrows move through them.
6
+ *
7
+ * @remarks
8
+ * The orientation is one axis because it decides two things that have to agree: which arrows
9
+ * move focus, which the root reads, and which way the items run, which the recipe draws. A group
10
+ * the arrows move through in both directions wraps, because a set a reader moves through on two
11
+ * axes is a set that runs onto a second line.
12
+ */
13
+ /**
14
+ * Lays the items out in a row the arrows run along until a caller says otherwise.
15
+ */
16
+ const recipe$2 = defineSlotRecipe({
17
+ base: {
18
+ item: { minInlineSize: "0" },
19
+ root: { display: "flex" }
20
+ },
21
+ className: "roving-focus",
22
+ defaultVariants: { orientation: "horizontal" },
23
+ jsx: [/^RovingFocus(\.\w+)?$/u],
24
+ slots: ["root", "item"],
25
+ variants: { orientation: {
26
+ both: { root: {
27
+ flexDirection: "row",
28
+ flexWrap: "wrap"
29
+ } },
30
+ horizontal: { root: { flexDirection: "row" } },
31
+ vertical: { root: { flexDirection: "column" } }
32
+ } }
33
+ });
34
+ //#endregion
35
+ //#region src/skip-nav/recipe.ts
36
+ /**
37
+ * States what a way past the navigation is: a link a keyboard reaches first and an eye sees only
38
+ * while focus is on it, and the place on the page it jumps to.
39
+ *
40
+ * @remarks
41
+ * The link is hidden the way the hidden text is hidden, and comes into view under focus, so a
42
+ * reader tabbing into the page meets it and a reader who never tabs never sees it. The target
43
+ * states nothing but its own class, because a page decides what it looks like and the target's
44
+ * whole job is to be somewhere focus can land.
45
+ */
46
+ /**
47
+ * Draws the link over the top of the page while focus is on it, and leaves the target alone.
48
+ */
49
+ const recipe$1 = defineSlotRecipe({
50
+ base: {
51
+ link: {
52
+ _focusVisible: {
53
+ borderRadius: "l2",
54
+ insetBlockStart: "inset.md",
55
+ insetInlineStart: "inset.md",
56
+ layerStyle: "fill.surface",
57
+ paddingBlock: "inset.sm",
58
+ paddingInline: "inset.md",
59
+ position: "fixed",
60
+ srOnly: false,
61
+ textStyle: "label.md",
62
+ zIndex: "skipNav"
63
+ },
64
+ srOnly: true
65
+ },
66
+ target: { scrollMarginBlockStart: "inset.lg" }
67
+ },
68
+ className: "skip-nav",
69
+ jsx: [/^SkipNav(\.\w+)?$/u],
70
+ slots: ["link", "target"]
71
+ });
72
+ //#endregion
73
+ //#region src/visually-hidden/recipe.ts
74
+ /**
75
+ * States what visually hidden text is: words a screen reader reads and an eye never sees, and
76
+ * which come into view where a keyboard reaches them.
77
+ *
78
+ * @remarks
79
+ * The compiler's `srOnly` utility writes the hiding, which takes the words out of sight without
80
+ * taking them out of the accessibility tree the way `display: none` and `visibility: hidden`
81
+ * both do. A control a reader can reach by keyboard has to be visible once focus is on it, or a
82
+ * sighted reader tabbing through the page loses their place, so the focusable value undoes the
83
+ * hiding for as long as focus is there.
84
+ */
85
+ /**
86
+ * Hides what it holds from sight and leaves it to a screen reader.
87
+ */
88
+ const recipe = defineRecipe({
89
+ base: { srOnly: true },
90
+ className: "visually-hidden",
91
+ jsx: [/^VisuallyHidden$/u],
92
+ variants: { focusable: { true: { _focusVisible: {
93
+ borderRadius: "l1",
94
+ insetBlockStart: "inset.md",
95
+ insetInlineStart: "inset.md",
96
+ layerStyle: "fill.surface",
97
+ paddingBlock: "inset.sm",
98
+ paddingInline: "inset.md",
99
+ position: "fixed",
100
+ srOnly: false,
101
+ textStyle: "label.md",
102
+ zIndex: "skipNav"
103
+ } } } }
104
+ });
105
+ //#endregion
106
+ export { recipe$1 as n, recipe$2 as r, recipe as t };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,12 @@
1
+ //#region src/theme.d.ts
2
+ /**
3
+ * Publishes the preset that registers every recipe in this package, for an application's compiler
4
+ * to install.
5
+ *
6
+ * @remarks
7
+ * The list is written by hand. The package's own specification reports a recipe file the list
8
+ * leaves out, so no generator runs here.
9
+ */
10
+ declare const _default: import("@stealthscale/theme/authoring").Preset;
11
+ //#endregion
12
+ export { _default as default };
package/dist/theme.js ADDED
@@ -0,0 +1,23 @@
1
+ import { n as recipe$2, r as recipe$1, t as recipe } from "./recipe-BKojQp6r.js";
2
+ import { definePreset } from "@stealthscale/theme/authoring";
3
+ //#region src/theme.ts
4
+ /**
5
+ * Publishes the preset that registers every recipe in this package, for an application's compiler
6
+ * to install.
7
+ *
8
+ * @remarks
9
+ * The list is written by hand. The package's own specification reports a recipe file the list
10
+ * leaves out, so no generator runs here.
11
+ */
12
+ var theme_default = definePreset({
13
+ name: "@stealthscale/component-a11y",
14
+ theme: { extend: {
15
+ recipes: { visuallyHidden: recipe },
16
+ slotRecipes: {
17
+ rovingFocus: recipe$1,
18
+ skipNav: recipe$2
19
+ }
20
+ } }
21
+ });
22
+ //#endregion
23
+ export { theme_default as default };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@stealthscale/component-a11y",
3
+ "version": "0.1.0",
4
+ "description": "Draws what a keyboard and a screen reader need, and an eye does not.",
5
+ "keywords": [
6
+ "a11y",
7
+ "accessibility",
8
+ "components",
9
+ "react"
10
+ ],
11
+ "homepage": "https://github.com/stealth-scale/config/tree/main/components/a11y#readme",
12
+ "bugs": "https://github.com/stealth-scale/config/issues",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/stealth-scale/config.git",
17
+ "directory": "components/a11y"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "LICENSE",
22
+ "README.md"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "imports": {
27
+ "#*": "./src/*"
28
+ },
29
+ "exports": {
30
+ ".": "./dist/index.js",
31
+ "./theme": "./dist/theme.js",
32
+ "./package.json": "./package.json"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "devDependencies": {
38
+ "@arethetypeswrong/core": "^0.18.5",
39
+ "@rolldown/plugin-babel": "^0.2.4",
40
+ "@stealthscale/hooks": "^0.1.0",
41
+ "@stealthscale/theme": "^0.3.0",
42
+ "@stealthscale/vite-config": "^0.6.0",
43
+ "@stealthscale/vite-config-react": "^0.8.0",
44
+ "@stealthscale/vite-config-theme": "^0.1.0",
45
+ "@stealthscale/vite-config-typescript": "^0.3.0",
46
+ "@testing-library/react": "^16.3.0",
47
+ "@types/react": "^19.3.0",
48
+ "@types/react-dom": "^19.3.0",
49
+ "babel-plugin-react-compiler": "^1.0.0",
50
+ "happy-dom": "^20.14.3",
51
+ "oxc-transform-react": "^0.149.0",
52
+ "publint": "^0.3.24",
53
+ "react": "^19.3.0",
54
+ "react-dom": "^19.3.0",
55
+ "typescript": "^7.0.2",
56
+ "vitest": "4.1.11"
57
+ },
58
+ "peerDependencies": {
59
+ "@stealthscale/hooks": "^0.1.0",
60
+ "@stealthscale/theme": "^0.3.0",
61
+ "react": "^19.3.0"
62
+ },
63
+ "engines": {
64
+ "node": ">=26.0.0"
65
+ },
66
+ "scripts": {
67
+ "build": "vp pack",
68
+ "check": "vp check",
69
+ "test": "vp test"
70
+ }
71
+ }