@noxlovette/material 0.6.1 → 0.7.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/claude-skill/material-design/SKILL.md +5 -2
- package/claude-skill/material-design/references/accessibility-checklist.md +5 -0
- package/claude-skill/material-design/references/component-patterns.md +24 -3
- package/claude-skill/material-design/references/motion-guide.md +53 -11
- package/claude-skill/material-design/references/tokens-and-styles.md +4 -0
- package/dist/animation/TransitionPatterns.stories.svelte +50 -0
- package/dist/animation/containerTransform.js +10 -4
- package/dist/animation/reducedMotion.d.ts +2 -0
- package/dist/animation/reducedMotion.js +2 -0
- package/dist/animation/sharedAxisTransition.d.ts +20 -3
- package/dist/animation/sharedAxisTransition.js +67 -8
- package/dist/attachments/drag.d.ts +52 -0
- package/dist/attachments/drag.js +217 -0
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/components/chips/Chip.mdx +24 -11
- package/dist/components/chips/Chip.stories.svelte +10 -0
- package/dist/components/chips/Chip.svelte +58 -18
- package/dist/components/chips/Chip.svelte.d.ts +3 -1
- package/dist/components/chips/ChipGroup.stories.svelte +73 -0
- package/dist/components/chips/ChipGroup.stories.svelte.d.ts +4 -0
- package/dist/components/chips/ChipGroup.svelte +242 -0
- package/dist/components/chips/ChipGroup.svelte.d.ts +33 -0
- package/dist/components/chips/index.d.ts +1 -0
- package/dist/components/chips/index.js +1 -0
- package/dist/components/chips/theme.d.ts +182 -21
- package/dist/components/chips/theme.js +116 -25
- package/dist/components/chips/types.d.ts +36 -2
- package/dist/components/containers/app/theme.d.ts +1 -1
- package/dist/components/containers/app/theme.js +1 -1
- package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +1 -1
- package/dist/components/containers/list/List.svelte +8 -2
- package/dist/components/containers/list/ListItem.mdx +6 -0
- package/dist/components/containers/list/ListItem.svelte +77 -2
- package/dist/components/containers/list/context.d.ts +14 -0
- package/dist/components/containers/list/theme.d.ts +6 -0
- package/dist/components/containers/list/theme.js +10 -2
- package/dist/components/containers/pane/DraggablePane.svelte +25 -63
- package/dist/components/containers/pane/Pane.svelte +1 -1
- package/dist/components/containers/pane/theme.js +32 -6
- package/dist/components/containers/popover/theme.d.ts +3 -3
- package/dist/components/date/theme.d.ts +6 -6
- package/dist/components/forms/checkbox/theme.js +1 -1
- package/dist/components/forms/command/theme.d.ts +3 -3
- package/dist/components/misc/Avatar.stories.svelte +56 -7
- package/dist/components/misc/Avatar.stories.svelte.d.ts +2 -17
- package/dist/components/misc/Avatar.svelte +82 -8
- package/dist/components/misc/Avatar.svelte.d.ts +5 -0
- package/dist/components/misc/theme.d.ts +6 -0
- package/dist/components/misc/theme.js +5 -3
- package/dist/components/misc/types.d.ts +17 -1
- package/dist/components/nav/appbar/theme.js +2 -1
- package/dist/components/nav/currentRoute.d.ts +11 -0
- package/dist/components/nav/currentRoute.js +28 -0
- package/dist/components/nav/navbar/NavbarItem.svelte +3 -6
- package/dist/components/nav/navbar/theme.d.ts +3 -3
- package/dist/components/nav/rail/Rail.mdx +65 -23
- package/dist/components/nav/rail/Rail.stories.svelte +24 -4
- package/dist/components/nav/rail/Rail.stories.svelte.d.ts +2 -17
- package/dist/components/nav/rail/Rail.svelte +163 -48
- package/dist/components/nav/rail/Rail.svelte.d.ts +6 -3
- package/dist/components/nav/rail/RailItem.svelte +122 -65
- package/dist/components/nav/rail/RailItem.svelte.d.ts +3 -2
- package/dist/components/nav/rail/theme.d.ts +69 -84
- package/dist/components/nav/rail/theme.js +79 -66
- package/dist/components/nav/tabs/Tabs.stories.svelte +2 -1
- package/dist/index.css +2 -0
- package/dist/styles/components.css +50 -0
- package/dist/styles/icon.css +13 -0
- package/dist/utils/icon/Icon.stories.svelte +97 -0
- package/dist/utils/icon/Icon.stories.svelte.d.ts +4 -0
- package/dist/utils/icon/Icon.svelte +101 -12
- package/dist/utils/icon/Icon.svelte.d.ts +9 -0
- package/dist/utils/icon/MaterialSymbolsProvider.svelte +11 -4
- package/dist/utils/icon/base-icons.d.ts +1 -1
- package/dist/utils/icon/base-icons.js +1 -1
- package/dist/utils/icon/icon-names.d.ts +2 -0
- package/dist/utils/icon/icon-names.js +2 -0
- package/dist/utils/icon/swap.d.ts +6 -0
- package/dist/utils/icon/swap.js +45 -0
- package/dist/utils/icon/types.d.ts +59 -7
- package/dist/utils/tv.d.ts +3 -4
- package/dist/utils/tv.js +14 -6
- package/package.json +2 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { on } from 'svelte/events';
|
|
2
|
+
/* Pointer samples older than this don't count towards the release velocity. */
|
|
3
|
+
const VELOCITY_WINDOW_MS = 100;
|
|
4
|
+
/* How far a resting finger may wander during `touchDelay` before it counts as a scroll. */
|
|
5
|
+
const TOUCH_SLOP = 8;
|
|
6
|
+
/**
|
|
7
|
+
* Pointer dragging for an element: capture, a start threshold, a long-press delay on touch, and
|
|
8
|
+
* the release velocity. It moves nothing itself: `onMove` gets the pointer's offset and the
|
|
9
|
+
* caller positions things, typically with `resist` at the edges and a Motion spring on release.
|
|
10
|
+
*
|
|
11
|
+
* Options are read through a getter at event time, so changing them never tears down a drag in
|
|
12
|
+
* progress:
|
|
13
|
+
*
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <div {@attach drag(() => ({ threshold: 4, onMove: (o) => (offset = o) }))}>…</div>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* After a drag the click that would follow the release is swallowed, so dragging a button
|
|
19
|
+
* doesn't also press it.
|
|
20
|
+
*/
|
|
21
|
+
export function drag(getOptions) {
|
|
22
|
+
return (node) => {
|
|
23
|
+
let phase = 'idle';
|
|
24
|
+
let pointerId = -1;
|
|
25
|
+
let origin = { x: 0, y: 0 };
|
|
26
|
+
let samples = [];
|
|
27
|
+
let touchTimer;
|
|
28
|
+
let suppressClick = false;
|
|
29
|
+
let unwatch;
|
|
30
|
+
const offsetOf = (event) => ({
|
|
31
|
+
x: event.clientX - origin.x,
|
|
32
|
+
y: event.clientY - origin.y
|
|
33
|
+
});
|
|
34
|
+
const record = (event) => {
|
|
35
|
+
samples.push({ x: event.clientX, y: event.clientY, t: event.timeStamp });
|
|
36
|
+
samples = samples.filter((sample) => event.timeStamp - sample.t <= VELOCITY_WINDOW_MS);
|
|
37
|
+
};
|
|
38
|
+
const velocity = () => {
|
|
39
|
+
const first = samples[0];
|
|
40
|
+
const last = samples[samples.length - 1];
|
|
41
|
+
const seconds = first && last ? (last.t - first.t) / 1000 : 0;
|
|
42
|
+
if (!seconds)
|
|
43
|
+
return { x: 0, y: 0 };
|
|
44
|
+
return { x: (last.x - first.x) / seconds, y: (last.y - first.y) / seconds };
|
|
45
|
+
};
|
|
46
|
+
const reset = () => {
|
|
47
|
+
clearTimeout(touchTimer);
|
|
48
|
+
unwatch?.();
|
|
49
|
+
unwatch = undefined;
|
|
50
|
+
phase = 'idle';
|
|
51
|
+
pointerId = -1;
|
|
52
|
+
};
|
|
53
|
+
const capture = () => {
|
|
54
|
+
try {
|
|
55
|
+
node.setPointerCapture(pointerId);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Not an active pointer (a synthetic event, or one already gone): drag uncaptured.
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const begin = (event) => {
|
|
62
|
+
clearTimeout(touchTimer);
|
|
63
|
+
touchTimer = undefined;
|
|
64
|
+
phase = 'dragging';
|
|
65
|
+
// Capturing only now, not on pointerdown: a capture from the start would retarget the
|
|
66
|
+
// click to this node, and a press that never became a drag would lose its click.
|
|
67
|
+
capture();
|
|
68
|
+
getOptions().onStart?.(event);
|
|
69
|
+
};
|
|
70
|
+
const finish = (v) => {
|
|
71
|
+
const wasDragging = phase === 'dragging';
|
|
72
|
+
if (node.hasPointerCapture?.(pointerId))
|
|
73
|
+
node.releasePointerCapture(pointerId);
|
|
74
|
+
reset();
|
|
75
|
+
if (!wasDragging)
|
|
76
|
+
return;
|
|
77
|
+
suppressClick = true;
|
|
78
|
+
// The click, if any, fires synchronously after pointerup; anything later is a new press.
|
|
79
|
+
setTimeout(() => (suppressClick = false));
|
|
80
|
+
getOptions().onEnd?.(v);
|
|
81
|
+
};
|
|
82
|
+
/*
|
|
83
|
+
The whole gesture, pending or dragging, is followed on the window rather than the node.
|
|
84
|
+
Capture alone isn't enough: moving the captured node in the DOM (a list reordering under the
|
|
85
|
+
drag) silently releases it, and then the release would land on some other element and never
|
|
86
|
+
end the drag. Plain listeners, not svelte/events' `on`: that defers pointer listeners to a
|
|
87
|
+
microtask, and these are added mid-gesture, so they must be live for the very next event.
|
|
88
|
+
*/
|
|
89
|
+
const watch = () => {
|
|
90
|
+
const onMove = (event) => {
|
|
91
|
+
if (event.pointerId !== pointerId)
|
|
92
|
+
return;
|
|
93
|
+
record(event);
|
|
94
|
+
if (phase === 'dragging') {
|
|
95
|
+
// The button came up somewhere we didn't hear about (outside the window, uncaptured).
|
|
96
|
+
if (event.pointerType === 'mouse' && event.buttons === 0)
|
|
97
|
+
return finish(velocity());
|
|
98
|
+
getOptions().onMove?.(offsetOf(event), event);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const { threshold = 0 } = getOptions();
|
|
102
|
+
const moved = Math.hypot(event.clientX - origin.x, event.clientY - origin.y);
|
|
103
|
+
if (event.pointerType === 'touch' && touchTimer !== undefined) {
|
|
104
|
+
// Moved before the long press: it's a scroll, not a drag.
|
|
105
|
+
if (moved > TOUCH_SLOP)
|
|
106
|
+
reset();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (moved >= threshold) {
|
|
110
|
+
begin(event);
|
|
111
|
+
getOptions().onMove?.(offsetOf(event), event);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const onUp = (event) => {
|
|
115
|
+
if (event.pointerId !== pointerId)
|
|
116
|
+
return;
|
|
117
|
+
if (phase === 'dragging')
|
|
118
|
+
finish(velocity());
|
|
119
|
+
else
|
|
120
|
+
reset();
|
|
121
|
+
};
|
|
122
|
+
const onCancel = (event) => {
|
|
123
|
+
if (event.pointerId !== pointerId)
|
|
124
|
+
return;
|
|
125
|
+
if (phase === 'dragging')
|
|
126
|
+
finish({ x: 0, y: 0 });
|
|
127
|
+
else
|
|
128
|
+
reset();
|
|
129
|
+
};
|
|
130
|
+
// Capture lost mid-drag (the node moved): take it back, so a release outside the browser
|
|
131
|
+
// window still reaches us.
|
|
132
|
+
const onLostCapture = (event) => {
|
|
133
|
+
if (phase === 'dragging' && event.pointerId === pointerId && node.isConnected)
|
|
134
|
+
capture();
|
|
135
|
+
};
|
|
136
|
+
window.addEventListener('pointermove', onMove);
|
|
137
|
+
window.addEventListener('pointerup', onUp);
|
|
138
|
+
window.addEventListener('pointercancel', onCancel);
|
|
139
|
+
node.addEventListener('lostpointercapture', onLostCapture);
|
|
140
|
+
return () => {
|
|
141
|
+
window.removeEventListener('pointermove', onMove);
|
|
142
|
+
window.removeEventListener('pointerup', onUp);
|
|
143
|
+
window.removeEventListener('pointercancel', onCancel);
|
|
144
|
+
node.removeEventListener('lostpointercapture', onLostCapture);
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
const offDown = on(node, 'pointerdown', (event) => {
|
|
148
|
+
if (phase !== 'idle' || !event.isPrimary || event.button !== 0)
|
|
149
|
+
return;
|
|
150
|
+
const { disabled, threshold = 0, touchDelay = 0 } = getOptions();
|
|
151
|
+
if (disabled)
|
|
152
|
+
return;
|
|
153
|
+
pointerId = event.pointerId;
|
|
154
|
+
origin = { x: event.clientX, y: event.clientY };
|
|
155
|
+
samples = [{ x: event.clientX, y: event.clientY, t: event.timeStamp }];
|
|
156
|
+
unwatch = watch();
|
|
157
|
+
if (event.pointerType === 'touch' && touchDelay > 0) {
|
|
158
|
+
phase = 'pending';
|
|
159
|
+
touchTimer = setTimeout(() => {
|
|
160
|
+
touchTimer = undefined;
|
|
161
|
+
begin(event);
|
|
162
|
+
}, touchDelay);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (threshold > 0) {
|
|
166
|
+
phase = 'pending';
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
begin(event);
|
|
170
|
+
});
|
|
171
|
+
// A touch drag started by a long press: stop the page from scrolling under it. Needs a
|
|
172
|
+
// non-passive listener; touch-action can't be switched once the gesture has begun.
|
|
173
|
+
const offTouchMove = on(node, 'touchmove', (event) => {
|
|
174
|
+
if (phase === 'dragging')
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
}, { passive: false });
|
|
177
|
+
// The long press would otherwise open the context menu or the text callout.
|
|
178
|
+
const offContextMenu = on(node, 'contextmenu', (event) => {
|
|
179
|
+
if (phase !== 'idle')
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
});
|
|
182
|
+
const offClick = on(node, 'click', (event) => {
|
|
183
|
+
if (!suppressClick)
|
|
184
|
+
return;
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
event.stopPropagation();
|
|
187
|
+
}, { capture: true });
|
|
188
|
+
return () => {
|
|
189
|
+
reset();
|
|
190
|
+
offDown();
|
|
191
|
+
offTouchMove();
|
|
192
|
+
offContextMenu();
|
|
193
|
+
offClick();
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/*
|
|
198
|
+
Share of the overshoot still applied at the edge, shrinking the further it's pulled. The
|
|
199
|
+
UIScrollView rubber-band constant; M3 has no token for this, tune by eye.
|
|
200
|
+
*/
|
|
201
|
+
const RUBBER_BAND = 0.55;
|
|
202
|
+
/** Resisted overshoot: the further past the edge, the less it follows. */
|
|
203
|
+
function rubberBand(overshoot, limit) {
|
|
204
|
+
return (1 - 1 / ((overshoot * RUBBER_BAND) / limit + 1)) * limit;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* `value` clamped to `[min, max]` with rubber-banding instead of a wall: past an edge it keeps
|
|
208
|
+
* following the pointer, less and less. `size` is the scale of the resistance, usually the
|
|
209
|
+
* extent of the bounds on that axis.
|
|
210
|
+
*/
|
|
211
|
+
export function resist(value, min, max, size) {
|
|
212
|
+
if (value < min)
|
|
213
|
+
return min - rubberBand(min - value, size);
|
|
214
|
+
if (value > max)
|
|
215
|
+
return max + rubberBand(value - max, size);
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
@@ -20,12 +20,12 @@ import * as ChipStories from './Chip.stories.svelte';
|
|
|
20
20
|
|
|
21
21
|
`variant` sets both the look and the element the chip renders:
|
|
22
22
|
|
|
23
|
-
| Variant | Use for | Renders
|
|
24
|
-
| ------------ | --------------------------------------------------------------- |
|
|
25
|
-
| `assist` | A smart or automated action, like adding an event to a calendar | `<button>` (or `<a>` with `href`); icon tinted primary
|
|
26
|
-
| `filter` | Filtering content by tags or descriptive words | A toggle; a checkmark replaces the icon when selected
|
|
27
|
-
| `input` | A piece of information the user entered (a contact, a value) | A
|
|
28
|
-
| `suggestion` | A generated suggestion that narrows intent | Like `assist
|
|
23
|
+
| Variant | Use for | Renders |
|
|
24
|
+
| ------------ | --------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
25
|
+
| `assist` | A smart or automated action, like adding an event to a calendar | `<button>` (or `<a>` with `href`); icon tinted primary |
|
|
26
|
+
| `filter` | Filtering content by tags or descriptive words | A toggle; a checkmark replaces the icon when selected |
|
|
27
|
+
| `input` | A piece of information the user entered (a contact, a value) | A container with a primary button; `onRemove` adds a remove button |
|
|
28
|
+
| `suggestion` | A generated suggestion that narrows intent | Like `assist`; its label is on-surface-variant |
|
|
29
29
|
|
|
30
30
|
`elevated` swaps the flat, outlined container for a shadowed one on every variant except `input`,
|
|
31
31
|
which the spec keeps flat. For a static label with no interaction (a status or a count), use
|
|
@@ -41,6 +41,9 @@ which the spec keeps flat. For a static label with no interaction (a status or a
|
|
|
41
41
|
|
|
42
42
|
<Canvas of={ChipStories.Input} />
|
|
43
43
|
|
|
44
|
+
An `avatar` snippet replaces the leading icon and is clipped to a 24dp circle; it wins over
|
|
45
|
+
`iconProps` when both are given.
|
|
46
|
+
|
|
44
47
|
Render one input chip per entry, and remove the entry from your own array in `onRemove`:
|
|
45
48
|
|
|
46
49
|
```svelte
|
|
@@ -48,6 +51,7 @@ Render one input chip per entry, and remove the entry from your own array in `on
|
|
|
48
51
|
<Chip
|
|
49
52
|
variant="input"
|
|
50
53
|
iconProps={{ name: 'person' }}
|
|
54
|
+
removeLabel="Remove {name}"
|
|
51
55
|
onRemove={() => (recipients = recipients.filter((r) => r !== name))}
|
|
52
56
|
>
|
|
53
57
|
{name}
|
|
@@ -57,12 +61,21 @@ Render one input chip per entry, and remove the entry from your own array in `on
|
|
|
57
61
|
|
|
58
62
|
<Canvas of={ChipStories.Suggestion} />
|
|
59
63
|
|
|
64
|
+
## Sets of chips
|
|
65
|
+
|
|
66
|
+
Lay several chips out with `ChipGroup` (8dp apart, wrapping). With `reorderable`, people can drag
|
|
67
|
+
them into a new order, or move the focused one with Alt+Arrow; see Chips → Chip Group.
|
|
68
|
+
|
|
60
69
|
## Accessibility
|
|
61
70
|
|
|
62
71
|
- **Roles.** Assist and suggestion chips are real buttons or links. Filter chips are toggles
|
|
63
|
-
whose `aria-pressed` reflects `selected
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
72
|
+
whose `aria-pressed` reflects `selected`; `trailingIconProps` adds a decorative trailing icon,
|
|
73
|
+
e.g. `arrow_drop_down` for a chip that opens a menu. An input chip holds two sibling buttons:
|
|
74
|
+
its primary action (leading content and label, taking `onclick` and the rest props) and the
|
|
75
|
+
remove button, so neither nests inside the other. Backspace or Delete on the primary action
|
|
76
|
+
calls `onRemove`; give the remove button a `removeLabel` that names the item, and move focus
|
|
77
|
+
to a neighbour after removing.
|
|
78
|
+
- **Disabled.** Every chip uses the native `disabled` attribute; an input chip sets it on both
|
|
79
|
+
of its buttons.
|
|
80
|
+
- **Target size.** The remove icon is 18dp with a 48dp touch target around it.
|
|
68
81
|
- **Contrast.** Every state pairs a container with its `on-*` role, so text keeps AA contrast.
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
<div class="gap-spacing-200 p-spacing-300 flex flex-wrap items-center">
|
|
40
40
|
<Chip variant="filter" bind:selected={filterSelected}>Filter chip</Chip>
|
|
41
41
|
<Chip variant="filter" iconProps={{ name: 'star' }}>Starred</Chip>
|
|
42
|
+
<Chip variant="filter" trailingIconProps={{ name: 'arrow_drop_down' }}>Sort by</Chip>
|
|
42
43
|
<Chip variant="filter" elevated selected>Elevated selected</Chip>
|
|
43
44
|
<Chip variant="filter" disabled>Disabled</Chip>
|
|
44
45
|
</div>
|
|
@@ -48,6 +49,15 @@
|
|
|
48
49
|
<div class="gap-spacing-200 p-spacing-300 flex flex-wrap items-center">
|
|
49
50
|
<Chip variant="input" iconProps={{ name: 'person' }} onRemove={() => {}}>Ada Lovelace</Chip>
|
|
50
51
|
<Chip variant="input" onRemove={() => {}}>No avatar</Chip>
|
|
52
|
+
<Chip variant="input" onRemove={() => {}}>
|
|
53
|
+
{#snippet avatar()}
|
|
54
|
+
<span
|
|
55
|
+
class="bg-md-sys-color-primary text-md-sys-color-on-primary md-sys-typescale-label-small flex size-full items-center justify-center"
|
|
56
|
+
>GH</span
|
|
57
|
+
>
|
|
58
|
+
{/snippet}
|
|
59
|
+
Grace Hopper
|
|
60
|
+
</Chip>
|
|
51
61
|
<Chip variant="input" selected iconProps={{ name: 'person' }} onRemove={() => {}}>Selected</Chip
|
|
52
62
|
>
|
|
53
63
|
<Chip variant="input" disabled iconProps={{ name: 'person' }} onRemove={() => {}}>Disabled</Chip
|
|
@@ -4,7 +4,9 @@ Chips help people enter information, make selections, filter content, or trigger
|
|
|
4
4
|
|
|
5
5
|
- Assist: Represents a smart or automated action, such as adding an event to a calendar
|
|
6
6
|
- Filter: Uses tags or descriptive words to filter content; supports a selected state
|
|
7
|
-
- Input: Represents a discrete piece of information entered by a user, and can be removed
|
|
7
|
+
- Input: Represents a discrete piece of information entered by a user, and can be removed. Its
|
|
8
|
+
primary action is a button of its own (focusable, pressable, `onclick`); Backspace or Delete on
|
|
9
|
+
it calls `onRemove`
|
|
8
10
|
- Suggestion: Helps narrow a user's intent by presenting dynamically generated suggestions
|
|
9
11
|
|
|
10
12
|
@see https://m3.material.io/components/chips/overview
|
|
@@ -15,7 +17,6 @@ Chips help people enter information, make selections, filter content, or trigger
|
|
|
15
17
|
import { chip } from './theme.js';
|
|
16
18
|
import clsx from 'clsx';
|
|
17
19
|
import { Button, Toggle, type ButtonRootProps, type ToggleRootProps } from 'bits-ui';
|
|
18
|
-
import type { HTMLAttributes } from 'svelte/elements';
|
|
19
20
|
|
|
20
21
|
let {
|
|
21
22
|
children,
|
|
@@ -25,8 +26,10 @@ Chips help people enter information, make selections, filter content, or trigger
|
|
|
25
26
|
onPressedChange,
|
|
26
27
|
disabled = false,
|
|
27
28
|
iconProps,
|
|
29
|
+
trailingIconProps,
|
|
28
30
|
avatar,
|
|
29
31
|
onRemove,
|
|
32
|
+
removeLabel = 'Remove',
|
|
30
33
|
formaction,
|
|
31
34
|
class: className,
|
|
32
35
|
...restProps
|
|
@@ -37,10 +40,37 @@ Chips help people enter information, make selections, filter content, or trigger
|
|
|
37
40
|
const isSelected = $derived(variant === 'filter' || variant === 'input' ? selected : false);
|
|
38
41
|
const showCheck = $derived(isFilter && selected);
|
|
39
42
|
|
|
40
|
-
const
|
|
41
|
-
|
|
43
|
+
const leadingKind = $derived(
|
|
44
|
+
showCheck ? 'icon' : avatar ? 'avatar' : iconProps ? 'icon' : 'none'
|
|
45
|
+
);
|
|
46
|
+
const removable = $derived(variant === 'input' && Boolean(onRemove));
|
|
47
|
+
const hasTrailingIcon = $derived(isFilter && Boolean(trailingIconProps));
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
base,
|
|
51
|
+
icon,
|
|
52
|
+
avatar: avatarCls,
|
|
53
|
+
action,
|
|
54
|
+
label,
|
|
55
|
+
trailing,
|
|
56
|
+
checkIcon
|
|
57
|
+
} = $derived(
|
|
58
|
+
chip({
|
|
59
|
+
variant,
|
|
60
|
+
elevated: isElevated,
|
|
61
|
+
selected: isSelected,
|
|
62
|
+
leading: leadingKind,
|
|
63
|
+
trailing: removable || hasTrailingIcon,
|
|
64
|
+
disabled
|
|
65
|
+
})
|
|
42
66
|
);
|
|
43
67
|
const chipCls = $derived(base({ class: clsx(className) }));
|
|
68
|
+
|
|
69
|
+
function removeOnKey(e: KeyboardEvent) {
|
|
70
|
+
if (!onRemove || disabled || (e.key !== 'Backspace' && e.key !== 'Delete')) return;
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
onRemove();
|
|
73
|
+
}
|
|
44
74
|
</script>
|
|
45
75
|
|
|
46
76
|
{#snippet leading()}
|
|
@@ -49,14 +79,15 @@ Chips help people enter information, make selections, filter content, or trigger
|
|
|
49
79
|
viewBox="0 0 24 24"
|
|
50
80
|
xmlns="http://www.w3.org/2000/svg"
|
|
51
81
|
class={checkIcon()}
|
|
82
|
+
aria-hidden="true"
|
|
52
83
|
fill="currentColor"
|
|
53
84
|
>
|
|
54
85
|
<path d="M9.55 18 3.85 12.3 5.275 10.875 9.55 15.15 18.725 5.975 20.15 7.4Z" />
|
|
55
86
|
</svg>
|
|
56
87
|
{:else if avatar}
|
|
57
|
-
{@render avatar()}
|
|
88
|
+
<span class={avatarCls()}>{@render avatar()}</span>
|
|
58
89
|
{:else if iconProps}
|
|
59
|
-
<Icon size="
|
|
90
|
+
<Icon size="sm" {...iconProps} class={icon({ class: clsx(iconProps.class) })} />
|
|
60
91
|
{/if}
|
|
61
92
|
{/snippet}
|
|
62
93
|
|
|
@@ -71,18 +102,28 @@ Chips help people enter information, make selections, filter content, or trigger
|
|
|
71
102
|
>
|
|
72
103
|
{@render leading()}
|
|
73
104
|
<span class={label()}>{@render children()}</span>
|
|
105
|
+
{#if trailingIconProps}
|
|
106
|
+
<Icon
|
|
107
|
+
size="sm"
|
|
108
|
+
{...trailingIconProps}
|
|
109
|
+
class={icon({ class: ['text-current', clsx(trailingIconProps.class)] })}
|
|
110
|
+
/>
|
|
111
|
+
{/if}
|
|
74
112
|
<Layer />
|
|
75
113
|
</Toggle.Root>
|
|
76
114
|
{:else if variant === 'input'}
|
|
77
|
-
<div
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
115
|
+
<div class={chipCls} data-cy="m3-chip" data-disabled={disabled ? true : undefined}>
|
|
116
|
+
<Button.Root
|
|
117
|
+
{disabled}
|
|
118
|
+
class={action()}
|
|
119
|
+
data-chip-action
|
|
120
|
+
onkeydown={removeOnKey}
|
|
121
|
+
{...restProps as ButtonRootProps}
|
|
122
|
+
>
|
|
123
|
+
{@render leading()}
|
|
124
|
+
<span class={label()}>{@render children()}</span>
|
|
125
|
+
<Layer />
|
|
126
|
+
</Button.Root>
|
|
86
127
|
{#if onRemove}
|
|
87
128
|
<button
|
|
88
129
|
type="button"
|
|
@@ -92,13 +133,12 @@ Chips help people enter information, make selections, filter content, or trigger
|
|
|
92
133
|
e.stopPropagation();
|
|
93
134
|
onRemove?.();
|
|
94
135
|
}}
|
|
95
|
-
aria-label=
|
|
136
|
+
aria-label={removeLabel}
|
|
96
137
|
>
|
|
97
|
-
<Icon size="
|
|
138
|
+
<Icon size="sm" name="close" class="size-[18px] text-[18px]" />
|
|
98
139
|
<Layer />
|
|
99
140
|
</button>
|
|
100
141
|
{/if}
|
|
101
|
-
<Layer />
|
|
102
142
|
</div>
|
|
103
143
|
{:else}
|
|
104
144
|
<Button.Root
|
|
@@ -4,7 +4,9 @@ import type { ChipProps } from './types.js';
|
|
|
4
4
|
*
|
|
5
5
|
* - Assist: Represents a smart or automated action, such as adding an event to a calendar
|
|
6
6
|
* - Filter: Uses tags or descriptive words to filter content; supports a selected state
|
|
7
|
-
* - Input: Represents a discrete piece of information entered by a user, and can be removed
|
|
7
|
+
* - Input: Represents a discrete piece of information entered by a user, and can be removed. Its
|
|
8
|
+
* primary action is a button of its own (focusable, pressable, `onclick`); Backspace or Delete on
|
|
9
|
+
* it calls `onRemove`
|
|
8
10
|
* - Suggestion: Helps narrow a user's intent by presenting dynamically generated suggestions
|
|
9
11
|
*
|
|
10
12
|
* @see https://m3.material.io/components/chips/overview
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
3
|
+
import Chip from './Chip.svelte';
|
|
4
|
+
import ChipGroup from './ChipGroup.svelte';
|
|
5
|
+
|
|
6
|
+
const { Story } = defineMeta({
|
|
7
|
+
title: 'Chips/Chip Group',
|
|
8
|
+
component: ChipGroup,
|
|
9
|
+
tags: ['autodocs']
|
|
10
|
+
});
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
let people = $state([
|
|
15
|
+
'Ada Lovelace',
|
|
16
|
+
'Grace Hopper',
|
|
17
|
+
'Alan Turing',
|
|
18
|
+
'Katherine Johnson',
|
|
19
|
+
'Edsger Dijkstra'
|
|
20
|
+
]);
|
|
21
|
+
let filters = $state([
|
|
22
|
+
{ id: 'wifi', label: 'Wifi', selected: true },
|
|
23
|
+
{ id: 'parking', label: 'Parking', selected: false },
|
|
24
|
+
{ id: 'pool', label: 'Pool', selected: false },
|
|
25
|
+
{ id: 'gym', label: 'Gym', selected: true },
|
|
26
|
+
{ id: 'pets', label: 'Pets allowed', selected: false },
|
|
27
|
+
{ id: 'breakfast', label: 'Breakfast', selected: false }
|
|
28
|
+
]);
|
|
29
|
+
let tags = $state(['Design', 'Svelte', 'Motion', 'Accessibility']);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<Story name="Reorderable input chips" asChild>
|
|
33
|
+
<div class="p-spacing-300 max-w-md">
|
|
34
|
+
<ChipGroup bind:items={people} key={(name) => name} reorderable aria-label="Recipients">
|
|
35
|
+
{#snippet chip(name)}
|
|
36
|
+
<Chip
|
|
37
|
+
variant="input"
|
|
38
|
+
iconProps={{ name: 'person' }}
|
|
39
|
+
removeLabel="Remove {name}"
|
|
40
|
+
onRemove={() => (people = people.filter((p) => p !== name))}
|
|
41
|
+
>
|
|
42
|
+
{name}
|
|
43
|
+
</Chip>
|
|
44
|
+
{/snippet}
|
|
45
|
+
</ChipGroup>
|
|
46
|
+
</div>
|
|
47
|
+
</Story>
|
|
48
|
+
|
|
49
|
+
<Story name="Reorderable filter chips" asChild>
|
|
50
|
+
<div class="p-spacing-300 max-w-md">
|
|
51
|
+
<ChipGroup
|
|
52
|
+
bind:items={filters}
|
|
53
|
+
key={(f) => f.id}
|
|
54
|
+
itemLabel={(f) => f.label}
|
|
55
|
+
reorderable
|
|
56
|
+
aria-label="Amenities"
|
|
57
|
+
>
|
|
58
|
+
{#snippet chip(f)}
|
|
59
|
+
<Chip variant="filter" bind:selected={f.selected}>{f.label}</Chip>
|
|
60
|
+
{/snippet}
|
|
61
|
+
</ChipGroup>
|
|
62
|
+
</div>
|
|
63
|
+
</Story>
|
|
64
|
+
|
|
65
|
+
<Story name="Static set" asChild>
|
|
66
|
+
<div class="p-spacing-300 max-w-md">
|
|
67
|
+
<ChipGroup items={tags} key={(t) => t} aria-label="Topics">
|
|
68
|
+
{#snippet chip(tag)}
|
|
69
|
+
<Chip variant="assist">{tag}</Chip>
|
|
70
|
+
{/snippet}
|
|
71
|
+
</ChipGroup>
|
|
72
|
+
</div>
|
|
73
|
+
</Story>
|