@rcarls/rc-toolbar 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/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # `@rcarls/rc-toolbar`
2
+
3
+ A WAI-ARIA compliant toolbar component implementing the [Toolbar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/). Manages roving tabindex across slotted controls so the toolbar acts as a single tab stop with arrow-key internal navigation. Built with [Lit 3](https://lit.dev).
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @rcarls/rc-toolbar
11
+ ```
12
+
13
+ ## Import
14
+
15
+ ```js
16
+ import '@rcarls/rc-toolbar'; // side-effect: registers <rc-toolbar>
17
+ import { RCToolbar } from '@rcarls/rc-toolbar'; // named class export
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Basic usage
23
+
24
+ Place buttons, toggles, selects, or any focusable controls as direct children. Non-focusable elements (separators, labels) are rendered but skipped during keyboard navigation.
25
+
26
+ ```html
27
+ <rc-toolbar label="Formatting">
28
+ <button onclick="bold()">Bold</button>
29
+ <button onclick="italic()">Italic</button>
30
+ <button onclick="underline()">Underline</button>
31
+ <hr aria-hidden="true" />
32
+ <button onclick="alignLeft()">Align left</button>
33
+ <button onclick="alignCenter()">Align center</button>
34
+ <button onclick="alignRight()">Align right</button>
35
+ </rc-toolbar>
36
+ ```
37
+
38
+ Vertical orientation:
39
+
40
+ ```html
41
+ <rc-toolbar orientation="vertical" label="Actions">
42
+ <button>Cut</button>
43
+ <button>Copy</button>
44
+ <button>Paste</button>
45
+ </rc-toolbar>
46
+ ```
47
+
48
+ ---
49
+
50
+ ## API
51
+
52
+ ### Properties / attributes
53
+
54
+ | Property | Attribute | Type | Default | Description |
55
+ |---|---|---|---|---|
56
+ | `label` | `label` | `string` | `'Toolbar'` | `aria-label` for the toolbar. Required — no visible label is rendered by the component. |
57
+ | `orientation` | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Layout direction and keyboard navigation axis. |
58
+
59
+ ### CSS custom properties
60
+
61
+ | Property | Default | Description |
62
+ |---|---|---|
63
+ | `--rc-toolbar-gap-inline` | `0.25em` | Gap between toolbar items |
64
+ | `--rc-toolbar-padding-inline` | `0.25em` | Horizontal padding on the toolbar container |
65
+ | `--rc-toolbar-padding-block` | `0.125em` | Vertical padding on the toolbar container |
66
+
67
+ ### CSS parts
68
+
69
+ | Part | Element | Description |
70
+ |--------|-----------------------|-------------------------------|
71
+ | `root` | `div[role="toolbar"]` | The toolbar container element |
72
+
73
+ ### Slots
74
+
75
+ | Slot | Description |
76
+ |---|---|
77
+ | *(default)* | Toolbar controls. Focusable elements (`<button>`, `<input>`, `<select>`, `<a href>`, elements with `tabindex`) participate in roving-tabindex navigation. All others are rendered but skipped. |
78
+
79
+ ### Events
80
+
81
+ `rc-toolbar` dispatches no custom events. Listen for standard events (`click`, `change`, etc.) on slotted children.
82
+
83
+ ### Public methods
84
+
85
+ ```ts
86
+ focusItem(item?: HTMLElement | null): void
87
+ // Focus a specific toolbar item programmatically.
88
+
89
+ focusItemAt(index: number): void
90
+ // Focus the item at zero-based index.
91
+ ```
92
+
93
+ ### Read-only getters
94
+
95
+ ```ts
96
+ items: HTMLElement[] // All currently navigable (focusable) slotted items
97
+ firstItem: HTMLElement | undefined
98
+ lastItem: HTMLElement | undefined
99
+ nextItem: HTMLElement | undefined // Relative to the currently focused item
100
+ previousItem: HTMLElement | undefined
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Keyboard behaviour
106
+
107
+ The toolbar is a single tab stop. Arrow keys navigate within it.
108
+
109
+ | Key | Orientation | Action |
110
+ |---|---|---|
111
+ | `ArrowRight` | Horizontal | Focus next item (wraps) |
112
+ | `ArrowLeft` | Horizontal | Focus previous item (wraps) |
113
+ | `ArrowDown` | Vertical | Focus next item (wraps) |
114
+ | `ArrowUp` | Vertical | Focus previous item (wraps) |
115
+ | `Home` | Both | Focus first item |
116
+ | `End` | Both | Focus last item |
117
+ | `Tab` | Both | Leave toolbar (standard tab order) |
118
+
119
+ ---
120
+
121
+ ## ARIA
122
+
123
+ | Attribute | Where | Value |
124
+ |---|---|---|
125
+ | `role="toolbar"` | Root `div` | — |
126
+ | `aria-orientation` | Root `div` | `"horizontal"` or `"vertical"` |
127
+ | `aria-label` | Root `div` | Value of `label` property |
128
+ | `tabindex="0"` | Active item | One item tabbable at a time (roving tabindex) |
129
+ | `tabindex="-1"` | All other items | Removed from tab order |
130
+
131
+ ---
132
+
133
+ ## Browser support
134
+
135
+ All modern browsers. Requires Web Components support (Chrome 67+, Firefox 63+, Safari 12.1+).
@@ -0,0 +1,240 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "readme": "",
4
+ "modules": [
5
+ {
6
+ "kind": "javascript-module",
7
+ "path": "src/define.ts",
8
+ "declarations": [],
9
+ "exports": [
10
+ {
11
+ "kind": "custom-element-definition",
12
+ "name": "rc-toolbar",
13
+ "declaration": {
14
+ "name": "RCToolbar",
15
+ "module": "/src/index.js"
16
+ }
17
+ },
18
+ {
19
+ "kind": "js",
20
+ "name": "*",
21
+ "declaration": {
22
+ "name": "*",
23
+ "module": "src/index.js"
24
+ }
25
+ }
26
+ ]
27
+ },
28
+ {
29
+ "kind": "javascript-module",
30
+ "path": "src/index.ts",
31
+ "declarations": [],
32
+ "exports": [
33
+ {
34
+ "kind": "js",
35
+ "name": "*",
36
+ "declaration": {
37
+ "name": "*",
38
+ "module": "src/rc-toolbar"
39
+ }
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "kind": "javascript-module",
45
+ "path": "src/rc-toolbar.ts",
46
+ "declarations": [
47
+ {
48
+ "kind": "class",
49
+ "description": "A toolbar component, as defined in WAI-ARIA",
50
+ "name": "RCToolbar",
51
+ "cssProperties": [
52
+ {
53
+ "description": "Gap between toolbar items",
54
+ "name": "--rc-toolbar-gap-inline",
55
+ "default": "0.25em"
56
+ },
57
+ {
58
+ "description": "Horizontal padding on the toolbar container",
59
+ "name": "--rc-toolbar-padding-inline",
60
+ "default": "calc(var(--rc-control-padding-inline) / 2)"
61
+ },
62
+ {
63
+ "description": "Vertical padding on the toolbar container",
64
+ "name": "--rc-toolbar-padding-block",
65
+ "default": "calc(var(--rc-control-padding-block) / 2)"
66
+ },
67
+ {
68
+ "description": "Toolbar container border radius",
69
+ "name": "--rc-toolbar-radius",
70
+ "default": "var(--rc-control-radius)"
71
+ }
72
+ ],
73
+ "cssParts": [
74
+ {
75
+ "description": "The toolbar container element",
76
+ "name": "root"
77
+ }
78
+ ],
79
+ "slots": [
80
+ {
81
+ "description": "any number of child elements to display in the toolbar. Only focusable elements are navigable.",
82
+ "name": "Takes"
83
+ }
84
+ ],
85
+ "members": [
86
+ {
87
+ "kind": "field",
88
+ "name": "label",
89
+ "type": {
90
+ "text": "string"
91
+ },
92
+ "default": "'Toolbar'",
93
+ "description": "Accessible label for this toolbar. Default label is 'Toolbar'.",
94
+ "attribute": "label"
95
+ },
96
+ {
97
+ "kind": "field",
98
+ "name": "orientation",
99
+ "type": {
100
+ "text": "'horizontal' | 'vertical'"
101
+ },
102
+ "default": "'horizontal'",
103
+ "description": "Toolbar orientation, for keyboard navigation.",
104
+ "attribute": "orientation"
105
+ },
106
+ {
107
+ "kind": "field",
108
+ "name": "_$root",
109
+ "type": {
110
+ "text": "HTMLDivElement"
111
+ },
112
+ "privacy": "protected"
113
+ },
114
+ {
115
+ "kind": "field",
116
+ "name": "_disabledObserver",
117
+ "privacy": "private",
118
+ "readonly": true,
119
+ "default": "new MutationObserver(() => this._syncTabStop())"
120
+ },
121
+ {
122
+ "kind": "method",
123
+ "name": "_syncTabStop",
124
+ "privacy": "private",
125
+ "return": {
126
+ "type": {
127
+ "text": "void"
128
+ }
129
+ }
130
+ },
131
+ {
132
+ "kind": "method",
133
+ "name": "_initItems",
134
+ "privacy": "protected"
135
+ },
136
+ {
137
+ "kind": "method",
138
+ "name": "_onNavigate",
139
+ "privacy": "protected",
140
+ "parameters": [
141
+ {
142
+ "name": "action",
143
+ "type": {
144
+ "text": "KeyboardNavigationAction"
145
+ }
146
+ }
147
+ ]
148
+ },
149
+ {
150
+ "kind": "method",
151
+ "name": "_collectItems",
152
+ "privacy": "protected",
153
+ "return": {
154
+ "type": {
155
+ "text": "Element[]"
156
+ }
157
+ },
158
+ "parameters": [
159
+ {
160
+ "name": "slot",
161
+ "type": {
162
+ "text": "HTMLSlotElement"
163
+ }
164
+ }
165
+ ]
166
+ },
167
+ {
168
+ "kind": "method",
169
+ "name": "_isToolbarItem",
170
+ "privacy": "private",
171
+ "return": {
172
+ "type": {
173
+ "text": "boolean"
174
+ }
175
+ },
176
+ "parameters": [
177
+ {
178
+ "name": "el",
179
+ "type": {
180
+ "text": "Element"
181
+ }
182
+ }
183
+ ]
184
+ }
185
+ ],
186
+ "attributes": [
187
+ {
188
+ "name": "label",
189
+ "type": {
190
+ "text": "string"
191
+ },
192
+ "default": "'Toolbar'",
193
+ "description": "Accessible label for this toolbar. Default label is 'Toolbar'.",
194
+ "fieldName": "label"
195
+ },
196
+ {
197
+ "name": "orientation",
198
+ "type": {
199
+ "text": "'horizontal' | 'vertical'"
200
+ },
201
+ "default": "'horizontal'",
202
+ "description": "Toolbar orientation, for keyboard navigation.",
203
+ "fieldName": "orientation"
204
+ }
205
+ ],
206
+ "mixins": [
207
+ {
208
+ "name": "RovingTabIndexMixin",
209
+ "package": "@rcarls/rc-common"
210
+ }
211
+ ],
212
+ "superclass": {
213
+ "name": "LitElement",
214
+ "package": "lit"
215
+ },
216
+ "tagName": "rc-toolbar",
217
+ "customElement": true
218
+ }
219
+ ],
220
+ "exports": [
221
+ {
222
+ "kind": "js",
223
+ "name": "RCToolbar",
224
+ "declaration": {
225
+ "name": "RCToolbar",
226
+ "module": "src/rc-toolbar.ts"
227
+ }
228
+ },
229
+ {
230
+ "kind": "js",
231
+ "name": "default",
232
+ "declaration": {
233
+ "name": "RCToolbar",
234
+ "module": "src/rc-toolbar.ts"
235
+ }
236
+ }
237
+ ]
238
+ }
239
+ ]
240
+ }
package/dist/demo.css ADDED
@@ -0,0 +1,85 @@
1
+ *, *::before, *::after {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ :root {
6
+ color-scheme: light dark;
7
+ --rc-accent: Highlight; /* Chrome doesn't support AccentColor on the open web */
8
+ }
9
+
10
+ @supports (color: AccentColor) {
11
+ :root {
12
+ --rc-accent: AccentColor;
13
+ }
14
+ }
15
+
16
+ [data-theme="light"] { color-scheme: light; }
17
+ [data-theme="dark"] { color-scheme: dark; }
18
+
19
+ [data-theme="solarized-light"] {
20
+ color-scheme: light;
21
+ --rc-surface: #fdf6e3;
22
+ --rc-text: #657b83;
23
+ --rc-border: 1px solid #93a1a1;
24
+ --rc-shadow: 0 2px 8px rgba(0, 0, 0, .12);
25
+ }
26
+
27
+ [data-theme="solarized-dark"] {
28
+ color-scheme: dark;
29
+ --rc-surface: #002b36;
30
+ --rc-text: #839496;
31
+ --rc-border: 1px solid #586e75;
32
+ --rc-shadow: 0 2px 8px rgba(0, 0, 0, .3);
33
+ }
34
+
35
+ body {
36
+ font-family: system-ui, sans-serif;
37
+ margin: 0;
38
+ }
39
+
40
+ .demo-page {
41
+ max-width: 60rem;
42
+ margin: 0 auto;
43
+ padding: 2rem 1.5rem;
44
+ }
45
+
46
+ .demo-controls {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 1rem;
50
+ margin-bottom: 2rem;
51
+ padding-bottom: 1rem;
52
+ border-bottom: 1px solid ButtonBorder;
53
+ }
54
+
55
+ .demo-controls .demo-title {
56
+ flex: 1;
57
+ }
58
+
59
+ .demo-controls h1 {
60
+ margin: 0 0 0.15rem;
61
+ font-size: 1.5rem;
62
+ }
63
+
64
+ .demo-controls a {
65
+ font-size: 0.8rem;
66
+ }
67
+
68
+ .demo-section {
69
+ margin-bottom: 2rem;
70
+ }
71
+
72
+ .demo-section h2 {
73
+ margin: 0 0 0.5rem;
74
+ }
75
+
76
+ .theme-picker {
77
+ padding: 0.35em 0.75em;
78
+ font-family: inherit;
79
+ font-size: 0.8rem;
80
+ cursor: pointer;
81
+ border: 1px solid ButtonBorder;
82
+ border-radius: 4px;
83
+ background: Canvas;
84
+ color: CanvasText;
85
+ }
package/dist/demo.js ADDED
@@ -0,0 +1,40 @@
1
+ const THEMES = ['', 'light', 'dark', 'solarized-light', 'solarized-dark'];
2
+ const LABELS = ['Auto', 'Light', 'Dark', 'Solarized ☀', 'Solarized ☾'];
3
+
4
+ const stored = localStorage.getItem('rc-demo-theme') ?? '';
5
+ applyTheme(stored);
6
+
7
+ function applyTheme(theme) {
8
+ if (theme) {
9
+ document.documentElement.dataset.theme = theme;
10
+ } else {
11
+ delete document.documentElement.dataset.theme;
12
+ }
13
+ }
14
+
15
+ function currentIndex() {
16
+ const current = document.documentElement.dataset.theme ?? '';
17
+ const idx = THEMES.indexOf(current);
18
+ return idx === -1 ? 0 : idx;
19
+ }
20
+
21
+ function updateButtons() {
22
+ const label = LABELS[currentIndex()];
23
+ document.querySelectorAll('.theme-picker').forEach((btn) => {
24
+ btn.textContent = label;
25
+ });
26
+ }
27
+
28
+ window.cycleTheme = function () {
29
+ const next = THEMES[(currentIndex() + 1) % THEMES.length];
30
+ applyTheme(next);
31
+ localStorage.setItem('rc-demo-theme', next);
32
+ updateButtons();
33
+ };
34
+
35
+ document.addEventListener('DOMContentLoaded', () => {
36
+ updateButtons();
37
+ document.querySelectorAll('.theme-picker').forEach((btn) => {
38
+ btn.addEventListener('click', window.cycleTheme);
39
+ });
40
+ });
@@ -0,0 +1,119 @@
1
+ import { css as u, LitElement as h, html as b } from "lit";
2
+ import { property as d, query as f } from "lit/decorators.js";
3
+ import { RovingTabIndexMixin as m, isFocusable as l, keyNavigation as p } from "@rcarls/rc-common";
4
+ const v = u`
5
+ :host {
6
+ display: inline-block;
7
+ }
8
+
9
+ #root {
10
+ display: flex;
11
+ flex-direction: row;
12
+ gap: var(--rc-toolbar-gap-inline, var(--rc-control-gap, 0.25em));
13
+ padding-inline: var(--rc-toolbar-padding-inline, calc(var(--rc-control-padding-inline, 0.5em) / 2));
14
+ padding-block: var(--rc-toolbar-padding-block, calc(var(--rc-control-padding-block, 0.25em) / 2));
15
+ border-radius: var(--rc-toolbar-radius, var(--rc-control-radius, 0));
16
+ font-family: var(--rc-font-family, inherit);
17
+ font-size: var(--rc-font-size, inherit);
18
+ line-height: var(--rc-line-height, normal);
19
+
20
+ &[aria-orientation='vertical'] {
21
+ flex-direction: column;
22
+ }
23
+
24
+ /* because :has(:focus-visible) doens't work across slot boundary */
25
+ &[data-interaction-mode='keyboard']:focus-within {
26
+ outline: var(--rc-focus-ring, auto);
27
+ outline-offset: var(--rc-focus-ring-offset, 0);
28
+ }
29
+ }
30
+
31
+ #slot-wrap {
32
+ display: contents;
33
+ }
34
+ `;
35
+ var g = Object.defineProperty, s = (a, t, e, _) => {
36
+ for (var i = void 0, o = a.length - 1, c; o >= 0; o--)
37
+ (c = a[o]) && (i = c(t, e, i) || i);
38
+ return i && g(t, e, i), i;
39
+ };
40
+ const n = class n extends m(h) {
41
+ constructor() {
42
+ super(...arguments), this.label = "Toolbar", this.orientation = "horizontal", this._disabledObserver = new MutationObserver(() => this._syncTabStop());
43
+ }
44
+ connectedCallback() {
45
+ super.connectedCallback(), this._disabledObserver.observe(this, { attributeFilter: ["disabled"], subtree: !0 });
46
+ }
47
+ disconnectedCallback() {
48
+ super.disconnectedCallback(), this._disabledObserver.disconnect();
49
+ }
50
+ _syncTabStop() {
51
+ !this._lastFocused || l(this._lastFocused) || (this._lastFocused = void 0, this._initItems());
52
+ }
53
+ _initItems() {
54
+ super._initItems();
55
+ const t = this._lastFocused ?? this.firstItem;
56
+ queueMicrotask(() => {
57
+ this.matches(":focus-within") && this.focusItem(t);
58
+ });
59
+ }
60
+ _onNavigate(t) {
61
+ switch (t) {
62
+ case "next":
63
+ this.focusItem(this.nextItem);
64
+ break;
65
+ case "prev":
66
+ this.focusItem(this.previousItem);
67
+ break;
68
+ case "start":
69
+ this.focusItem(this.firstItem);
70
+ break;
71
+ case "end":
72
+ this.focusItem(this.lastItem);
73
+ break;
74
+ }
75
+ }
76
+ _collectItems(t) {
77
+ return t.assignedElements().filter((e) => this._isToolbarItem(e));
78
+ }
79
+ _isToolbarItem(t) {
80
+ if (t instanceof HTMLButtonElement || t instanceof HTMLInputElement || t instanceof HTMLSelectElement || t instanceof HTMLTextAreaElement)
81
+ return !0;
82
+ if (t instanceof HTMLAnchorElement || t instanceof HTMLAreaElement)
83
+ return t.hasAttribute("href");
84
+ const e = t.getAttribute("role");
85
+ return e === "button" || e === "checkbox" || e === "link" || e === "menuitem" || e === "radio" || e === "slider" || e === "spinbutton" || e === "switch" || e === "textbox" ? !0 : l(t);
86
+ }
87
+ render() {
88
+ return b`
89
+ <div
90
+ id="root"
91
+ part="root"
92
+ ${p(this._onNavigate)}
93
+ data-interaction-mode="keyboard"
94
+ role="toolbar"
95
+ aria-orientation=${this.orientation}
96
+ aria-label=${this.label}
97
+ >
98
+ <div id="slot-wrap" @focusin=${this._handleItemFocus}>
99
+ <slot id="items" @slotchange=${this._onSlotChange}></slot>
100
+ </div>
101
+ </div>
102
+ `;
103
+ }
104
+ };
105
+ n.styles = [v];
106
+ let r = n;
107
+ s([
108
+ d({ type: String })
109
+ ], r.prototype, "label");
110
+ s([
111
+ d({ type: String })
112
+ ], r.prototype, "orientation");
113
+ s([
114
+ f("#root", !0)
115
+ ], r.prototype, "_$root");
116
+ export {
117
+ r as R
118
+ };
119
+ //# sourceMappingURL=rc-toolbar-D7nv57AW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rc-toolbar-D7nv57AW.js","sources":["../src/rc-toolbar.styles.ts","../src/rc-toolbar.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const toolbarStyles = css`\n :host {\n display: inline-block;\n }\n\n #root {\n display: flex;\n flex-direction: row;\n gap: var(--rc-toolbar-gap-inline, var(--rc-control-gap, 0.25em));\n padding-inline: var(--rc-toolbar-padding-inline, calc(var(--rc-control-padding-inline, 0.5em) / 2));\n padding-block: var(--rc-toolbar-padding-block, calc(var(--rc-control-padding-block, 0.25em) / 2));\n border-radius: var(--rc-toolbar-radius, var(--rc-control-radius, 0));\n font-family: var(--rc-font-family, inherit);\n font-size: var(--rc-font-size, inherit);\n line-height: var(--rc-line-height, normal);\n\n &[aria-orientation='vertical'] {\n flex-direction: column;\n }\n\n /* because :has(:focus-visible) doens't work across slot boundary */\n &[data-interaction-mode='keyboard']:focus-within {\n outline: var(--rc-focus-ring, auto);\n outline-offset: var(--rc-focus-ring-offset, 0);\n }\n }\n\n #slot-wrap {\n display: contents;\n }\n`;\n\nexport default toolbarStyles;\n","import { LitElement, html } from 'lit';\nimport { property, query } from 'lit/decorators.js';\n\nimport {\n isFocusable,\n keyNavigation,\n type KeyboardNavigationAction,\n RovingTabIndexMixin,\n} from '@rcarls/rc-common';\n\nimport toolbarStyles from './rc-toolbar.styles';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-toolbar': RCToolbar;\n }\n}\n\n/**\n * A toolbar component, as defined in WAI-ARIA\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/\n * @slot Takes any number of child elements to display in the toolbar. Only focusable elements are navigable.\n * @cssprop [--rc-toolbar-gap-inline=0.25em] - Gap between toolbar items\n * @cssprop [--rc-toolbar-padding-inline=calc(var(--rc-control-padding-inline) / 2)] - Horizontal padding on the toolbar container\n * @cssprop [--rc-toolbar-padding-block=calc(var(--rc-control-padding-block) / 2)] - Vertical padding on the toolbar container\n * @cssprop [--rc-toolbar-radius=var(--rc-control-radius)] - Toolbar container border radius\n * @csspart root - The toolbar container element\n */\nexport class RCToolbar extends RovingTabIndexMixin(LitElement) {\n static styles = [toolbarStyles];\n\n /** Accessible label for this toolbar. Default label is 'Toolbar'. */\n @property({ type: String })\n label = 'Toolbar';\n\n /** Toolbar orientation, for keyboard navigation. */\n @property({ type: String })\n orientation: 'horizontal' | 'vertical' = 'horizontal';\n\n @query('#root', true)\n protected _$root!: HTMLDivElement;\n\n private readonly _disabledObserver = new MutationObserver(() => this._syncTabStop());\n\n override connectedCallback(): void {\n super.connectedCallback();\n this._disabledObserver.observe(this, { attributeFilter: ['disabled'], subtree: true });\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._disabledObserver.disconnect();\n }\n\n private _syncTabStop(): void {\n if (!this._lastFocused || isFocusable(this._lastFocused)) return;\n this._lastFocused = undefined;\n this._initItems();\n }\n\n protected override _initItems() {\n // Set tabindex synchronously so items are in the tab order immediately,\n // even if the document has no focus yet (focus() would be a no-op).\n super._initItems();\n\n // Defer focus to avoid firing focusin/focusout inside a SolidJS reactive\n // update cycle, which can cause the update to hang.\n // Guard: only restore focus when the toolbar already contains it — prevents\n // focus-stealing when a toolbar mounts inside a larger component (e.g. rc-transfer-list).\n const target = this._lastFocused ?? this.firstItem;\n queueMicrotask(() => {\n if (this.matches(':focus-within')) this.focusItem(target);\n });\n }\n\n protected _onNavigate(action: KeyboardNavigationAction) {\n switch (action) {\n case 'next':\n this.focusItem(this.nextItem);\n break;\n case 'prev':\n this.focusItem(this.previousItem);\n break;\n case 'start':\n this.focusItem(this.firstItem);\n break;\n case 'end':\n this.focusItem(this.lastItem);\n break;\n }\n }\n\n protected override _collectItems(slot: HTMLSlotElement): Element[] {\n return slot.assignedElements().filter((el) => this._isToolbarItem(el));\n }\n\n private _isToolbarItem(el: Element): boolean {\n if (\n el instanceof HTMLButtonElement ||\n el instanceof HTMLInputElement ||\n el instanceof HTMLSelectElement ||\n el instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n\n if (el instanceof HTMLAnchorElement || el instanceof HTMLAreaElement) {\n return el.hasAttribute('href');\n }\n\n const role = el.getAttribute('role');\n if (\n role === 'button' ||\n role === 'checkbox' ||\n role === 'link' ||\n role === 'menuitem' ||\n role === 'radio' ||\n role === 'slider' ||\n role === 'spinbutton' ||\n role === 'switch' ||\n role === 'textbox'\n ) {\n return true;\n }\n\n return isFocusable(el);\n }\n\n protected render() {\n return html`\n <div\n id=\"root\"\n part=\"root\"\n ${keyNavigation(this._onNavigate)}\n data-interaction-mode=\"keyboard\"\n role=\"toolbar\"\n aria-orientation=${this.orientation}\n aria-label=${this.label}\n >\n <div id=\"slot-wrap\" @focusin=${this._handleItemFocus}>\n <slot id=\"items\" @slotchange=${this._onSlotChange}></slot>\n </div>\n </div>\n `;\n }\n}\n\nexport default RCToolbar;\n"],"names":["toolbarStyles","css","_RCToolbar","RovingTabIndexMixin","LitElement","isFocusable","target","action","slot","el","role","html","keyNavigation","RCToolbar","__decorateClass","property","query"],"mappings":";;;AAEO,MAAMA,IAAgBC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;;;AC2BtB,MAAMC,IAAN,MAAMA,UAAkBC,EAAoBC,CAAU,EAAE;AAAA,EAAxD,cAAA;AAAA,UAAA,GAAA,SAAA,GAKL,KAAA,QAAQ,WAIR,KAAA,cAAyC,cAKzC,KAAiB,oBAAoB,IAAI,iBAAiB,MAAM,KAAK,cAAc;AAAA,EAAA;AAAA,EAE1E,oBAA0B;AACjC,UAAM,kBAAA,GACN,KAAK,kBAAkB,QAAQ,MAAM,EAAE,iBAAiB,CAAC,UAAU,GAAG,SAAS,IAAM;AAAA,EACvF;AAAA,EAES,uBAA6B;AACpC,UAAM,qBAAA,GACN,KAAK,kBAAkB,WAAA;AAAA,EACzB;AAAA,EAEQ,eAAqB;AAC3B,IAAI,CAAC,KAAK,gBAAgBC,EAAY,KAAK,YAAY,MACvD,KAAK,eAAe,QACpB,KAAK,WAAA;AAAA,EACP;AAAA,EAEmB,aAAa;AAG9B,UAAM,WAAA;AAMN,UAAMC,IAAS,KAAK,gBAAgB,KAAK;AACzC,mBAAe,MAAM;AACnB,MAAI,KAAK,QAAQ,eAAe,KAAG,KAAK,UAAUA,CAAM;AAAA,IAC1D,CAAC;AAAA,EACH;AAAA,EAEU,YAAYC,GAAkC;AACtD,YAAQA,GAAA;AAAA,MACN,KAAK;AACH,aAAK,UAAU,KAAK,QAAQ;AAC5B;AAAA,MACF,KAAK;AACH,aAAK,UAAU,KAAK,YAAY;AAChC;AAAA,MACF,KAAK;AACH,aAAK,UAAU,KAAK,SAAS;AAC7B;AAAA,MACF,KAAK;AACH,aAAK,UAAU,KAAK,QAAQ;AAC5B;AAAA,IAAA;AAAA,EAEN;AAAA,EAEmB,cAAcC,GAAkC;AACjE,WAAOA,EAAK,mBAAmB,OAAO,CAACC,MAAO,KAAK,eAAeA,CAAE,CAAC;AAAA,EACvE;AAAA,EAEQ,eAAeA,GAAsB;AAC3C,QACEA,aAAc,qBACdA,aAAc,oBACdA,aAAc,qBACdA,aAAc;AAEd,aAAO;AAGT,QAAIA,aAAc,qBAAqBA,aAAc;AACnD,aAAOA,EAAG,aAAa,MAAM;AAG/B,UAAMC,IAAOD,EAAG,aAAa,MAAM;AACnC,WACEC,MAAS,YACTA,MAAS,cACTA,MAAS,UACTA,MAAS,cACTA,MAAS,WACTA,MAAS,YACTA,MAAS,gBACTA,MAAS,YACTA,MAAS,YAEF,KAGFL,EAAYI,CAAE;AAAA,EACvB;AAAA,EAEU,SAAS;AACjB,WAAOE;AAAA;AAAA;AAAA;AAAA,UAIDC,EAAc,KAAK,WAAW,CAAC;AAAA;AAAA;AAAA,2BAGd,KAAK,WAAW;AAAA,qBACtB,KAAK,KAAK;AAAA;AAAA,uCAEQ,KAAK,gBAAgB;AAAA,yCACnB,KAAK,aAAa;AAAA;AAAA;AAAA;AAAA,EAIzD;AACF;AApHEV,EAAO,SAAS,CAACF,CAAa;AADzB,IAAMa,IAANX;AAKLY,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,OAAA,CAAQ;AAAA,GAJfF,EAKX,WAAA,OAAA;AAIAC,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,OAAA,CAAQ;AAAA,GARfF,EASX,WAAA,aAAA;AAGUC,EAAA;AAAA,EADTE,EAAM,SAAS,EAAI;AAAA,GAXTH,EAYD,WAAA,QAAA;"}
@@ -0,0 +1,6 @@
1
+ import { R as o } from "./rc-toolbar-D7nv57AW.js";
2
+ customElements.get("rc-toolbar") || customElements.define("rc-toolbar", o);
3
+ export {
4
+ o as RCToolbar
5
+ };
6
+ //# sourceMappingURL=rc-toolbar-define.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rc-toolbar-define.js","sources":["../src/define.ts"],"sourcesContent":["import { RCToolbar } from './index.js';\n\ncustomElements.get('rc-toolbar') || customElements.define('rc-toolbar', RCToolbar);\n\nexport * from './index.js';\n"],"names":["RCToolbar"],"mappings":";AAEA,eAAe,IAAI,YAAY,KAAK,eAAe,OAAO,cAAcA,CAAS;"}
@@ -0,0 +1,5 @@
1
+ import { R as a } from "./rc-toolbar-D7nv57AW.js";
2
+ export {
3
+ a as RCToolbar
4
+ };
5
+ //# sourceMappingURL=rc-toolbar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rc-toolbar.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1 @@
1
+ export * from './index.js';
@@ -0,0 +1 @@
1
+ export * from './rc-toolbar';
@@ -0,0 +1,37 @@
1
+ import { LitElement } from 'lit';
2
+ import { KeyboardNavigationAction } from '@rcarls/rc-common';
3
+ declare global {
4
+ interface HTMLElementTagNameMap {
5
+ 'rc-toolbar': RCToolbar;
6
+ }
7
+ }
8
+ declare const RCToolbar_base: typeof LitElement & (new (...args: any[]) => import('@rcarls/rc-common').RovingTabIndexMixinBase);
9
+ /**
10
+ * A toolbar component, as defined in WAI-ARIA
11
+ *
12
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
13
+ * @slot Takes any number of child elements to display in the toolbar. Only focusable elements are navigable.
14
+ * @cssprop [--rc-toolbar-gap-inline=0.25em] - Gap between toolbar items
15
+ * @cssprop [--rc-toolbar-padding-inline=calc(var(--rc-control-padding-inline) / 2)] - Horizontal padding on the toolbar container
16
+ * @cssprop [--rc-toolbar-padding-block=calc(var(--rc-control-padding-block) / 2)] - Vertical padding on the toolbar container
17
+ * @cssprop [--rc-toolbar-radius=var(--rc-control-radius)] - Toolbar container border radius
18
+ * @csspart root - The toolbar container element
19
+ */
20
+ export declare class RCToolbar extends RCToolbar_base {
21
+ static styles: import('lit').CSSResult[];
22
+ /** Accessible label for this toolbar. Default label is 'Toolbar'. */
23
+ label: string;
24
+ /** Toolbar orientation, for keyboard navigation. */
25
+ orientation: 'horizontal' | 'vertical';
26
+ protected _$root: HTMLDivElement;
27
+ private readonly _disabledObserver;
28
+ connectedCallback(): void;
29
+ disconnectedCallback(): void;
30
+ private _syncTabStop;
31
+ protected _initItems(): void;
32
+ protected _onNavigate(action: KeyboardNavigationAction): void;
33
+ protected _collectItems(slot: HTMLSlotElement): Element[];
34
+ private _isToolbarItem;
35
+ protected render(): import('lit').TemplateResult<1>;
36
+ }
37
+ export default RCToolbar;
@@ -0,0 +1,2 @@
1
+ export declare const toolbarStyles: import('lit').CSSResult;
2
+ export default toolbarStyles;
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@rcarls/rc-toolbar",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.1.0",
7
+ "description": "Headless WAI-ARIA toolbar web component built with Lit",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
+ "directory": "packages/rc-toolbar"
12
+ },
13
+ "homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "module": "./dist/rc-toolbar.js",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/types/packages/rc-toolbar/src/index.d.ts",
24
+ "default": "./dist/rc-toolbar.js"
25
+ }
26
+ },
27
+ "./define": {
28
+ "import": {
29
+ "types": "./dist/types/packages/rc-toolbar/src/define.d.ts",
30
+ "default": "./dist/rc-toolbar-define.js"
31
+ }
32
+ }
33
+ },
34
+ "types": "./dist/types/packages/rc-toolbar/src/index.d.ts",
35
+ "sideEffects": [
36
+ "./dist/rc-toolbar-define.js"
37
+ ],
38
+ "customElements": "dist/custom-elements.json",
39
+ "scripts": {
40
+ "dev": "vite",
41
+ "build": "tsc && vite build && cem analyze",
42
+ "cem:analyze": "cem analyze",
43
+ "preview": "vite preview",
44
+ "test:browser": "vitest",
45
+ "test:browser:chrome": "vitest --project=chromium",
46
+ "test:browser:firefox": "vitest --project=firefox",
47
+ "yalc:publish": "yalc publish --push"
48
+ },
49
+ "dependencies": {
50
+ "@rcarls/rc-common": "workspace:^"
51
+ },
52
+ "devDependencies": {
53
+ "@custom-elements-manifest/analyzer": "0.11.0",
54
+ "@guanghechen/rollup-plugin-copy": "^6.0.9",
55
+ "@vitest/browser-playwright": "4.1.5",
56
+ "lit": "^3.0.0",
57
+ "playwright": "^1.56.0",
58
+ "rollup": "^4.60.2",
59
+ "typescript": "~5.9.3",
60
+ "vite": "^7.1.7",
61
+ "vite-plugin-dts": "^4.5.4",
62
+ "vitest": "^4.0.6",
63
+ "vitest-browser-lit": "^1.0.1"
64
+ },
65
+ "peerDependencies": {
66
+ "lit": "^3.0.0"
67
+ }
68
+ }