sinwan-ui-core 1.1.1 → 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 +7 -0
- package/README.md +1 -1
- package/dist/chunks/{chunk-2sw6berz.js → chunk-s03me75x.js} +68 -4
- package/dist/controls.d.ts +12 -0
- package/dist/controls.js +2 -1
- package/dist/index.js +6 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
|
|
5
12
|
## [1.1.1]
|
|
6
13
|
|
|
7
14
|
### Fixed
|
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 |
|
|
@@ -1,6 +1,9 @@
|
|
|
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";
|
|
@@ -9,6 +12,7 @@ import {
|
|
|
9
12
|
cc,
|
|
10
13
|
getRawProps,
|
|
11
14
|
inject,
|
|
15
|
+
onUnmounted,
|
|
12
16
|
provide
|
|
13
17
|
} from "sinwan/component";
|
|
14
18
|
import { For as For2, Show as Show2 } from "sinwan/control-flow";
|
|
@@ -103,14 +107,17 @@ var _$tmpl_8 = {
|
|
|
103
107
|
]
|
|
104
108
|
};
|
|
105
109
|
var _$tmpl_9 = {
|
|
106
|
-
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>',
|
|
107
111
|
slots: [
|
|
112
|
+
{ path: [], type: "ref", name: "ref" },
|
|
108
113
|
{ path: [], type: "attr", name: "id" },
|
|
109
114
|
{ path: [], type: "attr", name: "data-state" },
|
|
110
115
|
{ path: [], type: "attr", name: "aria-checked" },
|
|
116
|
+
{ path: [], type: "attr", name: "tabIndex" },
|
|
111
117
|
{ path: [], type: "attr", name: "disabled" },
|
|
112
118
|
{ path: [], type: "attr", name: "class" },
|
|
113
119
|
{ path: [], type: "event", name: "onclick" },
|
|
120
|
+
{ path: [], type: "event", name: "onkeydown" },
|
|
114
121
|
{ path: [], type: "child" }
|
|
115
122
|
]
|
|
116
123
|
};
|
|
@@ -335,6 +342,10 @@ var SwitchRoot2 = cc((props) => {
|
|
|
335
342
|
]);
|
|
336
343
|
});
|
|
337
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
|
+
}
|
|
338
349
|
var RadioGroupRoot2 = cc((props) => {
|
|
339
350
|
const valueLocal = signal(props.defaultValue ?? "");
|
|
340
351
|
const value = "value" in props ? computed(() => props.value ?? "") : valueLocal;
|
|
@@ -342,28 +353,81 @@ var RadioGroupRoot2 = cc((props) => {
|
|
|
342
353
|
if (!("value" in props))
|
|
343
354
|
valueLocal.value = next;
|
|
344
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
|
+
});
|
|
345
366
|
provide(RadioGroupKey2, {
|
|
346
367
|
value,
|
|
347
368
|
name: props.name,
|
|
348
|
-
setValue
|
|
349
|
-
|
|
350
|
-
|
|
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());
|
|
351
385
|
}
|
|
352
386
|
});
|
|
353
387
|
return _$createTemplate(_$tmpl_8, [() => _$unwrap(props).class, () => _$unwrap(props).children]);
|
|
354
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
|
+
}
|
|
355
399
|
var RadioGroupItem2 = cc((props) => {
|
|
356
400
|
const api = inject(RadioGroupKey2);
|
|
401
|
+
const direction = inject(DirectionKey2, LTR);
|
|
402
|
+
let unregister;
|
|
403
|
+
onUnmounted(() => unregister?.());
|
|
357
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
|
+
},
|
|
358
414
|
() => _$unwrap(props).id,
|
|
359
415
|
() => api.value.value === _$unwrap(props).value ? "checked" : "unchecked",
|
|
360
416
|
() => api.value.value === _$unwrap(props).value,
|
|
417
|
+
() => api.tabStop.value === _$unwrap(props).value ? 0 : -1,
|
|
361
418
|
() => _$unwrap(props).disabled,
|
|
362
419
|
() => _$unwrap(props).class,
|
|
363
420
|
() => {
|
|
364
421
|
if (!props.disabled)
|
|
365
422
|
api.setValue(props.value);
|
|
366
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
|
+
},
|
|
367
431
|
() => _$unwrap(props).children
|
|
368
432
|
]);
|
|
369
433
|
});
|
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;
|
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";
|
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",
|