@adecore/ui 0.21.2 → 0.22.1
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/dist/Segmented.d.ts +2 -1
- package/dist/Segmented.js +25 -3
- package/package.json +1 -2
- package/src/Segmented.tsx +42 -7
package/dist/Segmented.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface SegmentedProps<T extends string> {
|
|
|
11
11
|
options: readonly SegmentedOption<T>[];
|
|
12
12
|
label: string;
|
|
13
13
|
disabled?: boolean;
|
|
14
|
+
orientation?: 'horizontal' | 'vertical';
|
|
14
15
|
className?: string;
|
|
15
16
|
ref?: Ref<HTMLDivElement>;
|
|
16
17
|
}
|
|
17
|
-
export declare function Segmented<T extends string>({ value, onValueChange, options, label, disabled, className, ref }: SegmentedProps<T>): import("react").JSX.Element;
|
|
18
|
+
export declare function Segmented<T extends string>({ value, onValueChange, options, label, disabled, orientation, className, ref }: SegmentedProps<T>): import("react").JSX.Element;
|
package/dist/Segmented.js
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { Icon } from './Icon.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const STEPS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
5
|
+
/*
|
|
6
|
+
* One of a few options side by side in one sunken track, the picked one lifted out of it. A radio
|
|
7
|
+
* group: one tab stop, and the arrows pick and focus the next option, around the ends.
|
|
8
|
+
*/
|
|
9
|
+
export function Segmented({ value, onValueChange, options, label, disabled, orientation = 'horizontal', className, ref }) {
|
|
10
|
+
const vertical = orientation === 'vertical';
|
|
11
|
+
const picked = options.findIndex((option) => option.id === value);
|
|
12
|
+
const stop = picked === -1 ? 0 : picked;
|
|
13
|
+
function move(event, index) {
|
|
14
|
+
const next = event.key in STEPS
|
|
15
|
+
? (index + STEPS[event.key] + options.length) % options.length
|
|
16
|
+
: event.key === 'Home'
|
|
17
|
+
? 0
|
|
18
|
+
: event.key === 'End'
|
|
19
|
+
? options.length - 1
|
|
20
|
+
: null;
|
|
21
|
+
if (next === null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
onValueChange(options[next].id);
|
|
26
|
+
event.currentTarget.parentElement?.querySelectorAll('[role="radio"]')[next]?.focus();
|
|
27
|
+
}
|
|
28
|
+
return (_jsx("div", { ref: ref, className: clsx('flex rounded-lg bg-surface-sunken p-0.5 text-xs font-medium', vertical ? 'w-full flex-col items-stretch gap-0.5' : 'min-h-8 w-fit items-center', className), role: "radiogroup", "aria-label": label, "aria-orientation": vertical ? 'vertical' : undefined, children: options.map((option, index) => (_jsxs("button", { type: "button", role: "radio", "aria-checked": value === option.id, tabIndex: index === stop ? 0 : -1, disabled: disabled, className: clsx('flex min-h-7 items-center gap-1.5 rounded-md px-3 py-1 text-left transition-colors disabled:opacity-50', value === option.id ? 'bg-surface-raised text-text shadow-raised' : 'text-text-muted hover:text-text'), onClick: () => onValueChange(option.id), onKeyDown: (event) => move(event, index), children: [option.icon && _jsx(Icon, { icon: option.icon, size: 14, className: "shrink-0" }), option.label] }, option.id))) }));
|
|
7
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
@@ -85,7 +85,6 @@
|
|
|
85
85
|
"@types/react-dom": "^19.3.0",
|
|
86
86
|
"i18next": "^26.4.2",
|
|
87
87
|
"opentype.js": "2.0.0",
|
|
88
|
-
"oxc-parser": "^0.151.0",
|
|
89
88
|
"react": "^19.3.0",
|
|
90
89
|
"react-dom": "^19.3.0",
|
|
91
90
|
"react-i18next": "^17.0.15",
|
package/src/Segmented.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ref } from 'react';
|
|
1
|
+
import type { KeyboardEvent, Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
4
|
import { Icon } from './Icon.tsx';
|
|
@@ -16,33 +16,68 @@ export interface SegmentedProps<T extends string> {
|
|
|
16
16
|
/* The accessible name of the group. */
|
|
17
17
|
label: string;
|
|
18
18
|
disabled?: boolean;
|
|
19
|
+
/* `vertical` stacks the options in a column as wide as its container, for labels too long to stand side by side. */
|
|
20
|
+
orientation?: 'horizontal' | 'vertical';
|
|
19
21
|
className?: string;
|
|
20
22
|
ref?: Ref<HTMLDivElement>;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
const STEPS: Readonly<Record<string, number>> = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* One of a few options side by side in one sunken track, the picked one lifted out of it. A radio
|
|
29
|
+
* group: one tab stop, and the arrows pick and focus the next option, around the ends.
|
|
30
|
+
*/
|
|
31
|
+
export function Segmented<T extends string>({ value, onValueChange, options, label, disabled, orientation = 'horizontal', className, ref }: SegmentedProps<T>) {
|
|
32
|
+
const vertical = orientation === 'vertical';
|
|
33
|
+
const picked = options.findIndex((option) => option.id === value);
|
|
34
|
+
const stop = picked === -1 ? 0 : picked;
|
|
35
|
+
|
|
36
|
+
function move(event: KeyboardEvent<HTMLButtonElement>, index: number) {
|
|
37
|
+
const next =
|
|
38
|
+
event.key in STEPS
|
|
39
|
+
? (index + STEPS[event.key]! + options.length) % options.length
|
|
40
|
+
: event.key === 'Home'
|
|
41
|
+
? 0
|
|
42
|
+
: event.key === 'End'
|
|
43
|
+
? options.length - 1
|
|
44
|
+
: null;
|
|
45
|
+
if (next === null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
onValueChange(options[next]!.id);
|
|
50
|
+
event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role="radio"]')[next]?.focus();
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
return (
|
|
26
54
|
<div
|
|
27
55
|
ref={ref}
|
|
28
|
-
className={clsx(
|
|
56
|
+
className={clsx(
|
|
57
|
+
'flex rounded-lg bg-surface-sunken p-0.5 text-xs font-medium',
|
|
58
|
+
vertical ? 'w-full flex-col items-stretch gap-0.5' : 'min-h-8 w-fit items-center',
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
29
61
|
role="radiogroup"
|
|
30
62
|
aria-label={label}
|
|
63
|
+
aria-orientation={vertical ? 'vertical' : undefined}
|
|
31
64
|
>
|
|
32
|
-
{options.map((option) => (
|
|
65
|
+
{options.map((option, index) => (
|
|
33
66
|
<button
|
|
34
67
|
key={option.id}
|
|
35
68
|
type="button"
|
|
36
69
|
role="radio"
|
|
37
70
|
aria-checked={value === option.id}
|
|
71
|
+
tabIndex={index === stop ? 0 : -1}
|
|
38
72
|
disabled={disabled}
|
|
39
73
|
className={clsx(
|
|
40
|
-
'flex h-7 items-center gap-1.5 rounded-md px-3 transition-colors disabled:opacity-50',
|
|
74
|
+
'flex min-h-7 items-center gap-1.5 rounded-md px-3 py-1 text-left transition-colors disabled:opacity-50',
|
|
41
75
|
value === option.id ? 'bg-surface-raised text-text shadow-raised' : 'text-text-muted hover:text-text'
|
|
42
76
|
)}
|
|
43
77
|
onClick={() => onValueChange(option.id)}
|
|
78
|
+
onKeyDown={(event) => move(event, index)}
|
|
44
79
|
>
|
|
45
|
-
{option.icon && <Icon icon={option.icon} size={14} />}
|
|
80
|
+
{option.icon && <Icon icon={option.icon} size={14} className="shrink-0" />}
|
|
46
81
|
{option.label}
|
|
47
82
|
</button>
|
|
48
83
|
))}
|