@zylem/ui 0.8.13 → 0.9.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/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +4 -1
- package/src/components/BuildLabel/BuildLabel.tsx +0 -1
- package/src/components/BuildLabel/mount-build-label.ts +0 -1
- package/src/components/Callout/Callout.tsx +1 -1
- package/src/components/Card/Card.css.ts +1 -0
- package/src/components/Card/Card.tsx +11 -2
- package/src/components/Checkbox/Checkbox.tsx +4 -2
- package/src/components/ColorPalette/ColorPalette.tsx +1 -1
- package/src/components/Console/Console.tsx +3 -0
- package/src/components/Dialog/Dialog.css.ts +41 -17
- package/src/components/Dialog/Dialog.tsx +134 -53
- package/src/components/FrameStrip/FrameStrip.tsx +2 -3
- package/src/components/ItemPicker/ItemPicker.tsx +46 -33
- package/src/components/MenuButton/MenuButton.tsx +1 -0
- package/src/components/Menubar/Menubar.css.ts +29 -29
- package/src/components/Mixer/Knob.tsx +10 -3
- package/src/components/Mixer/Mixer.css.ts +19 -0
- package/src/components/Mixer/MixerStrip.tsx +29 -8
- package/src/components/NumberField/NumberField.tsx +5 -2
- package/src/components/Panel/Panel.css.ts +1 -0
- package/src/components/Panel/Panel.tsx +9 -1
- package/src/components/PianoRoll/PianoRoll.tsx +1 -0
- package/src/components/Progress/Progress.tsx +3 -0
- package/src/components/Property/Property.css.ts +13 -0
- package/src/components/Property/Property.tsx +3 -2
- package/src/components/SearchInput/SearchInput.tsx +1 -1
- package/src/components/Select/Select.tsx +3 -1
- package/src/components/Sidebar/Sidebar.css.ts +1 -0
- package/src/components/Sidebar/Sidebar.tsx +23 -5
- package/src/components/Slider/Slider.tsx +4 -2
- package/src/components/Switch/Switch.tsx +3 -1
- package/src/components/Tabs/Tabs.css.ts +0 -5
- package/src/components/Tabs/Tabs.tsx +23 -12
- package/src/components/TextField/TextField.tsx +4 -0
- package/src/components/TimelineLanes/TimelineLanes.css.ts +6 -0
- package/src/components/TimelineLanes/TimelineLanes.tsx +52 -4
- package/src/components/TimelineLanes/lanes-math.ts +51 -0
- package/src/components/TimelineTrack/TimelineTrack.tsx +16 -3
- package/src/components/TitleBar/TitleBar.css.ts +1 -0
- package/src/components/TitleBar/TitleBar.tsx +7 -1
- package/src/components/Toast/Toast.tsx +1 -2
- package/src/components/ToolButton/ToolButton.tsx +1 -1
- package/src/components/Toolbar/Toolbar.tsx +6 -1
- package/src/components/ToolbarButton/ToolbarButton.tsx +9 -2
- package/src/components/ToolboxButton/ToolboxButton.tsx +2 -1
- package/src/components/TrackHeader/TrackHeader.css.ts +21 -0
- package/src/components/TrackHeader/TrackHeader.tsx +37 -12
- package/src/components/TransportBar/TransportBar.tsx +4 -1
- package/src/components/Waveform/Waveform.css.ts +10 -0
- package/src/components/Waveform/Waveform.tsx +59 -27
- package/src/components/WindowControls/WindowControls.tsx +4 -4
- package/src/components/index.ts +1 -0
- package/src/components/shared/Heading.tsx +30 -0
- package/src/global/accordion.css.ts +6 -1
- package/src/global/common.css.ts +13 -0
- package/src/layers/LayerContext.tsx +2 -2
|
@@ -2,51 +2,51 @@ import { globalStyle } from '@vanilla-extract/css';
|
|
|
2
2
|
import { vars } from '../../theme.css';
|
|
3
3
|
|
|
4
4
|
globalStyle('.zylem-menubar', {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
gap: '2px',
|
|
8
|
+
minWidth: 0,
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
// Flat chrome (see DESIGN.md): transparent at rest, a tint on hover and a
|
|
12
12
|
// stronger tint while the menu is open. No gloss, so the bar's glossy title
|
|
13
13
|
// surfaces and primary buttons stay the focal points.
|
|
14
14
|
globalStyle('.zylem-menubar-trigger', {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
display: 'inline-flex',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
gap: vars.spacing.xs,
|
|
18
|
+
padding: `${vars.spacing.xs} ${vars.spacing.sm}`,
|
|
19
|
+
border: '1px solid transparent',
|
|
20
|
+
borderRadius: vars.radii.button,
|
|
21
|
+
background: 'transparent',
|
|
22
|
+
color: vars.colors.text,
|
|
23
|
+
fontFamily: vars.typography.fontFamily,
|
|
24
|
+
fontSize: `calc(${vars.typography.fontSize} - 1px)`,
|
|
25
|
+
fontWeight: 600,
|
|
26
|
+
letterSpacing: '0.02em',
|
|
27
|
+
lineHeight: 1.2,
|
|
28
|
+
cursor: 'pointer',
|
|
29
|
+
userSelect: 'none',
|
|
30
|
+
outline: 'none',
|
|
31
|
+
transition: `background ${vars.motion.fast} ${vars.motion.easeOut}, color ${vars.motion.fast} ${vars.motion.easeOut}`,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
globalStyle('.zylem-menubar-trigger:hover', {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
background: 'rgba(97, 166, 232, 0.12)',
|
|
36
|
+
color: vars.colors.primaryHover,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
globalStyle('.zylem-menubar-trigger[data-expanded]', {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
background: 'rgba(97, 166, 232, 0.22)',
|
|
41
|
+
borderColor: 'rgba(97, 166, 232, 0.35)',
|
|
42
|
+
color: '#F4FAFF',
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
globalStyle('.zylem-menubar-trigger:focus-visible', {
|
|
46
|
-
|
|
46
|
+
boxShadow: vars.effects.focusRing,
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
globalStyle('.zylem-menubar-trigger[data-disabled]', {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
opacity: 0.45,
|
|
51
|
+
cursor: 'not-allowed',
|
|
52
52
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, Show } from 'solid-js';
|
|
1
|
+
import { createSignal, createUniqueId, Show } from 'solid-js';
|
|
2
2
|
import { clamp } from '../TimelineLanes/lanes-math';
|
|
3
3
|
|
|
4
4
|
export interface KnobProps {
|
|
@@ -11,7 +11,10 @@ export interface KnobProps {
|
|
|
11
11
|
step?: number;
|
|
12
12
|
/** Double-click resets to this. */
|
|
13
13
|
defaultValue?: number;
|
|
14
|
+
/** Caption under the knob, which also names it. */
|
|
14
15
|
label?: string;
|
|
16
|
+
/** Accessible name when there is no visible `label`. */
|
|
17
|
+
'aria-label'?: string;
|
|
15
18
|
/** Readout under the knob; defaults to the value with `step` precision. */
|
|
16
19
|
format?: (value: number) => string;
|
|
17
20
|
/** Draw the arc from the centre (for pan and other ±ranges). */
|
|
@@ -48,6 +51,7 @@ export function Knob(props: KnobProps) {
|
|
|
48
51
|
const max = () => props.max ?? 1;
|
|
49
52
|
const step = () => props.step ?? (max() - min()) / 100;
|
|
50
53
|
const size = () => props.size ?? 36;
|
|
54
|
+
const labelId = createUniqueId();
|
|
51
55
|
const ratio = () => (max() > min() ? clamp((props.value - min()) / (max() - min()), 0, 1) : 0);
|
|
52
56
|
const angle = () => START_ANGLE + ratio() * SWEEP;
|
|
53
57
|
const zeroAngle = () =>
|
|
@@ -147,7 +151,8 @@ export function Knob(props: KnobProps) {
|
|
|
147
151
|
class="zylem-knob-dial"
|
|
148
152
|
role="slider"
|
|
149
153
|
tabIndex={props.disabled ? -1 : 0}
|
|
150
|
-
aria-
|
|
154
|
+
aria-labelledby={props.label ? labelId : undefined}
|
|
155
|
+
aria-label={props.label ? undefined : props['aria-label']}
|
|
151
156
|
aria-valuemin={min()}
|
|
152
157
|
aria-valuemax={max()}
|
|
153
158
|
aria-valuenow={props.value}
|
|
@@ -180,7 +185,9 @@ export function Knob(props: KnobProps) {
|
|
|
180
185
|
</svg>
|
|
181
186
|
</div>
|
|
182
187
|
<Show when={props.label}>
|
|
183
|
-
<span class="zylem-knob-label">
|
|
188
|
+
<span id={labelId} class="zylem-knob-label">
|
|
189
|
+
{props.label}
|
|
190
|
+
</span>
|
|
184
191
|
</Show>
|
|
185
192
|
<span class="zylem-knob-value">{readout()}</span>
|
|
186
193
|
</div>
|
|
@@ -236,6 +236,25 @@ globalStyle('.zylem-mixer-name', {
|
|
|
236
236
|
textOverflow: 'ellipsis',
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// A selectable strip's name is a button that still reads as the name.
|
|
240
|
+
globalStyle('button.zylem-mixer-name', {
|
|
241
|
+
appearance: 'none',
|
|
242
|
+
margin: 0,
|
|
243
|
+
padding: 0,
|
|
244
|
+
border: 0,
|
|
245
|
+
background: 'none',
|
|
246
|
+
color: 'inherit',
|
|
247
|
+
fontFamily: 'inherit',
|
|
248
|
+
lineHeight: 'inherit',
|
|
249
|
+
cursor: 'inherit',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
globalStyle('button.zylem-mixer-name:focus-visible', {
|
|
253
|
+
outline: 'none',
|
|
254
|
+
borderRadius: '3px',
|
|
255
|
+
boxShadow: vars.effects.focusRing,
|
|
256
|
+
});
|
|
257
|
+
|
|
239
258
|
globalStyle('.zylem-mixer-strip[data-muted] .zylem-mixer-name', {
|
|
240
259
|
color: vars.colors.textSecondary,
|
|
241
260
|
});
|
|
@@ -26,6 +26,7 @@ export interface MixerStripProps {
|
|
|
26
26
|
/** Wider strip with a stronger label; for the master bus. */
|
|
27
27
|
master?: boolean;
|
|
28
28
|
selected?: boolean;
|
|
29
|
+
/** Pointer down anywhere on the strip; given, the name is also a button for the keyboard. */
|
|
29
30
|
onSelect?: () => void;
|
|
30
31
|
disabled?: boolean;
|
|
31
32
|
class?: string;
|
|
@@ -52,6 +53,7 @@ export function formatPan(value: number): string {
|
|
|
52
53
|
export function MixerStrip(props: MixerStripProps) {
|
|
53
54
|
const min = () => props.minVolume ?? -60;
|
|
54
55
|
const max = () => props.maxVolume ?? 6;
|
|
56
|
+
const volumeText = () => formatDb(clamp(props.volume, min(), max()), min());
|
|
55
57
|
|
|
56
58
|
return (
|
|
57
59
|
<div
|
|
@@ -61,6 +63,8 @@ export function MixerStrip(props: MixerStripProps) {
|
|
|
61
63
|
data-selected={props.selected ? '' : undefined}
|
|
62
64
|
data-disabled={props.disabled ? '' : undefined}
|
|
63
65
|
data-muted={props.muted ? '' : undefined}
|
|
66
|
+
role="group"
|
|
67
|
+
aria-label={props.name}
|
|
64
68
|
onPointerDown={() => props.onSelect?.()}
|
|
65
69
|
>
|
|
66
70
|
<Show when={props.pan !== undefined}>
|
|
@@ -91,14 +95,13 @@ export function MixerStrip(props: MixerStripProps) {
|
|
|
91
95
|
disabled={props.disabled}
|
|
92
96
|
onChange={(values) => props.onVolumeChange?.(values[0] ?? min())}
|
|
93
97
|
onChangeEnd={(values) => props.onVolumeCommit?.(values[0] ?? min())}
|
|
94
|
-
getValueLabel={(params) => formatDb(params.values[0] ?? min(), min())}
|
|
95
98
|
>
|
|
96
99
|
<KSlider.Label class="zylem-fader-sr">{`${props.name} volume`}</KSlider.Label>
|
|
97
100
|
<KSlider.Track class="zylem-fader-track">
|
|
98
101
|
<KSlider.Fill class="zylem-fader-fill" />
|
|
99
102
|
<span class="zylem-fader-zero" aria-hidden="true" />
|
|
100
|
-
<KSlider.Thumb class="zylem-fader-thumb">
|
|
101
|
-
<KSlider.Input class="zylem-fader-sr" />
|
|
103
|
+
<KSlider.Thumb class="zylem-fader-thumb" aria-valuetext={volumeText()}>
|
|
104
|
+
<KSlider.Input class="zylem-fader-sr" aria-valuetext={volumeText()} />
|
|
102
105
|
</KSlider.Thumb>
|
|
103
106
|
</KSlider.Track>
|
|
104
107
|
</KSlider.Root>
|
|
@@ -117,7 +120,7 @@ export function MixerStrip(props: MixerStripProps) {
|
|
|
117
120
|
class="zylem-track-toggle"
|
|
118
121
|
data-kind="mute"
|
|
119
122
|
aria-pressed={props.muted ? 'true' : 'false'}
|
|
120
|
-
aria-label=
|
|
123
|
+
aria-label="Mute"
|
|
121
124
|
title={props.muted ? 'Unmute' : 'Mute'}
|
|
122
125
|
disabled={props.disabled}
|
|
123
126
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -132,7 +135,7 @@ export function MixerStrip(props: MixerStripProps) {
|
|
|
132
135
|
class="zylem-track-toggle"
|
|
133
136
|
data-kind="solo"
|
|
134
137
|
aria-pressed={props.solo ? 'true' : 'false'}
|
|
135
|
-
aria-label=
|
|
138
|
+
aria-label="Solo"
|
|
136
139
|
title={props.solo ? 'Unsolo' : 'Solo'}
|
|
137
140
|
disabled={props.disabled}
|
|
138
141
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -142,9 +145,27 @@ export function MixerStrip(props: MixerStripProps) {
|
|
|
142
145
|
</button>
|
|
143
146
|
</Show>
|
|
144
147
|
</div>
|
|
145
|
-
<
|
|
146
|
-
{props.
|
|
147
|
-
|
|
148
|
+
<Show
|
|
149
|
+
when={props.onSelect}
|
|
150
|
+
fallback={
|
|
151
|
+
<span class="zylem-mixer-name" title={props.name}>
|
|
152
|
+
{props.name}
|
|
153
|
+
</span>
|
|
154
|
+
}
|
|
155
|
+
>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
class="zylem-mixer-name"
|
|
159
|
+
aria-current={props.selected ? 'true' : undefined}
|
|
160
|
+
title={props.name}
|
|
161
|
+
onClick={(event) => {
|
|
162
|
+
// The pointer selected on pointer down; keys and assistive tech click without one.
|
|
163
|
+
if (event.detail === 0) props.onSelect?.();
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{props.name}
|
|
167
|
+
</button>
|
|
168
|
+
</Show>
|
|
148
169
|
</div>
|
|
149
170
|
);
|
|
150
171
|
}
|
|
@@ -3,6 +3,8 @@ import { Show } from 'solid-js';
|
|
|
3
3
|
|
|
4
4
|
export interface NumberFieldProps {
|
|
5
5
|
label?: string;
|
|
6
|
+
/** Accessible name when there is no visible `label`. */
|
|
7
|
+
'aria-label'?: string;
|
|
6
8
|
error?: string;
|
|
7
9
|
value?: number;
|
|
8
10
|
onChange?: (value: number) => void;
|
|
@@ -45,19 +47,20 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
45
47
|
<div class="zylem-numberfield-group">
|
|
46
48
|
<KNumberField.Input
|
|
47
49
|
class="zylem-field zylem-numberfield-input"
|
|
50
|
+
aria-label={props['aria-label']}
|
|
48
51
|
placeholder={props.placeholder}
|
|
49
52
|
/>
|
|
50
53
|
<KNumberField.IncrementTrigger
|
|
51
54
|
class="zylem-numberfield-stepper zylem-numberfield-stepper--up"
|
|
52
55
|
aria-label="Increment"
|
|
53
56
|
>
|
|
54
|
-
|
|
57
|
+
<span aria-hidden="true">▲</span>
|
|
55
58
|
</KNumberField.IncrementTrigger>
|
|
56
59
|
<KNumberField.DecrementTrigger
|
|
57
60
|
class="zylem-numberfield-stepper zylem-numberfield-stepper--down"
|
|
58
61
|
aria-label="Decrement"
|
|
59
62
|
>
|
|
60
|
-
|
|
63
|
+
<span aria-hidden="true">▼</span>
|
|
61
64
|
</KNumberField.DecrementTrigger>
|
|
62
65
|
</div>
|
|
63
66
|
<Show when={props.error}>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Show, splitProps } from 'solid-js';
|
|
2
2
|
import type { JSX } from 'solid-js';
|
|
3
|
+
import { Heading, type HeadingLevel } from '../shared/Heading';
|
|
3
4
|
|
|
4
5
|
export interface PanelProps
|
|
5
6
|
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
6
7
|
/** Optional glossy header title. */
|
|
7
8
|
title?: JSX.Element;
|
|
9
|
+
/** Heading level of the title (default 2). */
|
|
10
|
+
headingLevel?: HeadingLevel;
|
|
8
11
|
/** Extra content rendered on the right side of the header. */
|
|
9
12
|
headerActions?: JSX.Element;
|
|
10
13
|
/** Use the brighter window-glass treatment. */
|
|
@@ -28,6 +31,7 @@ export interface PanelProps
|
|
|
28
31
|
export function Panel(props: PanelProps) {
|
|
29
32
|
const [local, rest] = splitProps(props, [
|
|
30
33
|
'title',
|
|
34
|
+
'headingLevel',
|
|
31
35
|
'headerActions',
|
|
32
36
|
'window',
|
|
33
37
|
'flush',
|
|
@@ -51,7 +55,11 @@ export function Panel(props: PanelProps) {
|
|
|
51
55
|
<div class={classes()} {...rest}>
|
|
52
56
|
<Show when={local.title || local.headerActions}>
|
|
53
57
|
<div class="zylem-panel-header">
|
|
54
|
-
<span class="zylem-panel-header-title"
|
|
58
|
+
<Show when={local.title} fallback={<span class="zylem-panel-header-title" />}>
|
|
59
|
+
<Heading level={local.headingLevel ?? 2} class="zylem-panel-header-title">
|
|
60
|
+
{local.title}
|
|
61
|
+
</Heading>
|
|
62
|
+
</Show>
|
|
55
63
|
{local.headerActions}
|
|
56
64
|
</div>
|
|
57
65
|
</Show>
|
|
@@ -219,6 +219,7 @@ export function PianoRoll(props: PianoRollProps) {
|
|
|
219
219
|
tabIndex={0}
|
|
220
220
|
role="application"
|
|
221
221
|
aria-label="Piano roll"
|
|
222
|
+
aria-disabled={props.disabled ? 'true' : undefined}
|
|
222
223
|
data-disabled={props.disabled ? '' : undefined}
|
|
223
224
|
data-dragging={dragging() ? '' : undefined}
|
|
224
225
|
onKeyDown={handleKeyDown}
|
|
@@ -6,6 +6,8 @@ export interface ProgressProps {
|
|
|
6
6
|
minValue?: number;
|
|
7
7
|
maxValue?: number;
|
|
8
8
|
label?: string;
|
|
9
|
+
/** Accessible name when there is no visible `label`. */
|
|
10
|
+
'aria-label'?: string;
|
|
9
11
|
showValue?: boolean;
|
|
10
12
|
indeterminate?: boolean;
|
|
11
13
|
class?: string;
|
|
@@ -22,6 +24,7 @@ export function Progress(props: ProgressProps) {
|
|
|
22
24
|
minValue={props.minValue}
|
|
23
25
|
maxValue={props.maxValue}
|
|
24
26
|
indeterminate={props.indeterminate}
|
|
27
|
+
aria-label={props['aria-label']}
|
|
25
28
|
>
|
|
26
29
|
<Show when={props.label || props.showValue}>
|
|
27
30
|
<div class="zylem-progress-header">
|
|
@@ -57,13 +57,26 @@ globalStyle('.zylem-property-value', {
|
|
|
57
57
|
whiteSpace: 'nowrap',
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// A button that should read as the value text it stands in for.
|
|
60
61
|
globalStyle('.zylem-property-value--clickable', {
|
|
62
|
+
appearance: 'none',
|
|
63
|
+
margin: 0,
|
|
64
|
+
padding: 0,
|
|
65
|
+
border: 0,
|
|
66
|
+
background: 'none',
|
|
67
|
+
lineHeight: 'inherit',
|
|
68
|
+
textAlign: 'start',
|
|
61
69
|
cursor: 'pointer',
|
|
62
70
|
textDecoration: 'underline',
|
|
63
71
|
textDecorationStyle: 'dotted',
|
|
64
72
|
textUnderlineOffset: '2px',
|
|
65
73
|
});
|
|
66
74
|
|
|
75
|
+
globalStyle('.zylem-property-value--clickable:focus-visible', {
|
|
76
|
+
outline: 'none',
|
|
77
|
+
boxShadow: vars.effects.focusRing,
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
globalStyle('.zylem-property-value--clickable:hover', {
|
|
68
81
|
textDecorationStyle: 'solid',
|
|
69
82
|
color: vars.colors.active,
|
|
@@ -107,7 +107,8 @@ export function PropertyRow(props: PropertyRowProps) {
|
|
|
107
107
|
</span>
|
|
108
108
|
}
|
|
109
109
|
>
|
|
110
|
-
<
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
111
112
|
class={`zylem-property-value zylem-property-value--clickable ${props.valueClass ?? ''}`}
|
|
112
113
|
onClick={() => {
|
|
113
114
|
const fullValue = display().fullValue;
|
|
@@ -116,7 +117,7 @@ export function PropertyRow(props: PropertyRowProps) {
|
|
|
116
117
|
title="Click to reveal full path"
|
|
117
118
|
>
|
|
118
119
|
{display().element}
|
|
119
|
-
</
|
|
120
|
+
</button>
|
|
120
121
|
</Show>
|
|
121
122
|
</div>
|
|
122
123
|
);
|
|
@@ -18,7 +18,7 @@ export interface SearchInputProps {
|
|
|
18
18
|
export function SearchInput(props: SearchInputProps) {
|
|
19
19
|
return (
|
|
20
20
|
<div class={props.class ? `zylem-search ${props.class}` : 'zylem-search'}>
|
|
21
|
-
<Search class="zylem-search-icon" />
|
|
21
|
+
<Search class="zylem-search-icon" aria-hidden="true" />
|
|
22
22
|
<input
|
|
23
23
|
type="search"
|
|
24
24
|
ref={props.ref}
|
|
@@ -13,6 +13,8 @@ export interface SelectProps {
|
|
|
13
13
|
value?: string;
|
|
14
14
|
onChange?: (value: string | null) => void;
|
|
15
15
|
label?: string;
|
|
16
|
+
/** Accessible name when there is no visible `label`. */
|
|
17
|
+
'aria-label'?: string;
|
|
16
18
|
placeholder?: string;
|
|
17
19
|
disabled?: boolean;
|
|
18
20
|
name?: string;
|
|
@@ -55,7 +57,7 @@ export function Select(props: SelectProps) {
|
|
|
55
57
|
<KSelect.Trigger
|
|
56
58
|
ref={layer.anchorRef}
|
|
57
59
|
class="zylem-field zylem-select-trigger"
|
|
58
|
-
aria-label={props
|
|
60
|
+
aria-label={props['aria-label']}
|
|
59
61
|
>
|
|
60
62
|
<KSelect.Value<SelectOption> class="zylem-select-value">
|
|
61
63
|
{(state) => state.selectedOption()?.label}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { Show, splitProps } from 'solid-js';
|
|
1
|
+
import { createUniqueId, Show, splitProps } from 'solid-js';
|
|
2
2
|
import type { JSX } from 'solid-js';
|
|
3
|
+
import { Dynamic } from 'solid-js/web';
|
|
4
|
+
import { Heading, type HeadingLevel } from '../shared/Heading';
|
|
3
5
|
|
|
4
6
|
export interface SidebarProps extends JSX.HTMLAttributes<HTMLElement> {
|
|
5
7
|
children?: JSX.Element;
|
|
8
|
+
/**
|
|
9
|
+
* Landmark element: `aside` (the default) for a rail beside the main
|
|
10
|
+
* content, `nav` when the rail is the page's navigation.
|
|
11
|
+
*/
|
|
12
|
+
as?: 'aside' | 'nav';
|
|
6
13
|
/**
|
|
7
14
|
* Drop the glass surface and trailing border so the list sits inside a
|
|
8
15
|
* panel that already draws them (an inspector rail, a dialog column).
|
|
@@ -16,34 +23,43 @@ export interface SidebarProps extends JSX.HTMLAttributes<HTMLElement> {
|
|
|
16
23
|
* and `SidebarItem`.
|
|
17
24
|
*/
|
|
18
25
|
export function Sidebar(props: SidebarProps) {
|
|
19
|
-
const [local, rest] = splitProps(props, ['children', 'class', 'borderless']);
|
|
26
|
+
const [local, rest] = splitProps(props, ['children', 'class', 'borderless', 'as']);
|
|
20
27
|
return (
|
|
21
|
-
<
|
|
28
|
+
<Dynamic
|
|
29
|
+
component={local.as ?? 'aside'}
|
|
22
30
|
class={['sidebar', 'zylem-scroll', local.borderless ? 'sidebar--borderless' : '', local.class ?? '']
|
|
23
31
|
.filter(Boolean)
|
|
24
32
|
.join(' ')}
|
|
25
33
|
{...rest}
|
|
26
34
|
>
|
|
27
35
|
{local.children}
|
|
28
|
-
</
|
|
36
|
+
</Dynamic>
|
|
29
37
|
);
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export interface SidebarSectionProps {
|
|
33
41
|
title?: JSX.Element;
|
|
42
|
+
/** Heading level of the title (default 2). */
|
|
43
|
+
headingLevel?: HeadingLevel;
|
|
34
44
|
children?: JSX.Element;
|
|
35
45
|
class?: string;
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
/** A titled group of items; the title names the group for assistive tech. */
|
|
38
49
|
export function SidebarSection(props: SidebarSectionProps) {
|
|
50
|
+
const titleId = createUniqueId();
|
|
39
51
|
return (
|
|
40
52
|
<div
|
|
41
53
|
class={
|
|
42
54
|
props.class ? `sidebar-section ${props.class}` : 'sidebar-section'
|
|
43
55
|
}
|
|
56
|
+
role={props.title ? 'group' : undefined}
|
|
57
|
+
aria-labelledby={props.title ? titleId : undefined}
|
|
44
58
|
>
|
|
45
59
|
<Show when={props.title}>
|
|
46
|
-
<
|
|
60
|
+
<Heading level={props.headingLevel ?? 2} id={titleId} class="sidebar-section-title">
|
|
61
|
+
{props.title}
|
|
62
|
+
</Heading>
|
|
47
63
|
</Show>
|
|
48
64
|
{props.children}
|
|
49
65
|
</div>
|
|
@@ -52,6 +68,7 @@ export function SidebarSection(props: SidebarSectionProps) {
|
|
|
52
68
|
|
|
53
69
|
export interface SidebarItemProps
|
|
54
70
|
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
71
|
+
/** The current item; exposed as `aria-current="true"`. */
|
|
55
72
|
active?: boolean;
|
|
56
73
|
children?: JSX.Element;
|
|
57
74
|
}
|
|
@@ -63,6 +80,7 @@ export function SidebarItem(props: SidebarItemProps) {
|
|
|
63
80
|
type="button"
|
|
64
81
|
class={local.class ? `sidebar-item ${local.class}` : 'sidebar-item'}
|
|
65
82
|
data-active={local.active ? '' : undefined}
|
|
83
|
+
aria-current={local.active ? 'true' : undefined}
|
|
66
84
|
{...rest}
|
|
67
85
|
>
|
|
68
86
|
{local.children}
|
|
@@ -3,6 +3,8 @@ import { Show } from 'solid-js';
|
|
|
3
3
|
|
|
4
4
|
export interface SliderProps {
|
|
5
5
|
label?: string;
|
|
6
|
+
/** Accessible name when there is no visible `label`. */
|
|
7
|
+
'aria-label'?: string;
|
|
6
8
|
value?: number;
|
|
7
9
|
onChange?: (value: number) => void;
|
|
8
10
|
/**
|
|
@@ -48,8 +50,8 @@ export function Slider(props: SliderProps) {
|
|
|
48
50
|
</Show>
|
|
49
51
|
<KSlider.Track class="zylem-slider-track">
|
|
50
52
|
<KSlider.Fill class="zylem-slider-fill" />
|
|
51
|
-
<KSlider.Thumb class="zylem-slider-thumb">
|
|
52
|
-
<KSlider.Input class="zylem-slider-thumb-input" />
|
|
53
|
+
<KSlider.Thumb class="zylem-slider-thumb" aria-label={props['aria-label']}>
|
|
54
|
+
<KSlider.Input class="zylem-slider-thumb-input" aria-label={props['aria-label']} />
|
|
53
55
|
</KSlider.Thumb>
|
|
54
56
|
</KSlider.Track>
|
|
55
57
|
</KSlider.Root>
|
|
@@ -3,6 +3,8 @@ import { Show } from 'solid-js';
|
|
|
3
3
|
|
|
4
4
|
export interface SwitchProps {
|
|
5
5
|
label?: string;
|
|
6
|
+
/** Accessible name when there is no visible `label`. */
|
|
7
|
+
'aria-label'?: string;
|
|
6
8
|
checked?: boolean;
|
|
7
9
|
defaultChecked?: boolean;
|
|
8
10
|
onChange?: (checked: boolean) => void;
|
|
@@ -24,7 +26,7 @@ export function Switch(props: SwitchProps) {
|
|
|
24
26
|
disabled={props.disabled}
|
|
25
27
|
name={props.name}
|
|
26
28
|
>
|
|
27
|
-
<KSwitch.Input class="zylem-switch-input" />
|
|
29
|
+
<KSwitch.Input class="zylem-switch-input" aria-label={props['aria-label']} />
|
|
28
30
|
<KSwitch.Control class="zylem-switch-control">
|
|
29
31
|
<KSwitch.Thumb class="zylem-switch-thumb" />
|
|
30
32
|
</KSwitch.Control>
|
|
@@ -102,11 +102,6 @@ globalStyle('.zylem-tabs-close:hover', {
|
|
|
102
102
|
color: '#FFF4F2',
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
globalStyle('.zylem-tabs-close:focus-visible', {
|
|
106
|
-
outline: 'none',
|
|
107
|
-
boxShadow: vars.effects.focusRing,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
105
|
globalStyle('.zylem-tabs-content', {
|
|
111
106
|
flex: 1,
|
|
112
107
|
minHeight: 0,
|
|
@@ -46,12 +46,12 @@ export interface TabsTriggerProps {
|
|
|
46
46
|
disabled?: boolean;
|
|
47
47
|
class?: string;
|
|
48
48
|
/**
|
|
49
|
-
* Document-style tabs:
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* Document-style tabs: Delete (Backspace on a Mac keyboard) closes the
|
|
50
|
+
* focused tab, and a close affordance after the label does the same for the
|
|
51
|
+
* pointer. Closing does not select the tab.
|
|
52
52
|
*/
|
|
53
53
|
onClose?: () => void;
|
|
54
|
-
/**
|
|
54
|
+
/** Tooltip of the close affordance; defaults to `Close tab`. */
|
|
55
55
|
closeLabel?: string;
|
|
56
56
|
/** Unsaved-changes marker before the close affordance. */
|
|
57
57
|
dirty?: boolean;
|
|
@@ -59,10 +59,22 @@ export interface TabsTriggerProps {
|
|
|
59
59
|
|
|
60
60
|
export function TabsTrigger(props: TabsTriggerProps) {
|
|
61
61
|
const close = (event: Event) => {
|
|
62
|
+
// Stopped so a page-level Delete shortcut does not also act on the key.
|
|
62
63
|
event.stopPropagation();
|
|
63
64
|
event.preventDefault();
|
|
64
65
|
props.onClose?.();
|
|
65
66
|
};
|
|
67
|
+
const closeFromKeyboard = (event: KeyboardEvent & { currentTarget: HTMLButtonElement }) => {
|
|
68
|
+
if (!props.onClose || (event.key !== 'Delete' && event.key !== 'Backspace')) return;
|
|
69
|
+
const trigger = event.currentTarget;
|
|
70
|
+
const list = trigger.closest('[role="tablist"]');
|
|
71
|
+
close(event);
|
|
72
|
+
// A closed tab usually leaves the document, and focus with it.
|
|
73
|
+
queueMicrotask(() => {
|
|
74
|
+
if (trigger.isConnected) return;
|
|
75
|
+
list?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')?.focus();
|
|
76
|
+
});
|
|
77
|
+
};
|
|
66
78
|
return (
|
|
67
79
|
<KTabs.Trigger
|
|
68
80
|
class={
|
|
@@ -70,25 +82,24 @@ export function TabsTrigger(props: TabsTriggerProps) {
|
|
|
70
82
|
}
|
|
71
83
|
value={props.value}
|
|
72
84
|
disabled={props.disabled}
|
|
85
|
+
aria-keyshortcuts={props.onClose ? 'Delete Backspace' : undefined}
|
|
86
|
+
onKeyDown={closeFromKeyboard}
|
|
73
87
|
>
|
|
74
88
|
<span class="zylem-tabs-trigger-label">{props.children}</span>
|
|
75
89
|
<Show when={props.dirty}>
|
|
76
|
-
<span class="zylem-tabs-dirty" aria-
|
|
90
|
+
<span class="zylem-tabs-dirty" aria-hidden="true" title="Unsaved changes" />
|
|
91
|
+
<span class="zylem-visually-hidden">(unsaved changes)</span>
|
|
77
92
|
</Show>
|
|
78
93
|
<Show when={props.onClose}>
|
|
94
|
+
{/* Pointer only: a button may not nest in the trigger button, and the key covers it. */}
|
|
79
95
|
<span
|
|
80
96
|
class="zylem-tabs-close"
|
|
81
|
-
|
|
82
|
-
tabIndex={0}
|
|
83
|
-
aria-label={props.closeLabel ?? 'Close tab'}
|
|
97
|
+
aria-hidden="true"
|
|
84
98
|
title={props.closeLabel ?? 'Close tab'}
|
|
85
99
|
onClick={close}
|
|
86
100
|
onPointerDown={(event) => event.stopPropagation()}
|
|
87
|
-
onKeyDown={(event) => {
|
|
88
|
-
if (event.key === 'Enter' || event.key === ' ') close(event);
|
|
89
|
-
}}
|
|
90
101
|
>
|
|
91
|
-
<X
|
|
102
|
+
<X />
|
|
92
103
|
</span>
|
|
93
104
|
</Show>
|
|
94
105
|
</KTabs.Trigger>
|
|
@@ -4,6 +4,8 @@ import type { JSX } from 'solid-js';
|
|
|
4
4
|
|
|
5
5
|
export interface TextFieldProps {
|
|
6
6
|
label?: string;
|
|
7
|
+
/** Accessible name when there is no visible `label`. */
|
|
8
|
+
'aria-label'?: string;
|
|
7
9
|
error?: string;
|
|
8
10
|
placeholder?: string;
|
|
9
11
|
value?: string;
|
|
@@ -92,6 +94,7 @@ export function TextField(props: TextFieldProps) {
|
|
|
92
94
|
<KTextField.Input
|
|
93
95
|
class="zylem-field zylem-textfield-input"
|
|
94
96
|
type={props.type ?? 'text'}
|
|
97
|
+
aria-label={props['aria-label']}
|
|
95
98
|
placeholder={props.placeholder}
|
|
96
99
|
autofocus={props.autofocus}
|
|
97
100
|
onInput={props.onInput as JSX.EventHandlerUnion<HTMLInputElement, InputEvent>}
|
|
@@ -102,6 +105,7 @@ export function TextField(props: TextFieldProps) {
|
|
|
102
105
|
>
|
|
103
106
|
<KTextField.TextArea
|
|
104
107
|
class="zylem-field zylem-textfield-textarea"
|
|
108
|
+
aria-label={props['aria-label']}
|
|
105
109
|
placeholder={props.placeholder}
|
|
106
110
|
rows={props.rows}
|
|
107
111
|
autofocus={props.autofocus}
|