sinwan-ui-core 1.1.0 → 1.2.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 +13 -0
- package/README.md +1 -1
- package/dist/chunks/{chunk-3zrktrd2.js → chunk-33f19xws.js} +4 -4
- package/dist/chunks/{chunk-te52bwb8.js → chunk-s03me75x.js} +88 -27
- package/dist/controls.d.ts +13 -0
- package/dist/controls.js +2 -1
- package/dist/index.js +7 -7
- package/dist/theme.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package uses [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [1.2.0]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `RadioGroupRoot` / `RadioGroupItem` (`controls`) follow the WAI-ARIA radio group keyboard pattern. The group has a single tab stop: the checked radio, or the first enabled one when nothing is checked. ArrowDown/ArrowRight select and focus the next enabled radio, and ArrowUp/ArrowLeft the previous one. Both wrap and skip disabled radios, and ArrowLeft/ArrowRight swap inside a right-to-left `DirectionProvider`. Click and Space are unchanged. Before, every radio was its own tab stop and arrow keys did nothing.
|
|
10
|
+
- `RadioGroupKey` gains `register`, `tabStop` and `move`, and the `RadioGroupEntry` type is exported. Code that only reads `value` and `setValue` is unaffected.
|
|
11
|
+
|
|
12
|
+
## [1.1.1]
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `ToggleGroupRoot` (`controls`) forwards `aria-*` attributes to its `role="group"` element. Before, an `aria-label` such as "Billing period" was dropped, so screen readers announced an unnamed group. Values stay live when driven by a signal or getter.
|
|
17
|
+
|
|
5
18
|
## [1.1.0]
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Import from the root barrel or from a subpath (subpaths keep bundles small):
|
|
|
24
24
|
| --- | --- |
|
|
25
25
|
| `sinwan-ui-core/core` | `Presence` (keeps content mounted through CSS exit), `UiPortal`, `useControllableState`, `getFocusableElements`, `trapFocus`; re-exports the positioning helpers from `floating` |
|
|
26
26
|
| `sinwan-ui-core/overlay` | Dialog, Collapsible, Popover and Tooltip primitives (`DialogRoot`, `DialogTrigger`, `DialogOverlay`, `DialogContent`, `PopoverRoot`, …), their injection keys and `OpenApi` |
|
|
27
|
-
| `sinwan-ui-core/controls` | Tabs, Accordion, Checkbox, Switch, RadioGroup, Toggle and ToggleGroup primitives with injection keys |
|
|
27
|
+
| `sinwan-ui-core/controls` | Tabs, Accordion, Checkbox, Switch, RadioGroup (WAI-ARIA arrow keys and a single tab stop), Toggle and ToggleGroup primitives with injection keys |
|
|
28
28
|
| `sinwan-ui-core/floating` | `useAnchorPosition`, `usePointPosition`, `computePosition`, `estimatePosition` and placement helpers (Floating UI based) |
|
|
29
29
|
| `sinwan-ui-core/dismiss` | `isDismissExemptPointerTarget` for outside-press handling across portaled layers |
|
|
30
30
|
| `sinwan-ui-core/menu-sub` | `createMenuSubApi` (closes with its `parentOpen`), `createMenuSubTriggerHandlers`, `isInsideMenuSubContent`, `MenuSubContentLayer` for nested menus |
|
|
@@ -80,9 +80,9 @@ function writeDomPalette(next) {
|
|
|
80
80
|
document.documentElement.setAttribute("data-palette", next);
|
|
81
81
|
}
|
|
82
82
|
var ThemeProvider2 = cc((props) => {
|
|
83
|
-
const allowedPalettes = (props.palettes === undefined ? DEFAULT_PALETTES : props.palettes).length > 0 ? props.palettes === undefined ? DEFAULT_PALETTES : props.palettes : DEFAULT_PALETTES;
|
|
83
|
+
const allowedPalettes = () => (props.palettes === undefined ? DEFAULT_PALETTES : props.palettes).length > 0 ? props.palettes === undefined ? DEFAULT_PALETTES : props.palettes : DEFAULT_PALETTES;
|
|
84
84
|
const initialTheme = props.forcedTheme ?? readStoredTheme(props.storageKey === undefined ? "sinwan-ui-theme" : props.storageKey, props.defaultTheme === undefined ? "system" : props.defaultTheme);
|
|
85
|
-
const initialPalette = props.forcedPalette ?? readStoredPalette(props.paletteStorageKey === undefined ? "sinwan-ui-palette" : props.paletteStorageKey, props.defaultPalette === undefined ? "credit-blue" : props.defaultPalette, allowedPalettes);
|
|
85
|
+
const initialPalette = props.forcedPalette ?? readStoredPalette(props.paletteStorageKey === undefined ? "sinwan-ui-palette" : props.paletteStorageKey, props.defaultPalette === undefined ? "credit-blue" : props.defaultPalette, allowedPalettes());
|
|
86
86
|
const theme = signal(initialTheme);
|
|
87
87
|
const palette = signal(initialPalette);
|
|
88
88
|
const resolved = signal(initialTheme === "system" ? getSystemTheme() : initialTheme);
|
|
@@ -131,7 +131,7 @@ var ThemeProvider2 = cc((props) => {
|
|
|
131
131
|
const setPalette = (value) => {
|
|
132
132
|
if (props.forcedPalette)
|
|
133
133
|
return;
|
|
134
|
-
if (!allowedPalettes.includes(value))
|
|
134
|
+
if (!allowedPalettes().includes(value))
|
|
135
135
|
return;
|
|
136
136
|
try {
|
|
137
137
|
localStorage.setItem(props.paletteStorageKey === undefined ? "sinwan-ui-palette" : props.paletteStorageKey, value);
|
|
@@ -159,7 +159,7 @@ var ThemeProvider2 = cc((props) => {
|
|
|
159
159
|
themes: (props.enableSystem === undefined ? true : props.enableSystem) ? ["light", "dark", "system"] : ["light", "dark"],
|
|
160
160
|
palette,
|
|
161
161
|
setPalette,
|
|
162
|
-
palettes: allowedPalettes
|
|
162
|
+
palettes: allowedPalettes()
|
|
163
163
|
});
|
|
164
164
|
computed(() => resolved.value);
|
|
165
165
|
computed(() => palette.value);
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Presence2
|
|
3
3
|
} from "./chunk-ejwby1pn.js";
|
|
4
|
+
import {
|
|
5
|
+
DirectionKey2
|
|
6
|
+
} from "./chunk-f6hw8ash.js";
|
|
4
7
|
|
|
5
8
|
// src/controls.tsx
|
|
6
9
|
import { _$createTemplate } from "sinwan/renderer/web";
|
|
7
10
|
import { unwrap as _$unwrap } from "sinwan/reactivity";
|
|
8
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
cc,
|
|
13
|
+
getRawProps,
|
|
14
|
+
inject,
|
|
15
|
+
onUnmounted,
|
|
16
|
+
provide
|
|
17
|
+
} from "sinwan/component";
|
|
9
18
|
import { For as For2, Show as Show2 } from "sinwan/control-flow";
|
|
10
19
|
import { computed, signal } from "sinwan/reactivity";
|
|
11
20
|
import { jsx } from "sinwan/jsx-runtime";
|
|
@@ -98,14 +107,17 @@ var _$tmpl_8 = {
|
|
|
98
107
|
]
|
|
99
108
|
};
|
|
100
109
|
var _$tmpl_9 = {
|
|
101
|
-
html: '<button type="button" role="radio" id="" data-slot="radio-group-item" data-state="" aria-checked="" disabled=""></button>',
|
|
110
|
+
html: '<button type="button" role="radio" id="" data-slot="radio-group-item" data-state="" aria-checked="" tabIndex="" disabled=""></button>',
|
|
102
111
|
slots: [
|
|
112
|
+
{ path: [], type: "ref", name: "ref" },
|
|
103
113
|
{ path: [], type: "attr", name: "id" },
|
|
104
114
|
{ path: [], type: "attr", name: "data-state" },
|
|
105
115
|
{ path: [], type: "attr", name: "aria-checked" },
|
|
116
|
+
{ path: [], type: "attr", name: "tabIndex" },
|
|
106
117
|
{ path: [], type: "attr", name: "disabled" },
|
|
107
118
|
{ path: [], type: "attr", name: "class" },
|
|
108
119
|
{ path: [], type: "event", name: "onclick" },
|
|
120
|
+
{ path: [], type: "event", name: "onkeydown" },
|
|
109
121
|
{ path: [], type: "child" }
|
|
110
122
|
]
|
|
111
123
|
};
|
|
@@ -121,18 +133,6 @@ var _$tmpl_10 = {
|
|
|
121
133
|
]
|
|
122
134
|
};
|
|
123
135
|
var _$tmpl_11 = {
|
|
124
|
-
html: '<div data-slot="toggle-group" role="group" style="" data-variant="" data-size="" data-spacing="" data-orientation=""></div>',
|
|
125
|
-
slots: [
|
|
126
|
-
{ path: [], type: "attr", name: "class" },
|
|
127
|
-
{ path: [], type: "attr", name: "style" },
|
|
128
|
-
{ path: [], type: "attr", name: "data-variant" },
|
|
129
|
-
{ path: [], type: "attr", name: "data-size" },
|
|
130
|
-
{ path: [], type: "attr", name: "data-spacing" },
|
|
131
|
-
{ path: [], type: "attr", name: "data-orientation" },
|
|
132
|
-
{ path: [], type: "child" }
|
|
133
|
-
]
|
|
134
|
-
};
|
|
135
|
-
var _$tmpl_12 = {
|
|
136
136
|
html: '<button type="button" data-slot="toggle-group-item" data-variant="" data-size="" data-spacing="" data-state="" disabled=""></button>',
|
|
137
137
|
slots: [
|
|
138
138
|
{ path: [], type: "attr", name: "data-variant" },
|
|
@@ -342,6 +342,10 @@ var SwitchRoot2 = cc((props) => {
|
|
|
342
342
|
]);
|
|
343
343
|
});
|
|
344
344
|
var RadioGroupKey2 = Symbol("sinwan-ui.radio-group");
|
|
345
|
+
var DOCUMENT_POSITION_FOLLOWING = 4;
|
|
346
|
+
function inDocumentOrder(a, b) {
|
|
347
|
+
return a.el.compareDocumentPosition(b.el) & DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
348
|
+
}
|
|
345
349
|
var RadioGroupRoot2 = cc((props) => {
|
|
346
350
|
const valueLocal = signal(props.defaultValue ?? "");
|
|
347
351
|
const value = "value" in props ? computed(() => props.value ?? "") : valueLocal;
|
|
@@ -349,28 +353,81 @@ var RadioGroupRoot2 = cc((props) => {
|
|
|
349
353
|
if (!("value" in props))
|
|
350
354
|
valueLocal.value = next;
|
|
351
355
|
};
|
|
356
|
+
const setValue = (v) => {
|
|
357
|
+
set(v);
|
|
358
|
+
props.onValueChange?.(v);
|
|
359
|
+
};
|
|
360
|
+
const entries = signal([]);
|
|
361
|
+
const enabled = () => entries.value.filter((entry) => !entry.disabled());
|
|
362
|
+
const tabStop = computed(() => {
|
|
363
|
+
const list = enabled();
|
|
364
|
+
return (list.find((entry) => entry.value() === value.value) ?? list[0])?.value();
|
|
365
|
+
});
|
|
352
366
|
provide(RadioGroupKey2, {
|
|
353
367
|
value,
|
|
354
368
|
name: props.name,
|
|
355
|
-
setValue
|
|
356
|
-
|
|
357
|
-
|
|
369
|
+
setValue,
|
|
370
|
+
register: (entry) => {
|
|
371
|
+
entries.value = [...entries.value, entry];
|
|
372
|
+
return () => {
|
|
373
|
+
entries.value = entries.value.filter((other) => other !== entry);
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
tabStop,
|
|
377
|
+
move: (from, step) => {
|
|
378
|
+
const list = enabled().sort(inDocumentOrder);
|
|
379
|
+
if (list.length === 0)
|
|
380
|
+
return;
|
|
381
|
+
const index = list.findIndex((entry) => entry.value() === from);
|
|
382
|
+
const next = index === -1 ? list[step === 1 ? 0 : list.length - 1] : list[(index + step + list.length) % list.length];
|
|
383
|
+
next.el.focus();
|
|
384
|
+
setValue(next.value());
|
|
358
385
|
}
|
|
359
386
|
});
|
|
360
387
|
return _$createTemplate(_$tmpl_8, [() => _$unwrap(props).class, () => _$unwrap(props).children]);
|
|
361
388
|
});
|
|
389
|
+
var LTR = { dir: { value: "ltr" } };
|
|
390
|
+
function radioStep(key, dir) {
|
|
391
|
+
const forward = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
392
|
+
const backward = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
393
|
+
if (key === "ArrowDown" || key === forward)
|
|
394
|
+
return 1;
|
|
395
|
+
if (key === "ArrowUp" || key === backward)
|
|
396
|
+
return -1;
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
362
399
|
var RadioGroupItem2 = cc((props) => {
|
|
363
400
|
const api = inject(RadioGroupKey2);
|
|
401
|
+
const direction = inject(DirectionKey2, LTR);
|
|
402
|
+
let unregister;
|
|
403
|
+
onUnmounted(() => unregister?.());
|
|
364
404
|
return _$createTemplate(_$tmpl_9, [
|
|
405
|
+
(el) => {
|
|
406
|
+
if (el && !unregister) {
|
|
407
|
+
unregister = api.register({
|
|
408
|
+
value: () => props.value,
|
|
409
|
+
disabled: () => Boolean(props.disabled),
|
|
410
|
+
el
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
},
|
|
365
414
|
() => _$unwrap(props).id,
|
|
366
415
|
() => api.value.value === _$unwrap(props).value ? "checked" : "unchecked",
|
|
367
416
|
() => api.value.value === _$unwrap(props).value,
|
|
417
|
+
() => api.tabStop.value === _$unwrap(props).value ? 0 : -1,
|
|
368
418
|
() => _$unwrap(props).disabled,
|
|
369
419
|
() => _$unwrap(props).class,
|
|
370
420
|
() => {
|
|
371
421
|
if (!props.disabled)
|
|
372
422
|
api.setValue(props.value);
|
|
373
423
|
},
|
|
424
|
+
(event) => {
|
|
425
|
+
const step = radioStep(event.key, direction.dir.value);
|
|
426
|
+
if (step === 0)
|
|
427
|
+
return;
|
|
428
|
+
event.preventDefault();
|
|
429
|
+
api.move(props.value, step);
|
|
430
|
+
},
|
|
374
431
|
() => _$unwrap(props).children
|
|
375
432
|
]);
|
|
376
433
|
});
|
|
@@ -399,6 +456,7 @@ var ToggleRoot2 = cc((props) => {
|
|
|
399
456
|
var ToggleGroupKey2 = Symbol("sinwan-ui.toggle-group");
|
|
400
457
|
var ToggleGroupRoot2 = cc((props) => {
|
|
401
458
|
const type = () => props.type ?? "single";
|
|
459
|
+
const ariaProps = Object.fromEntries(Object.entries(getRawProps(props)).filter(([key]) => key.startsWith("aria-")));
|
|
402
460
|
const valueLocal = signal(toValueList(props.defaultValue));
|
|
403
461
|
const value = "value" in props ? computed(() => toValueList(props.value)) : valueLocal;
|
|
404
462
|
const set = (next) => {
|
|
@@ -419,19 +477,22 @@ var ToggleGroupRoot2 = cc((props) => {
|
|
|
419
477
|
props.onValueChange?.(type() === "single" ? next[0] ?? "" : next);
|
|
420
478
|
}
|
|
421
479
|
});
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
() => props
|
|
426
|
-
() => props
|
|
427
|
-
() => props["data-
|
|
428
|
-
() => props["data-
|
|
429
|
-
() =>
|
|
430
|
-
|
|
480
|
+
return /* @__PURE__ */ jsx("div", {
|
|
481
|
+
"data-slot": "toggle-group",
|
|
482
|
+
role: "group",
|
|
483
|
+
class: () => _$unwrap(props).class,
|
|
484
|
+
style: () => _$unwrap(props).style,
|
|
485
|
+
"data-variant": () => props["data-variant"],
|
|
486
|
+
"data-size": () => props["data-size"],
|
|
487
|
+
"data-spacing": () => props["data-spacing"],
|
|
488
|
+
"data-orientation": () => props["data-orientation"],
|
|
489
|
+
...ariaProps,
|
|
490
|
+
children: () => _$unwrap(props).children
|
|
491
|
+
});
|
|
431
492
|
});
|
|
432
493
|
var ToggleGroupItem2 = cc((props) => {
|
|
433
494
|
const api = inject(ToggleGroupKey2);
|
|
434
|
-
return _$createTemplate(_$
|
|
495
|
+
return _$createTemplate(_$tmpl_11, [
|
|
435
496
|
() => props["data-variant"],
|
|
436
497
|
() => props["data-size"],
|
|
437
498
|
() => props["data-spacing"],
|
package/dist/controls.d.ts
CHANGED
|
@@ -83,10 +83,22 @@ export declare const SwitchRoot: import("sinwan/component").SinwanComponent<{
|
|
|
83
83
|
children?: SinwanNode;
|
|
84
84
|
"data-size"?: string;
|
|
85
85
|
}>;
|
|
86
|
+
/** A radio registered with its group for roving focus. */
|
|
87
|
+
export type RadioGroupEntry = {
|
|
88
|
+
value: () => string;
|
|
89
|
+
disabled: () => boolean;
|
|
90
|
+
el: HTMLElement;
|
|
91
|
+
};
|
|
86
92
|
export declare const RadioGroupKey: InjectionKey<{
|
|
87
93
|
value: Signal<string> | Computed<string>;
|
|
88
94
|
setValue: (v: string) => void;
|
|
89
95
|
name?: string;
|
|
96
|
+
/** Adds a radio to the group's keyboard order; returns its removal. */
|
|
97
|
+
register: (entry: RadioGroupEntry) => () => void;
|
|
98
|
+
/** Value of the one radio that is in the tab order. */
|
|
99
|
+
tabStop: Computed<string | undefined>;
|
|
100
|
+
/** Moves focus and selection `step` enabled radios from `from`, wrapping. */
|
|
101
|
+
move: (from: string, step: 1 | -1) => void;
|
|
90
102
|
}>;
|
|
91
103
|
export declare const RadioGroupRoot: import("sinwan/component").SinwanComponent<{
|
|
92
104
|
children?: SinwanNode;
|
|
@@ -117,6 +129,7 @@ export declare const ToggleGroupKey: InjectionKey<{
|
|
|
117
129
|
toggle: (v: string) => void;
|
|
118
130
|
}>;
|
|
119
131
|
export declare const ToggleGroupRoot: import("sinwan/component").SinwanComponent<{
|
|
132
|
+
[key: `aria-${string}`]: string | boolean | undefined;
|
|
120
133
|
children?: SinwanNode;
|
|
121
134
|
type?: "single" | "multiple";
|
|
122
135
|
value?: string | string[];
|
package/dist/controls.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
getFocusableElements2,
|
|
27
27
|
trapFocus2
|
|
28
28
|
} from "./chunks/chunk-ejwby1pn.js";
|
|
29
|
+
import {
|
|
30
|
+
DirectionKey2,
|
|
31
|
+
DirectionProvider2,
|
|
32
|
+
useDirection2
|
|
33
|
+
} from "./chunks/chunk-f6hw8ash.js";
|
|
29
34
|
import {
|
|
30
35
|
For2,
|
|
31
36
|
TabsKey2,
|
|
@@ -48,12 +53,7 @@ import {
|
|
|
48
53
|
ToggleGroupKey2,
|
|
49
54
|
ToggleGroupRoot2,
|
|
50
55
|
ToggleGroupItem2
|
|
51
|
-
} from "./chunks/chunk-
|
|
52
|
-
import {
|
|
53
|
-
DirectionKey2,
|
|
54
|
-
DirectionProvider2,
|
|
55
|
-
useDirection2
|
|
56
|
-
} from "./chunks/chunk-f6hw8ash.js";
|
|
56
|
+
} from "./chunks/chunk-s03me75x.js";
|
|
57
57
|
import {
|
|
58
58
|
isDismissExemptPointerTarget2
|
|
59
59
|
} from "./chunks/chunk-9whagkb6.js";
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
ThemeProvider2,
|
|
90
90
|
useTheme2,
|
|
91
91
|
ThemeToggle2
|
|
92
|
-
} from "./chunks/chunk-
|
|
92
|
+
} from "./chunks/chunk-33f19xws.js";
|
|
93
93
|
export {
|
|
94
94
|
AccordionContent2 as AccordionContent,
|
|
95
95
|
AccordionItem2 as AccordionItem,
|
package/dist/theme.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sinwan-ui-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Headless behaviour primitives for Sinwan UI components: overlays, floating positioning, dismiss layers, controls, slots, theme and direction.",
|
|
5
5
|
"author": "Mohammed Ben Cheikh",
|
|
6
6
|
"license": "MIT",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@types/bun": "latest",
|
|
110
|
-
"bun-plugin-sinwan": "1.0.
|
|
110
|
+
"bun-plugin-sinwan": "^1.0.1",
|
|
111
111
|
"happy-dom": "^20.14.5",
|
|
112
112
|
"sinwan": "^2.0.3",
|
|
113
113
|
"typescript": "^5.9.3"
|
|
@@ -115,4 +115,4 @@
|
|
|
115
115
|
"peerDependencies": {
|
|
116
116
|
"sinwan": ">=2.0.3 <3.0.0"
|
|
117
117
|
}
|
|
118
|
-
}
|
|
118
|
+
}
|