@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
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Accordion as KAccordion } from '@kobalte/core';
|
|
2
2
|
import { For } from 'solid-js';
|
|
3
3
|
import type { JSX } from 'solid-js';
|
|
4
|
+
import { headingTag, type HeadingLevel } from '../shared/Heading';
|
|
4
5
|
|
|
5
6
|
export interface AccordionItemConfig {
|
|
6
7
|
id: string;
|
|
@@ -16,6 +17,8 @@ export interface AccordionProps {
|
|
|
16
17
|
onChange?: (value: string[]) => void;
|
|
17
18
|
defaultValue?: string[];
|
|
18
19
|
class?: string;
|
|
20
|
+
/** Heading level of each section header (default 3). */
|
|
21
|
+
headingLevel?: HeadingLevel;
|
|
19
22
|
/** Extra class applied to each content region (e.g. scroll helpers). */
|
|
20
23
|
contentClass?: string;
|
|
21
24
|
/**
|
|
@@ -47,7 +50,7 @@ export function Accordion(props: AccordionProps) {
|
|
|
47
50
|
<For each={props.items}>
|
|
48
51
|
{(item) => (
|
|
49
52
|
<KAccordion.Item value={item.id} class="accordion-item">
|
|
50
|
-
<KAccordion.Header class="accordion-header">
|
|
53
|
+
<KAccordion.Header as={headingTag(props.headingLevel ?? 3)} class="accordion-header">
|
|
51
54
|
<KAccordion.Trigger class="accordion-trigger">
|
|
52
55
|
{item.title}
|
|
53
56
|
</KAccordion.Trigger>
|
|
@@ -62,7 +62,6 @@ export function mountBuildLabel(
|
|
|
62
62
|
const el = document.createElement('div');
|
|
63
63
|
el.id = BUILD_LABEL_ID;
|
|
64
64
|
el.className = 'zylem-build-label';
|
|
65
|
-
el.setAttribute('aria-label', 'Build version');
|
|
66
65
|
el.textContent = formatBuildLabel(resolveBuildVersion(model));
|
|
67
66
|
const title = packagesTitle(model.packages);
|
|
68
67
|
if (title) el.title = title;
|
|
@@ -26,7 +26,7 @@ export function Callout(props: CalloutProps) {
|
|
|
26
26
|
<Show when={props.title}>
|
|
27
27
|
<span class="zylem-callout-title">{props.title}</span>
|
|
28
28
|
</Show>
|
|
29
|
-
<
|
|
29
|
+
<div class="zylem-callout-message">{props.children}</div>
|
|
30
30
|
</div>
|
|
31
31
|
);
|
|
32
32
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
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 CardProps
|
|
5
6
|
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
6
7
|
title?: JSX.Element;
|
|
8
|
+
/** Heading level of the title (default 3). */
|
|
9
|
+
headingLevel?: HeadingLevel;
|
|
7
10
|
description?: JSX.Element;
|
|
8
11
|
/** Artwork above the title: a thumbnail, preview swatch, or icon well. */
|
|
9
12
|
media?: JSX.Element;
|
|
10
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Renders hover glow + pointer cursor and click affordances. The card
|
|
15
|
+
* becomes a button, so it must not contain other controls.
|
|
16
|
+
*/
|
|
11
17
|
interactive?: boolean;
|
|
12
18
|
selected?: boolean;
|
|
13
19
|
/** Rendered after the description: tags, actions, metadata. */
|
|
@@ -22,6 +28,7 @@ export interface CardProps
|
|
|
22
28
|
export function Card(props: CardProps) {
|
|
23
29
|
const [local, rest] = splitProps(props, [
|
|
24
30
|
'title',
|
|
31
|
+
'headingLevel',
|
|
25
32
|
'description',
|
|
26
33
|
'media',
|
|
27
34
|
'interactive',
|
|
@@ -66,7 +73,9 @@ export function Card(props: CardProps) {
|
|
|
66
73
|
<div class="zylem-card-media">{local.media}</div>
|
|
67
74
|
</Show>
|
|
68
75
|
<Show when={local.title}>
|
|
69
|
-
<
|
|
76
|
+
<Heading level={local.headingLevel ?? 3} class="zylem-card-title">
|
|
77
|
+
{local.title}
|
|
78
|
+
</Heading>
|
|
70
79
|
</Show>
|
|
71
80
|
<Show when={local.description}>
|
|
72
81
|
<div class="zylem-card-description">{local.description}</div>
|
|
@@ -4,6 +4,8 @@ import { Show } from 'solid-js';
|
|
|
4
4
|
|
|
5
5
|
export interface CheckboxProps {
|
|
6
6
|
label?: string;
|
|
7
|
+
/** Accessible name when there is no visible `label`. */
|
|
8
|
+
'aria-label'?: string;
|
|
7
9
|
checked?: boolean;
|
|
8
10
|
defaultChecked?: boolean;
|
|
9
11
|
onChange?: (checked: boolean) => void;
|
|
@@ -25,10 +27,10 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
25
27
|
disabled={props.disabled}
|
|
26
28
|
name={props.name}
|
|
27
29
|
>
|
|
28
|
-
<KCheckbox.Input class="zylem-checkbox-input" />
|
|
30
|
+
<KCheckbox.Input class="zylem-checkbox-input" aria-label={props['aria-label']} />
|
|
29
31
|
<KCheckbox.Control class="zylem-checkbox-control">
|
|
30
32
|
<KCheckbox.Indicator>
|
|
31
|
-
<Check class="zylem-checkbox-icon" />
|
|
33
|
+
<Check class="zylem-checkbox-icon" aria-hidden="true" />
|
|
32
34
|
</KCheckbox.Indicator>
|
|
33
35
|
</KCheckbox.Control>
|
|
34
36
|
<Show when={props.label}>
|
|
@@ -9,6 +9,8 @@ export interface ConsoleProps {
|
|
|
9
9
|
onInput?: (value: string) => void;
|
|
10
10
|
onClear?: () => void;
|
|
11
11
|
readOnly?: boolean;
|
|
12
|
+
/** Accessible name of the text field (default "Console"). */
|
|
13
|
+
label?: string;
|
|
12
14
|
class?: string;
|
|
13
15
|
/** Extra action buttons rendered next to Clear. */
|
|
14
16
|
actions?: JSX.Element;
|
|
@@ -30,6 +32,7 @@ export function Console(props: ConsoleProps) {
|
|
|
30
32
|
<div class="zylem-console-wrapper">
|
|
31
33
|
<textarea
|
|
32
34
|
class="zylem-console zylem-scroll"
|
|
35
|
+
aria-label={props.label ?? 'Console'}
|
|
33
36
|
value={props.value}
|
|
34
37
|
spellcheck={false}
|
|
35
38
|
readOnly={props.readOnly || !props.onInput}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { globalStyle, keyframes } from '@vanilla-extract/css';
|
|
2
|
+
import { LAYER_PORTAL_CLASS } from '../../layers/resolve-mount';
|
|
2
3
|
import { vars } from '../../theme.css';
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
+
const backdropEnter = keyframes({
|
|
5
6
|
from: { opacity: 0 },
|
|
6
7
|
to: { opacity: 1 },
|
|
7
8
|
});
|
|
@@ -11,26 +12,56 @@ const contentEnter = keyframes({
|
|
|
11
12
|
to: { opacity: 1, transform: 'translateY(0) scale(1)' },
|
|
12
13
|
});
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// The <dialog> is a transparent sheet over the whole viewport; the card inside
|
|
16
|
+
// is the visible window. Undoes the UA's `dialog:modal` box (centered,
|
|
17
|
+
// max-sized, scrolling) but never sets `display`, which would override the UA
|
|
18
|
+
// rule that hides a closed dialog.
|
|
19
|
+
globalStyle('.zylem-dialog', {
|
|
15
20
|
position: 'fixed',
|
|
16
21
|
inset: 0,
|
|
17
|
-
|
|
22
|
+
width: '100%',
|
|
23
|
+
height: '100%',
|
|
24
|
+
maxWidth: 'none',
|
|
25
|
+
maxHeight: 'none',
|
|
26
|
+
margin: 0,
|
|
27
|
+
padding: 0,
|
|
28
|
+
border: 0,
|
|
29
|
+
background: 'transparent',
|
|
30
|
+
color: 'inherit',
|
|
31
|
+
font: 'inherit',
|
|
32
|
+
overflow: 'visible',
|
|
33
|
+
outline: 'none',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
globalStyle('.zylem-dialog::backdrop', {
|
|
18
37
|
background: 'rgba(5, 10, 18, 0.6)',
|
|
19
38
|
backdropFilter: 'blur(3px)',
|
|
20
|
-
animation: `${
|
|
39
|
+
animation: `${backdropEnter} 0.15s ${vars.motion.easeOut}`,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
globalStyle('html:has(.zylem-dialog[open])', {
|
|
43
|
+
overflow: 'hidden',
|
|
21
44
|
});
|
|
22
45
|
|
|
23
46
|
globalStyle('.zylem-dialog-positioner', {
|
|
24
|
-
position: 'fixed',
|
|
25
|
-
inset: 0,
|
|
26
|
-
// One above the backdrop it shares a tier with.
|
|
27
|
-
zIndex: `calc(${vars.layers.modal} + 1)`,
|
|
28
47
|
display: 'flex',
|
|
29
48
|
alignItems: 'center',
|
|
30
49
|
justifyContent: 'center',
|
|
50
|
+
boxSizing: 'border-box',
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: '100%',
|
|
31
53
|
padding: vars.spacing.lg,
|
|
32
54
|
});
|
|
33
55
|
|
|
56
|
+
// The scope's portal container, where overlays opened from the dialog mount.
|
|
57
|
+
// Out of flow so it takes no part in centering the card, at the dialog's
|
|
58
|
+
// origin, which is the viewport's.
|
|
59
|
+
globalStyle(`.zylem-dialog-positioner > .${LAYER_PORTAL_CLASS}`, {
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
top: 0,
|
|
62
|
+
left: 0,
|
|
63
|
+
});
|
|
64
|
+
|
|
34
65
|
globalStyle('.zylem-dialog-content', {
|
|
35
66
|
display: 'flex',
|
|
36
67
|
flexDirection: 'column',
|
|
@@ -124,20 +155,13 @@ globalStyle('.zylem-dialog-body', {
|
|
|
124
155
|
lineHeight: 1.45,
|
|
125
156
|
});
|
|
126
157
|
|
|
127
|
-
// In fullscreen the body
|
|
128
|
-
//
|
|
158
|
+
// In fullscreen the body becomes a flex column so content can lay out against
|
|
159
|
+
// the dialog's height (and own its scrolling).
|
|
129
160
|
globalStyle('.zylem-dialog-content[data-size="fullscreen"] .zylem-dialog-body', {
|
|
130
161
|
display: 'flex',
|
|
131
162
|
flexDirection: 'column',
|
|
132
163
|
});
|
|
133
164
|
|
|
134
|
-
globalStyle('.zylem-dialog-content[data-size="fullscreen"] .zylem-dialog-scope', {
|
|
135
|
-
display: 'flex',
|
|
136
|
-
flexDirection: 'column',
|
|
137
|
-
flex: '1 1 auto',
|
|
138
|
-
minHeight: 0,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
165
|
globalStyle('.zylem-dialog-footer', {
|
|
142
166
|
flex: 'none',
|
|
143
167
|
display: 'flex',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Show } from 'solid-js';
|
|
1
|
+
import { createUniqueId, onCleanup, onMount, Show } from 'solid-js';
|
|
3
2
|
import type { JSX } from 'solid-js';
|
|
4
|
-
import {
|
|
3
|
+
import { Portal } from 'solid-js/web';
|
|
4
|
+
import { LAYER_PORTAL_CLASS, LayerScope, useLayer } from '../../layers';
|
|
5
5
|
import { Button } from '../Button/Button';
|
|
6
6
|
import { WindowControls } from '../WindowControls/WindowControls';
|
|
7
7
|
|
|
@@ -16,7 +16,7 @@ export interface DialogProps {
|
|
|
16
16
|
open?: boolean;
|
|
17
17
|
onOpenChange?: (open: boolean) => void;
|
|
18
18
|
title?: JSX.Element;
|
|
19
|
-
/** Secondary line under the title. */
|
|
19
|
+
/** Secondary line under the title. Describes the dialog unless `aria-describedby` says otherwise. */
|
|
20
20
|
subtitle?: JSX.Element;
|
|
21
21
|
/** Content between the titles and the close control, e.g. a search field. */
|
|
22
22
|
headerExtra?: JSX.Element;
|
|
@@ -28,72 +28,147 @@ export interface DialogProps {
|
|
|
28
28
|
class?: string;
|
|
29
29
|
/** Extra class on the body, e.g. to hand scrolling to the content's columns. */
|
|
30
30
|
bodyClass?: string;
|
|
31
|
+
/** `alertdialog` for an urgent question the user has to answer, such as a confirmation. */
|
|
32
|
+
role?: 'dialog' | 'alertdialog';
|
|
33
|
+
/** Names the dialog when it has no `title`. */
|
|
34
|
+
'aria-label'?: string;
|
|
35
|
+
/** Id of the element that describes the dialog. */
|
|
36
|
+
'aria-describedby'?: string;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* HyperGlass dialog: glass window with a glossy title bar and jewel close
|
|
35
41
|
* control, centered over a dimmed backdrop.
|
|
36
42
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
43
|
+
* A native `<dialog>` opened with `showModal()`: it renders in the top layer,
|
|
44
|
+
* above everything else on the page, which is inert while it is open. Escape
|
|
45
|
+
* and a click outside the card ask to close it, and closing hands focus back
|
|
46
|
+
* to whatever had it before.
|
|
47
|
+
*
|
|
48
|
+
* The card shares a {@link LayerScope} with the scope's portal container, so
|
|
49
|
+
* overlays opened from the dialog render inside the `<dialog>` (outside it they
|
|
50
|
+
* would be inert) but beside the card rather than in it, where the card's
|
|
51
|
+
* clipping would cut them off.
|
|
42
52
|
*/
|
|
43
53
|
export function Dialog(props: DialogProps) {
|
|
44
54
|
const layer = useLayer('modal');
|
|
45
|
-
const dismiss = createDismissGuard();
|
|
46
55
|
|
|
47
56
|
return (
|
|
48
|
-
|
|
57
|
+
<>
|
|
49
58
|
{/*
|
|
50
59
|
A dialog is controlled and renders no trigger, so it has nothing in the
|
|
51
60
|
page to resolve its tree from. This marker sits where the dialog was
|
|
52
61
|
declared, which is enough to find an enclosing shadow root.
|
|
53
62
|
*/}
|
|
54
63
|
<span ref={layer.anchorRef} style={{ display: 'none' }} aria-hidden="true" />
|
|
55
|
-
<Show when={layer.mount()} keyed>
|
|
64
|
+
<Show when={props.open && layer.mount()} keyed>
|
|
56
65
|
{(mount) => (
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
<KDialog.Content
|
|
61
|
-
ref={dismiss.ref}
|
|
62
|
-
class={props.class ? `zylem-dialog-content ${props.class}` : 'zylem-dialog-content'}
|
|
63
|
-
data-size={props.size}
|
|
64
|
-
onInteractOutside={dismiss.onInteractOutside}
|
|
65
|
-
>
|
|
66
|
-
<div class="zylem-dialog-header">
|
|
67
|
-
<Show when={props.title || props.subtitle}>
|
|
68
|
-
<div class="zylem-dialog-titles">
|
|
69
|
-
<Show when={props.title}>
|
|
70
|
-
<KDialog.Title class="zylem-dialog-title">{props.title}</KDialog.Title>
|
|
71
|
-
</Show>
|
|
72
|
-
<Show when={props.subtitle}>
|
|
73
|
-
<p class="zylem-dialog-subtitle">{props.subtitle}</p>
|
|
74
|
-
</Show>
|
|
75
|
-
</div>
|
|
76
|
-
</Show>
|
|
77
|
-
<Show when={props.headerExtra}>
|
|
78
|
-
<div class="zylem-dialog-header-extra">{props.headerExtra}</div>
|
|
79
|
-
</Show>
|
|
80
|
-
<WindowControls onClose={() => props.onOpenChange?.(false)} />
|
|
81
|
-
</div>
|
|
82
|
-
<KDialog.Description
|
|
83
|
-
as="div"
|
|
84
|
-
class={props.bodyClass ? `zylem-dialog-body ${props.bodyClass}` : 'zylem-dialog-body'}
|
|
85
|
-
>
|
|
86
|
-
<LayerScope class="zylem-dialog-scope">{props.children}</LayerScope>
|
|
87
|
-
</KDialog.Description>
|
|
88
|
-
<Show when={props.footer}>
|
|
89
|
-
<div class="zylem-dialog-footer">{props.footer}</div>
|
|
90
|
-
</Show>
|
|
91
|
-
</KDialog.Content>
|
|
92
|
-
</div>
|
|
93
|
-
</KDialog.Portal>
|
|
66
|
+
<Portal mount={mount}>
|
|
67
|
+
<DialogWindow {...props} />
|
|
68
|
+
</Portal>
|
|
94
69
|
)}
|
|
95
70
|
</Show>
|
|
96
|
-
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Overlays a click outside the card dismisses on its own, without closing the dialog too. */
|
|
76
|
+
const DISMISSABLE_OVERLAY = '[role="menu"], [role="listbox"], [role="dialog"]';
|
|
77
|
+
|
|
78
|
+
/** The `<dialog>` itself, mounted for as long as the dialog is open. */
|
|
79
|
+
function DialogWindow(props: DialogProps) {
|
|
80
|
+
const titleId = createUniqueId();
|
|
81
|
+
const subtitleId = createUniqueId();
|
|
82
|
+
let dialog!: HTMLDialogElement;
|
|
83
|
+
let disposed = false;
|
|
84
|
+
let pressedBackdrop = false;
|
|
85
|
+
|
|
86
|
+
const close = () => props.onOpenChange?.(false);
|
|
87
|
+
// Outside the card means on the dialog itself or on the positioner filling it.
|
|
88
|
+
const isBackdrop = (target: EventTarget | null | undefined) =>
|
|
89
|
+
target === dialog || target === dialog.firstElementChild;
|
|
90
|
+
const hasOpenOverlay = () =>
|
|
91
|
+
dialog
|
|
92
|
+
.querySelector(`.zylem-dialog-positioner > .${LAYER_PORTAL_CLASS}`)
|
|
93
|
+
?.querySelector(DISMISSABLE_OVERLAY) != null;
|
|
94
|
+
|
|
95
|
+
onMount(() => {
|
|
96
|
+
// jsdom has no `showModal()`.
|
|
97
|
+
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
98
|
+
else dialog.setAttribute('open', '');
|
|
99
|
+
|
|
100
|
+
// Kobalte dismisses an open overlay from a capture listener on `document`,
|
|
101
|
+
// so this has to run first, on `window`, to still see it open.
|
|
102
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
103
|
+
pressedBackdrop = isBackdrop(event.composedPath()[0]) && !hasOpenOverlay();
|
|
104
|
+
};
|
|
105
|
+
window.addEventListener('pointerdown', onPointerDown, true);
|
|
106
|
+
onCleanup(() => window.removeEventListener('pointerdown', onPointerDown, true));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
onCleanup(() => {
|
|
110
|
+
disposed = true;
|
|
111
|
+
// Runs before the portal detaches the element: only a dialog closed while
|
|
112
|
+
// still in the document returns focus to whatever opened it.
|
|
113
|
+
if (dialog.open) dialog.close();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<dialog
|
|
118
|
+
ref={dialog}
|
|
119
|
+
class="zylem-dialog"
|
|
120
|
+
role={props.role}
|
|
121
|
+
aria-labelledby={props.title ? titleId : undefined}
|
|
122
|
+
aria-label={props['aria-label']}
|
|
123
|
+
aria-describedby={props['aria-describedby'] ?? (props.subtitle ? subtitleId : undefined)}
|
|
124
|
+
onCancel={(event) => {
|
|
125
|
+
// `open` stays the source of truth: the host closes the dialog.
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
close();
|
|
128
|
+
}}
|
|
129
|
+
onClose={() => {
|
|
130
|
+
// The browser closed it anyway, e.g. a second Escape it would not let be prevented.
|
|
131
|
+
if (!disposed) close();
|
|
132
|
+
}}
|
|
133
|
+
onClick={(event) => {
|
|
134
|
+
if (pressedBackdrop && isBackdrop(event.target)) close();
|
|
135
|
+
pressedBackdrop = false;
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<LayerScope class="zylem-dialog-positioner">
|
|
139
|
+
<div
|
|
140
|
+
class={props.class ? `zylem-dialog-content ${props.class}` : 'zylem-dialog-content'}
|
|
141
|
+
data-size={props.size}
|
|
142
|
+
>
|
|
143
|
+
<div class="zylem-dialog-header">
|
|
144
|
+
<Show when={props.title || props.subtitle}>
|
|
145
|
+
<div class="zylem-dialog-titles">
|
|
146
|
+
<Show when={props.title}>
|
|
147
|
+
<h2 id={titleId} class="zylem-dialog-title">
|
|
148
|
+
{props.title}
|
|
149
|
+
</h2>
|
|
150
|
+
</Show>
|
|
151
|
+
<Show when={props.subtitle}>
|
|
152
|
+
<p id={subtitleId} class="zylem-dialog-subtitle">
|
|
153
|
+
{props.subtitle}
|
|
154
|
+
</p>
|
|
155
|
+
</Show>
|
|
156
|
+
</div>
|
|
157
|
+
</Show>
|
|
158
|
+
<Show when={props.headerExtra}>
|
|
159
|
+
<div class="zylem-dialog-header-extra">{props.headerExtra}</div>
|
|
160
|
+
</Show>
|
|
161
|
+
<WindowControls onClose={close} closeLabel="Close dialog" />
|
|
162
|
+
</div>
|
|
163
|
+
<div class={props.bodyClass ? `zylem-dialog-body ${props.bodyClass}` : 'zylem-dialog-body'}>
|
|
164
|
+
{props.children}
|
|
165
|
+
</div>
|
|
166
|
+
<Show when={props.footer}>
|
|
167
|
+
<div class="zylem-dialog-footer">{props.footer}</div>
|
|
168
|
+
</Show>
|
|
169
|
+
</div>
|
|
170
|
+
</LayerScope>
|
|
171
|
+
</dialog>
|
|
97
172
|
);
|
|
98
173
|
}
|
|
99
174
|
|
|
@@ -110,21 +185,27 @@ export interface ConfirmDialogProps {
|
|
|
110
185
|
|
|
111
186
|
/**
|
|
112
187
|
* Confirmation dialog built on Dialog — a drop-in replacement for
|
|
113
|
-
* `window.confirm()` flows.
|
|
188
|
+
* `window.confirm()` flows. An `alertdialog` described by its message; it
|
|
189
|
+
* opens with focus on Confirm, or on Cancel when the action is destructive.
|
|
114
190
|
*/
|
|
115
191
|
export function ConfirmDialog(props: ConfirmDialogProps) {
|
|
192
|
+
const messageId = createUniqueId();
|
|
193
|
+
|
|
116
194
|
return (
|
|
117
195
|
<Dialog
|
|
118
196
|
open={props.open}
|
|
119
197
|
onOpenChange={props.onOpenChange}
|
|
198
|
+
role="alertdialog"
|
|
199
|
+
aria-describedby={messageId}
|
|
120
200
|
title={props.title ?? 'Confirm'}
|
|
121
201
|
footer={
|
|
122
202
|
<>
|
|
123
|
-
<Button variant="ghost" onClick={() => props.onOpenChange(false)}>
|
|
203
|
+
<Button variant="ghost" autofocus={props.danger} onClick={() => props.onOpenChange(false)}>
|
|
124
204
|
{props.cancelLabel ?? 'Cancel'}
|
|
125
205
|
</Button>
|
|
126
206
|
<Button
|
|
127
207
|
variant={props.danger ? 'danger' : 'primary'}
|
|
208
|
+
autofocus={!props.danger}
|
|
128
209
|
onClick={() => {
|
|
129
210
|
props.onConfirm();
|
|
130
211
|
props.onOpenChange(false);
|
|
@@ -135,7 +216,7 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
|
|
135
216
|
</>
|
|
136
217
|
}
|
|
137
218
|
>
|
|
138
|
-
{props.message}
|
|
219
|
+
<div id={messageId}>{props.message}</div>
|
|
139
220
|
</Dialog>
|
|
140
221
|
);
|
|
141
222
|
}
|
|
@@ -28,7 +28,7 @@ export function FrameStrip(props: FrameStripProps) {
|
|
|
28
28
|
<span class="zylem-frame-strip-title">{local.title}</span>
|
|
29
29
|
</Show>
|
|
30
30
|
</div>
|
|
31
|
-
<div class="zylem-frame-strip-row zylem-scroll" role="
|
|
31
|
+
<div class="zylem-frame-strip-row zylem-scroll" role="group" aria-label={local.label ?? 'Frames'}>
|
|
32
32
|
{local.children}
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
@@ -51,10 +51,9 @@ export function FrameThumb(props: FrameThumbProps) {
|
|
|
51
51
|
return (
|
|
52
52
|
<button
|
|
53
53
|
type="button"
|
|
54
|
-
role="option"
|
|
55
54
|
class={props.class ? `zylem-frame-thumb ${props.class}` : 'zylem-frame-thumb'}
|
|
56
55
|
data-selected={props.selected ? '' : undefined}
|
|
57
|
-
aria-
|
|
56
|
+
aria-pressed={props.selected ? 'true' : 'false'}
|
|
58
57
|
aria-label={props.label ?? `Frame ${props.index}`}
|
|
59
58
|
onClick={props.onClick}
|
|
60
59
|
>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { For, Show, createMemo, createSignal, type JSX } from 'solid-js';
|
|
1
|
+
import { For, Show, createMemo, createSignal, createUniqueId, type JSX } from 'solid-js';
|
|
2
2
|
import { SearchInput } from '../SearchInput/SearchInput';
|
|
3
3
|
import { filterItems, groupItems, type PickerItem } from './filter-items';
|
|
4
4
|
|
|
5
5
|
export interface ItemPickerProps<T extends PickerItem = PickerItem> {
|
|
6
6
|
items: T[];
|
|
7
7
|
onSelect: (item: T) => void;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Marked as the current choice. Given at all (`null` for none), the items
|
|
10
|
+
* become toggles that expose it as `aria-pressed`.
|
|
11
|
+
*/
|
|
9
12
|
selectedId?: string | null | undefined;
|
|
10
13
|
/** Hide the search field for a list short enough not to need one. */
|
|
11
14
|
searchable?: boolean | undefined;
|
|
@@ -34,6 +37,9 @@ export function ItemPicker<T extends PickerItem>(props: ItemPickerProps<T>) {
|
|
|
34
37
|
|
|
35
38
|
const searchable = () => props.searchable !== false;
|
|
36
39
|
const groups = createMemo(() => groupItems(filterItems(props.items, query())));
|
|
40
|
+
/** Items are toggles only when the caller tracks a choice (`null` is "none yet"). */
|
|
41
|
+
const pressed = (item: T) =>
|
|
42
|
+
props.selectedId === undefined ? undefined : props.selectedId === item.id;
|
|
37
43
|
|
|
38
44
|
return (
|
|
39
45
|
<div class={props.class ? `zylem-picker ${props.class}` : 'zylem-picker'}>
|
|
@@ -68,37 +74,44 @@ export function ItemPicker<T extends PickerItem>(props: ItemPickerProps<T>) {
|
|
|
68
74
|
>
|
|
69
75
|
<div class="zylem-picker__groups zylem-scroll">
|
|
70
76
|
<For each={groups()}>
|
|
71
|
-
{(group) =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
77
|
+
{(group) => {
|
|
78
|
+
const titleId = createUniqueId();
|
|
79
|
+
return (
|
|
80
|
+
<div class="zylem-picker__group" role="group" aria-labelledby={titleId}>
|
|
81
|
+
<div id={titleId} class="zylem-picker__group-title">
|
|
82
|
+
{group.group}
|
|
83
|
+
</div>
|
|
84
|
+
<For each={group.items}>
|
|
85
|
+
{(item) => (
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="zylem-picker__item"
|
|
89
|
+
disabled={item.disabled}
|
|
90
|
+
data-selected={
|
|
91
|
+
props.selectedId === item.id ? '' : undefined
|
|
92
|
+
}
|
|
93
|
+
aria-pressed={pressed(item)}
|
|
94
|
+
title={item.description}
|
|
95
|
+
onClick={() => props.onSelect(item)}
|
|
96
|
+
>
|
|
97
|
+
<Show when={item.icon}>
|
|
98
|
+
{(icon) => (
|
|
99
|
+
<span
|
|
100
|
+
class="zylem-picker__item-glyph"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
// Markup from the caller's own registry, in the
|
|
103
|
+
// caller's own realm; not a new trust boundary.
|
|
104
|
+
innerHTML={icon()}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
</Show>
|
|
108
|
+
<span class="zylem-picker__item-label">{item.label}</span>
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</For>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}}
|
|
102
115
|
</For>
|
|
103
116
|
</div>
|
|
104
117
|
</Show>
|
|
@@ -140,6 +140,7 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
140
140
|
<KPopover.Content
|
|
141
141
|
ref={dismiss.ref}
|
|
142
142
|
class="zylem-menu-button__panel"
|
|
143
|
+
aria-label={props.label}
|
|
143
144
|
onInteractOutside={dismiss.onInteractOutside}
|
|
144
145
|
>
|
|
145
146
|
{props.menu(() => setOpen(false))}
|