@classic-homes/theme-svelte 1.26.0 → 1.28.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.
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
|
3
3
|
import { cn } from '../utils.js';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
5
|
+
import type { Attachment } from 'svelte/attachments';
|
|
5
6
|
import Icon, { type IconName } from './Icon.svelte';
|
|
6
7
|
import { buttonVariants, type ButtonVariants } from './button-variants.js';
|
|
7
8
|
|
|
@@ -60,6 +61,19 @@
|
|
|
60
61
|
* so a text trigger (e.g. header navigation) reads as a menu rather than a link.
|
|
61
62
|
*/
|
|
62
63
|
showChevron?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Disable the trigger `<button>`: sets native `disabled` and stops the menu from opening
|
|
66
|
+
* (pointer and keyboard). Use this instead of nesting a disabled `<Button>` in `trigger`.
|
|
67
|
+
*/
|
|
68
|
+
triggerDisabled?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Keep the trigger's pointer, mouse, click and opening-key (Enter, Space, ArrowDown)
|
|
71
|
+
* events from bubbling past the trigger, so a menu inside a clickable row or card
|
|
72
|
+
* opens without also activating the row. The menu still opens as usual.
|
|
73
|
+
*/
|
|
74
|
+
triggerStopPropagation?: boolean;
|
|
75
|
+
/** Called on a click of the trigger, before the event bubbles to its ancestors */
|
|
76
|
+
onTriggerClick?: (event: MouseEvent) => void;
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
let {
|
|
@@ -75,8 +89,56 @@
|
|
|
75
89
|
triggerSize = 'sm',
|
|
76
90
|
triggerLabel,
|
|
77
91
|
showChevron = false,
|
|
92
|
+
triggerDisabled = false,
|
|
93
|
+
triggerStopPropagation = false,
|
|
94
|
+
onTriggerClick,
|
|
78
95
|
}: Props = $props();
|
|
79
96
|
|
|
97
|
+
type TriggerHandler = ((event: Event) => void) | undefined;
|
|
98
|
+
|
|
99
|
+
/** Keys the bits-ui trigger acts on (toggle/open), so they are the row-activating ones too. */
|
|
100
|
+
const OPENING_KEYS = new Set(['Enter', ' ', 'ArrowDown']);
|
|
101
|
+
|
|
102
|
+
/** Events that would reach a clickable ancestor (row click, row link, row keyboard activate). */
|
|
103
|
+
const ISOLATED_EVENTS = [
|
|
104
|
+
'pointerdown',
|
|
105
|
+
'pointerup',
|
|
106
|
+
'mousedown',
|
|
107
|
+
'mouseup',
|
|
108
|
+
'click',
|
|
109
|
+
'auxclick',
|
|
110
|
+
'keydown',
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* With `triggerStopPropagation`, listen natively on the trigger itself: run the bits-ui
|
|
115
|
+
* handler (and `onTriggerClick`) at the target, then stop the event there. Svelte's
|
|
116
|
+
* delegated handlers run at the root, so a stop inside one would also keep bits-ui's own
|
|
117
|
+
* (delegated) handlers from running and could not stop native listeners on ancestors.
|
|
118
|
+
*/
|
|
119
|
+
function isolateTrigger(props: Record<string, unknown>): Attachment<HTMLButtonElement> {
|
|
120
|
+
return (node) => {
|
|
121
|
+
const listeners = ISOLATED_EVENTS.map((type) => {
|
|
122
|
+
const listener = (event: Event) => {
|
|
123
|
+
(props[`on${type}`] as TriggerHandler)?.(event);
|
|
124
|
+
if (type === 'click') onTriggerClick?.(event as MouseEvent);
|
|
125
|
+
if (type === 'keydown' && !OPENING_KEYS.has((event as KeyboardEvent).key)) return;
|
|
126
|
+
event.stopPropagation();
|
|
127
|
+
};
|
|
128
|
+
node.addEventListener(type, listener);
|
|
129
|
+
return () => node.removeEventListener(type, listener);
|
|
130
|
+
});
|
|
131
|
+
return () => listeners.forEach((remove) => remove());
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Trigger props without the event handlers that `isolateTrigger` calls itself. */
|
|
136
|
+
function withoutHandlers(props: Record<string, unknown>): Record<string, unknown> {
|
|
137
|
+
const rest = { ...props };
|
|
138
|
+
for (const type of ISOLATED_EVENTS) delete rest[`on${type}`];
|
|
139
|
+
return rest;
|
|
140
|
+
}
|
|
141
|
+
|
|
80
142
|
function handleOpenChange(newOpen: boolean) {
|
|
81
143
|
open = newOpen;
|
|
82
144
|
onOpenChange?.(newOpen);
|
|
@@ -87,6 +149,21 @@
|
|
|
87
149
|
}
|
|
88
150
|
</script>
|
|
89
151
|
|
|
152
|
+
{#snippet triggerContent()}
|
|
153
|
+
{@render trigger()}
|
|
154
|
+
{#if showChevron}
|
|
155
|
+
<Icon
|
|
156
|
+
name="chevron-down"
|
|
157
|
+
size={16}
|
|
158
|
+
class={cn(
|
|
159
|
+
'ml-1 shrink-0 opacity-60 transition-transform duration-200 motion-reduce:transition-none',
|
|
160
|
+
open && 'rotate-180'
|
|
161
|
+
)}
|
|
162
|
+
aria-hidden="true"
|
|
163
|
+
/>
|
|
164
|
+
{/if}
|
|
165
|
+
{/snippet}
|
|
166
|
+
|
|
90
167
|
{#snippet itemContent(item: DropdownMenuItem)}
|
|
91
168
|
{#if item.icon}
|
|
92
169
|
<Icon name={item.icon} size={16} class="mr-2 shrink-0" aria-hidden="true" />
|
|
@@ -143,29 +220,32 @@
|
|
|
143
220
|
{/snippet}
|
|
144
221
|
|
|
145
222
|
<DropdownMenuPrimitive.Root bind:open onOpenChange={handleOpenChange}>
|
|
146
|
-
<DropdownMenuPrimitive.Trigger>
|
|
223
|
+
<DropdownMenuPrimitive.Trigger disabled={triggerDisabled}>
|
|
147
224
|
{#snippet child({ props })}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
225
|
+
{@const buttonClass = triggerVariant
|
|
226
|
+
? cn(buttonVariants({ variant: triggerVariant, size: triggerSize }), triggerClass)
|
|
227
|
+
: triggerClass}
|
|
228
|
+
{#if triggerStopPropagation}
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
{...withoutHandlers(props)}
|
|
232
|
+
class={buttonClass}
|
|
233
|
+
aria-label={triggerLabel}
|
|
234
|
+
{@attach isolateTrigger(props)}
|
|
235
|
+
>
|
|
236
|
+
{@render triggerContent()}
|
|
237
|
+
</button>
|
|
238
|
+
{:else}
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
{...props}
|
|
242
|
+
class={buttonClass}
|
|
243
|
+
aria-label={triggerLabel}
|
|
244
|
+
onclick={onTriggerClick}
|
|
245
|
+
>
|
|
246
|
+
{@render triggerContent()}
|
|
247
|
+
</button>
|
|
248
|
+
{/if}
|
|
169
249
|
{/snippet}
|
|
170
250
|
</DropdownMenuPrimitive.Trigger>
|
|
171
251
|
|
|
@@ -54,6 +54,19 @@ interface Props {
|
|
|
54
54
|
* so a text trigger (e.g. header navigation) reads as a menu rather than a link.
|
|
55
55
|
*/
|
|
56
56
|
showChevron?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Disable the trigger `<button>`: sets native `disabled` and stops the menu from opening
|
|
59
|
+
* (pointer and keyboard). Use this instead of nesting a disabled `<Button>` in `trigger`.
|
|
60
|
+
*/
|
|
61
|
+
triggerDisabled?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Keep the trigger's pointer, mouse, click and opening-key (Enter, Space, ArrowDown)
|
|
64
|
+
* events from bubbling past the trigger, so a menu inside a clickable row or card
|
|
65
|
+
* opens without also activating the row. The menu still opens as usual.
|
|
66
|
+
*/
|
|
67
|
+
triggerStopPropagation?: boolean;
|
|
68
|
+
/** Called on a click of the trigger, before the event bubbles to its ancestors */
|
|
69
|
+
onTriggerClick?: (event: MouseEvent) => void;
|
|
57
70
|
}
|
|
58
71
|
declare const DropdownMenu: import("svelte").Component<Props, {}, "open">;
|
|
59
72
|
type DropdownMenu = ReturnType<typeof DropdownMenu>;
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import MediaEmbed from './MediaEmbed.svelte';
|
|
33
33
|
import Video from './Video.svelte';
|
|
34
34
|
import MediaTile from './internal/MediaTile.svelte';
|
|
35
|
+
import TriggerChild, { type TriggerSnippetProps } from '../internal/TriggerChild.svelte';
|
|
35
36
|
|
|
36
37
|
/** Horizontal distance that counts as a swipe rather than a tap */
|
|
37
38
|
const SWIPE_THRESHOLD = 60;
|
|
@@ -59,8 +60,14 @@
|
|
|
59
60
|
zoomable?: boolean;
|
|
60
61
|
/** Show a download link for items that expose a URL */
|
|
61
62
|
downloadable?: boolean;
|
|
62
|
-
/**
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Optional trigger element; omit when controlling `open` yourself. It
|
|
65
|
+
* receives `{ props }`: spread them on your own button
|
|
66
|
+
* (`{#snippet trigger({ props })}<Button {...props}>Open</Button>{/snippet}`)
|
|
67
|
+
* so that button is the trigger and gets focus back on close. A snippet
|
|
68
|
+
* that ignores them still works, wrapped in a span trigger as before.
|
|
69
|
+
*/
|
|
70
|
+
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
64
71
|
/** Additional classes for the dialog surface */
|
|
65
72
|
class?: string;
|
|
66
73
|
}
|
|
@@ -81,6 +88,10 @@
|
|
|
81
88
|
class: className,
|
|
82
89
|
}: Props = $props();
|
|
83
90
|
|
|
91
|
+
const uid = $props.id();
|
|
92
|
+
// Changes once if the trigger snippet ignores its props (see TriggerChild)
|
|
93
|
+
let triggerId = $state(`${uid}-trigger`);
|
|
94
|
+
|
|
84
95
|
let swipeStartX = 0;
|
|
85
96
|
let swipeStartY = 0;
|
|
86
97
|
let swipeActive = false;
|
|
@@ -265,11 +276,9 @@
|
|
|
265
276
|
|
|
266
277
|
<DialogPrimitive.Root bind:open onOpenChange={handleOpenChange}>
|
|
267
278
|
{#if trigger}
|
|
268
|
-
<DialogPrimitive.Trigger>
|
|
279
|
+
<DialogPrimitive.Trigger id={triggerId}>
|
|
269
280
|
{#snippet child({ props })}
|
|
270
|
-
<
|
|
271
|
-
{@render trigger()}
|
|
272
|
-
</span>
|
|
281
|
+
<TriggerChild {props} {trigger} onfallback={() => (triggerId = `${uid}-trigger-wrap`)} />
|
|
273
282
|
{/snippet}
|
|
274
283
|
</DialogPrimitive.Trigger>
|
|
275
284
|
{/if}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Snippet } from 'svelte';
|
|
2
2
|
import type { MediaItem } from '../../types/components.js';
|
|
3
|
+
import { type TriggerSnippetProps } from '../internal/TriggerChild.svelte';
|
|
3
4
|
interface Props {
|
|
4
5
|
/** Items to page through */
|
|
5
6
|
items: MediaItem[];
|
|
@@ -23,8 +24,14 @@ interface Props {
|
|
|
23
24
|
zoomable?: boolean;
|
|
24
25
|
/** Show a download link for items that expose a URL */
|
|
25
26
|
downloadable?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Optional trigger element; omit when controlling `open` yourself. It
|
|
29
|
+
* receives `{ props }`: spread them on your own button
|
|
30
|
+
* (`{#snippet trigger({ props })}<Button {...props}>Open</Button>{/snippet}`)
|
|
31
|
+
* so that button is the trigger and gets focus back on close. A snippet
|
|
32
|
+
* that ignores them still works, wrapped in a span trigger as before.
|
|
33
|
+
*/
|
|
34
|
+
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
28
35
|
/** Additional classes for the dialog surface */
|
|
29
36
|
class?: string;
|
|
30
37
|
}
|