@juspay/svelte-ui-components 4.52.1 → 4.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte +3 -2
- package/dist/AttachmentChipRow/properties.d.ts +2 -0
- package/dist/Book/Book.svelte +2 -0
- package/dist/Book/properties.d.ts +2 -0
- package/dist/Chat/Chat.svelte +2 -0
- package/dist/Chat/properties.d.ts +2 -0
- package/dist/Chat/types.d.ts +2 -0
- package/dist/ChatMessage/ChatMessage.svelte +18 -3
- package/dist/ChatMessage/properties.d.ts +6 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +31 -7
- package/dist/ChatMessageList/properties.d.ts +2 -0
- package/dist/Choicebox/Choicebox.svelte +33 -23
- package/dist/DateRangePicker/DateRangePicker.svelte +82 -33
- package/dist/DateRangePicker/properties.d.ts +18 -2
- package/dist/Draggable/Draggable.svelte +4 -2
- package/dist/MediaPlayer/MediaPlayer.svelte +28 -13
- package/dist/MediaPlayer/properties.d.ts +2 -0
- package/dist/Modal/Modal.svelte +30 -43
- package/dist/Modal/properties.d.ts +5 -4
- package/dist/Pagination/Pagination.svelte +2 -0
- package/dist/Pagination/properties.d.ts +2 -0
- package/dist/Pill/Pill.svelte +61 -4
- package/dist/Pill/properties.d.ts +8 -16
- package/dist/Radio/Radio.svelte +6 -2
- package/dist/RatingGroup/RatingGroup.svelte +38 -26
- package/dist/Select/Select.svelte +8 -0
- package/dist/StatCard/StatCard.svelte +68 -17
- package/dist/StatCard/properties.d.ts +1 -1
- package/dist/Stepper/Step.svelte +2 -2
- package/dist/Table/Table.svelte +32 -2
- package/dist/Table/properties.d.ts +11 -5
- package/dist/Tabs/Tabs.svelte +187 -12
- package/dist/ToolCallLog/ToolCallLog.svelte +4 -4
- package/dist/utils.js +20 -2
- package/dist-codemod/codemod/cli.js +4 -4
- package/dist-wc/index.js +3134 -2717
- package/package.json +3 -2
- package/scripts/postinstall.mjs +13 -15
package/README.md
CHANGED
|
@@ -154,6 +154,20 @@ fix to something that did not work.
|
|
|
154
154
|
| **RelativeTime** | Auto-updating relative time display ("5 minutes ago") with locale support and optional tooltip. | [docs](docs/RelativeTime.md) |
|
|
155
155
|
| **AspectRatio** | Constrains content to a fixed width-to-height ratio using CSS `aspect-ratio`, reserving layout space before an image, video or iframe loads. | [docs](docs/AspectRatio.md) |
|
|
156
156
|
|
|
157
|
+
### Charts
|
|
158
|
+
|
|
159
|
+
Pure-SVG charts with no charting-library dependency; each takes plain data arrays.
|
|
160
|
+
|
|
161
|
+
| Component | Description | Docs |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| **AreaChart** | Responsive area chart for volume under a trend: multiple series, regular or 100% stacking, gradient fill, data labels and a hover overlay. | [docs](docs/AreaChart.md) |
|
|
164
|
+
| **BarChart** | Responsive bar chart for comparing categories: vertical or horizontal, single or multi-series (grouped or stacked), value labels, per-point colours and hover tooltips. | [docs](docs/BarChart.md) |
|
|
165
|
+
| **DualAxisBarChart** | Two independent Y-axes over one categorical X-axis; each series declares its own axis and render type. | [docs](docs/DualAxisBarChart.md) |
|
|
166
|
+
| **FunnelChart** | Horizontal funnel: one bar per stage, proportional to its value, with connectors between stages. | [docs](docs/FunnelChart.md) |
|
|
167
|
+
| **LineChart** | Responsive line chart for trends: multiple series, five curve interpolations, gradient fill and an optional area mode. | [docs](docs/LineChart.md) |
|
|
168
|
+
| **PieChart** | Responsive pie or donut chart with slice hover highlighting, a delta badge, custom labels, a legend and donut centre content. | [docs](docs/PieChart.md) |
|
|
169
|
+
| **SankeyChart** | Sankey diagram for flows between nodes, positioned into columns automatically. | [docs](docs/SankeyChart.md) |
|
|
170
|
+
|
|
157
171
|
### Feedback & Loading
|
|
158
172
|
|
|
159
173
|
| Component | Description | Docs |
|
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
removeIcon,
|
|
27
27
|
fileIcon,
|
|
28
28
|
testId,
|
|
29
|
-
classes
|
|
29
|
+
classes,
|
|
30
|
+
ariaLabel = 'Pending attachments'
|
|
30
31
|
}: AttachmentChipRowProperties = $props();
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
34
|
{#if images.length > 0 || videos.length > 0 || files.length > 0}
|
|
34
35
|
<Scroller
|
|
35
36
|
direction="horizontal"
|
|
36
|
-
ariaLabel
|
|
37
|
+
{ariaLabel}
|
|
37
38
|
showArrows={false}
|
|
38
39
|
hideScrollbar={false}
|
|
39
40
|
showGradient={false}
|
package/dist/Book/Book.svelte
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
showNavigation = true,
|
|
12
12
|
showPageIndicator = true,
|
|
13
13
|
enableSwipe = false,
|
|
14
|
+
ariaLabel = 'Book',
|
|
14
15
|
testId,
|
|
15
16
|
previousIcon,
|
|
16
17
|
nextIcon,
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
onmousedown={handleMouseDown}
|
|
109
110
|
onmouseup={handleMouseUp}
|
|
110
111
|
role="region"
|
|
112
|
+
aria-label={ariaLabel}
|
|
111
113
|
tabindex="0"
|
|
112
114
|
>
|
|
113
115
|
<div class="book-viewport">
|
|
@@ -14,6 +14,8 @@ export type OptionalBookProperties = {
|
|
|
14
14
|
showNavigation?: boolean;
|
|
15
15
|
showPageIndicator?: boolean;
|
|
16
16
|
enableSwipe?: boolean;
|
|
17
|
+
/** Accessible name for the keyboard-operated book region. Defaults to "Book". */
|
|
18
|
+
ariaLabel?: string;
|
|
17
19
|
testId?: string;
|
|
18
20
|
previousIcon?: Snippet;
|
|
19
21
|
nextIcon?: Snippet;
|
package/dist/Chat/Chat.svelte
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
hideScrollbar = false,
|
|
24
24
|
jump = true,
|
|
25
25
|
jumpLabel = 'Jump to latest',
|
|
26
|
+
messageListLabel,
|
|
26
27
|
jumpIcon,
|
|
27
28
|
toolStatus = null,
|
|
28
29
|
suggestions = [],
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
{/if}
|
|
98
99
|
|
|
99
100
|
<ChatMessageList
|
|
101
|
+
ariaLabel={messageListLabel ?? (title.trim() ? `${title} messages` : 'Conversation messages')}
|
|
100
102
|
{messages}
|
|
101
103
|
{autoscroll}
|
|
102
104
|
{scrollPolicy}
|
|
@@ -23,6 +23,8 @@ export type OptionalChatProperties = {
|
|
|
23
23
|
hideScrollbar?: ChatMessageListProperties['hideScrollbar'];
|
|
24
24
|
jump?: ChatMessageListProperties['jump'];
|
|
25
25
|
jumpLabel?: ChatMessageListProperties['jumpLabel'];
|
|
26
|
+
/** Transcript name; defaults to the chat title plus `messages`, or `Conversation messages`. */
|
|
27
|
+
messageListLabel?: ChatMessageListProperties['ariaLabel'];
|
|
26
28
|
jumpIcon?: ChatMessageListProperties['jumpIcon'];
|
|
27
29
|
toolStatus?: ChatToolStatus | null;
|
|
28
30
|
suggestions?: ChatSuggestion[];
|
package/dist/Chat/types.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type ChatMessageData = {
|
|
|
11
11
|
* MarkdownText). Wins over `html` and `content` in the bubble.
|
|
12
12
|
*/
|
|
13
13
|
markdown?: string;
|
|
14
|
+
/** Accessible table scroll-region name for static and streamed markdown bubbles. */
|
|
15
|
+
markdownTableLabel?: string;
|
|
14
16
|
streaming?: boolean;
|
|
15
17
|
/**
|
|
16
18
|
* Reveal this message progressively as it grows. Opt-in per message so it cannot stack with
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
import { onDestroy, onMount } from 'svelte';
|
|
11
11
|
import { partyOf } from '../Chat/roles';
|
|
12
12
|
import type { ChatMessageProperties } from './properties';
|
|
13
|
+
import type { RenderMarkdownOptions } from '../MarkdownText/properties';
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
16
|
role,
|
|
16
17
|
content = '',
|
|
17
18
|
html,
|
|
18
19
|
markdown,
|
|
20
|
+
markdownTableLabel,
|
|
19
21
|
body,
|
|
20
22
|
streaming = false,
|
|
21
23
|
typewriter = false,
|
|
@@ -90,7 +92,9 @@
|
|
|
90
92
|
`marked` — a peer dependency — is only pulled in at runtime, never during
|
|
91
93
|
SSR, and rendering `markdown` degrades to the `html`/`content` fallback
|
|
92
94
|
until the module resolves or when the peer is absent. */
|
|
93
|
-
let renderMarkdownFn = $state<
|
|
95
|
+
let renderMarkdownFn = $state<
|
|
96
|
+
((source: string, options?: RenderMarkdownOptions) => string) | null
|
|
97
|
+
>(null);
|
|
94
98
|
|
|
95
99
|
onMount(() => {
|
|
96
100
|
void import('../MarkdownText/markdown')
|
|
@@ -102,8 +106,19 @@
|
|
|
102
106
|
});
|
|
103
107
|
});
|
|
104
108
|
|
|
109
|
+
// Both rendering branches share the same options-aware renderer. Capture the label
|
|
110
|
+
// reactively so a locale/name update also refreshes already-revealed typewriter HTML.
|
|
111
|
+
let messageMarkdownRenderer = $derived.by(() => {
|
|
112
|
+
const renderer = renderMarkdownFn;
|
|
113
|
+
if (renderer === null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const options: RenderMarkdownOptions =
|
|
117
|
+
typeof markdownTableLabel === 'string' ? { tableLabel: markdownTableLabel } : {};
|
|
118
|
+
return (source: string) => renderer(source, options);
|
|
119
|
+
});
|
|
105
120
|
let effectiveHtml = $derived(
|
|
106
|
-
hasMarkdown &&
|
|
121
|
+
hasMarkdown && messageMarkdownRenderer !== null ? messageMarkdownRenderer(markdown ?? '') : html
|
|
107
122
|
);
|
|
108
123
|
let hasHtml = $derived(typeof effectiveHtml === 'string' && effectiveHtml.length > 0);
|
|
109
124
|
|
|
@@ -128,7 +143,7 @@
|
|
|
128
143
|
(!hasMarkdown || renderMarkdownFn !== null)
|
|
129
144
|
);
|
|
130
145
|
let typewriterRenderText = $derived(
|
|
131
|
-
hasMarkdown &&
|
|
146
|
+
hasMarkdown && messageMarkdownRenderer !== null ? { renderText: messageMarkdownRenderer } : {}
|
|
132
147
|
);
|
|
133
148
|
// Clamped at the boundary the way LoadingDots clamps `dots`, so the prop's floor is part
|
|
134
149
|
// of this component's contract rather than something a consumer discovers. The finite
|
|
@@ -20,6 +20,12 @@ export type OptionalChatMessageProperties = {
|
|
|
20
20
|
* (for server-rendered markdown use `MarkdownText` or `renderMarkdown`).
|
|
21
21
|
*/
|
|
22
22
|
markdown?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Accessible name for each table scroll region rendered from `markdown`, including
|
|
25
|
+
* progressive/typewriter rendering. Omitted or empty leaves the shared renderer's
|
|
26
|
+
* unnamed, keyboard-scrollable wrapper unchanged. Does not alter `html` or `body`.
|
|
27
|
+
*/
|
|
28
|
+
markdownTableLabel?: string;
|
|
23
29
|
/**
|
|
24
30
|
* Replaces the rendered bubble body with arbitrary markup while keeping the
|
|
25
31
|
* message chrome (role styling, avatar, header, attachments, copy/retry/
|
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
onretry,
|
|
29
29
|
onfeedback,
|
|
30
30
|
testId,
|
|
31
|
-
classes
|
|
31
|
+
classes,
|
|
32
|
+
ariaLabel = 'Conversation messages'
|
|
32
33
|
}: ChatMessageListProperties = $props();
|
|
33
34
|
|
|
34
35
|
let listEl: HTMLElement | null = $state(null);
|
|
35
36
|
let innerEl: HTMLElement | null = $state(null);
|
|
36
37
|
let atBottom = $state(true);
|
|
37
38
|
let scrollable = $state(false);
|
|
39
|
+
let keyboardScrollable = $state(false);
|
|
38
40
|
let pinActive = false;
|
|
39
41
|
// Set when a turn without pinHold has asked for its reservation back, but the reply is not yet
|
|
40
42
|
// tall enough to hold the pinned position without it. Cleared by tryReleasePin.
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
|
|
79
81
|
function reportScrollState(node: HTMLElement): void {
|
|
80
82
|
atBottom = isNearBottom(node);
|
|
83
|
+
keyboardScrollable = node.scrollHeight - node.clientHeight > 1;
|
|
81
84
|
scrollable = node.scrollHeight - node.clientHeight > NEAR_BOTTOM_THRESHOLD;
|
|
82
85
|
onscrollstate?.({ atBottom, scrollable });
|
|
83
86
|
}
|
|
@@ -260,19 +263,29 @@
|
|
|
260
263
|
if (typeof ResizeObserver === 'undefined') {
|
|
261
264
|
return;
|
|
262
265
|
}
|
|
266
|
+
let pendingFrame: number | null = null;
|
|
263
267
|
const observer = new ResizeObserver(() => {
|
|
264
|
-
if (
|
|
268
|
+
if (pendingFrame !== null) {
|
|
265
269
|
return;
|
|
266
270
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
pendingFrame = requestAnimationFrame(() => {
|
|
272
|
+
pendingFrame = null;
|
|
273
|
+
if (listEl === null || !node.isConnected) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (scrollPolicy === 'near-bottom' && autoscroll && atBottom) {
|
|
277
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
278
|
+
}
|
|
279
|
+
tryReleasePin();
|
|
280
|
+
reportScrollState(listEl);
|
|
281
|
+
});
|
|
272
282
|
});
|
|
273
283
|
observer.observe(node);
|
|
274
284
|
return {
|
|
275
285
|
destroy(): void {
|
|
286
|
+
if (pendingFrame !== null) {
|
|
287
|
+
cancelAnimationFrame(pendingFrame);
|
|
288
|
+
}
|
|
276
289
|
observer.disconnect();
|
|
277
290
|
}
|
|
278
291
|
};
|
|
@@ -291,9 +304,14 @@
|
|
|
291
304
|
};
|
|
292
305
|
</script>
|
|
293
306
|
|
|
307
|
+
<!-- The transcript is an independent scrolling surface. Native keyboard focus
|
|
308
|
+
enables the browser's scroll keys while it overflows, even without actions. -->
|
|
309
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
294
310
|
<div
|
|
295
311
|
class="chat-message-list {classes ?? ''}"
|
|
296
312
|
role="log"
|
|
313
|
+
aria-label={ariaLabel}
|
|
314
|
+
tabindex={keyboardScrollable ? 0 : null}
|
|
297
315
|
aria-live="polite"
|
|
298
316
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
299
317
|
testID={typeof testId === 'string' ? testId : null}
|
|
@@ -323,6 +341,7 @@
|
|
|
323
341
|
content={msg.content}
|
|
324
342
|
html={msg.html}
|
|
325
343
|
markdown={msg.markdown}
|
|
344
|
+
markdownTableLabel={msg.markdownTableLabel}
|
|
326
345
|
body={typeof messageBody === 'function' ? bodyFor : null}
|
|
327
346
|
streaming={msg.streaming}
|
|
328
347
|
typewriter={msg.typewriter}
|
|
@@ -352,6 +371,11 @@
|
|
|
352
371
|
</div>
|
|
353
372
|
|
|
354
373
|
<style>
|
|
374
|
+
.chat-message-list:focus-visible {
|
|
375
|
+
outline: var(--chat-message-list-focus-outline, 2px solid currentColor);
|
|
376
|
+
outline-offset: -2px;
|
|
377
|
+
}
|
|
378
|
+
|
|
355
379
|
.chat-message-list {
|
|
356
380
|
box-sizing: border-box;
|
|
357
381
|
display: flex;
|
|
@@ -44,6 +44,8 @@ export type OptionalChatMessageListProperties = {
|
|
|
44
44
|
allowCopy?: boolean;
|
|
45
45
|
testId?: string;
|
|
46
46
|
classes?: string;
|
|
47
|
+
/** Accessible transcript name. Default: `Conversation messages`. */
|
|
48
|
+
ariaLabel?: string;
|
|
47
49
|
};
|
|
48
50
|
export type ChatMessageListEventProperties = {
|
|
49
51
|
/**
|
|
@@ -82,12 +82,20 @@
|
|
|
82
82
|
onclick?.(selected);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
// Let native validation choose the first invalid control. Redirect only its
|
|
86
|
+
// actual focus to the visible card, so checkValidity() stays a silent query
|
|
87
|
+
// and later invalid fields cannot replace the first field's focus.
|
|
88
|
+
function handleNativeFocus(event: FocusEvent & { currentTarget: HTMLInputElement }): void {
|
|
89
|
+
const carrier = event.currentTarget;
|
|
90
|
+
// Finish native interactive validation before moving focus. Firefox can
|
|
91
|
+
// open its validation UI after a synchronous handoff and swallow the next
|
|
92
|
+
// pointer click. A later owner or an unmounted card must keep its focus.
|
|
93
|
+
queueMicrotask(() => {
|
|
94
|
+
const root = carrier.getRootNode();
|
|
95
|
+
if (card?.isConnected && 'activeElement' in root && root.activeElement === carrier) {
|
|
96
|
+
card.focus();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
function handleKeyDown(e: KeyboardEvent): void {
|
|
@@ -143,21 +151,6 @@
|
|
|
143
151
|
testID={testId}
|
|
144
152
|
{@attach groupMembership}
|
|
145
153
|
>
|
|
146
|
-
<input
|
|
147
|
-
type={mode === 'radio' ? 'radio' : 'checkbox'}
|
|
148
|
-
class="native-control"
|
|
149
|
-
checked={selected}
|
|
150
|
-
{disabled}
|
|
151
|
-
{value}
|
|
152
|
-
{required}
|
|
153
|
-
name={typeof name === 'string' ? name : null}
|
|
154
|
-
form={typeof form === 'string' ? form : null}
|
|
155
|
-
oninvalid={handleInvalid}
|
|
156
|
-
tabindex={-1}
|
|
157
|
-
aria-hidden="true"
|
|
158
|
-
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
159
|
-
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
160
|
-
/>
|
|
161
154
|
{#if typeof children === 'function'}
|
|
162
155
|
{@render children()}
|
|
163
156
|
{/if}
|
|
@@ -171,6 +164,23 @@
|
|
|
171
164
|
{/if}
|
|
172
165
|
</div>
|
|
173
166
|
|
|
167
|
+
<!-- Native form participation remains outside the interactive accessibility owner. -->
|
|
168
|
+
<input
|
|
169
|
+
type={mode === 'radio' ? 'radio' : 'checkbox'}
|
|
170
|
+
class="native-control"
|
|
171
|
+
checked={selected}
|
|
172
|
+
{disabled}
|
|
173
|
+
{value}
|
|
174
|
+
{required}
|
|
175
|
+
name={typeof name === 'string' ? name : null}
|
|
176
|
+
form={typeof form === 'string' ? form : null}
|
|
177
|
+
onfocus={handleNativeFocus}
|
|
178
|
+
tabindex={-1}
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
181
|
+
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
182
|
+
/>
|
|
183
|
+
|
|
174
184
|
{#if field.showsError}
|
|
175
185
|
<div
|
|
176
186
|
id={field.errorId}
|
|
@@ -197,8 +207,8 @@
|
|
|
197
207
|
.native-control {
|
|
198
208
|
position: absolute;
|
|
199
209
|
opacity: 0;
|
|
200
|
-
width:
|
|
201
|
-
height:
|
|
210
|
+
width: 1px;
|
|
211
|
+
height: 1px;
|
|
202
212
|
margin: 0;
|
|
203
213
|
pointer-events: none;
|
|
204
214
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type {
|
|
3
|
+
DateRangeCompareDraft,
|
|
3
4
|
DateRangePickerProperties,
|
|
4
5
|
DateRangePreset,
|
|
5
6
|
TimeDisplayBoundary
|
|
@@ -95,8 +96,11 @@
|
|
|
95
96
|
let draftStart: Date | null = $state(null);
|
|
96
97
|
let draftEnd: Date | null = $state(null);
|
|
97
98
|
let draftValue: Date | null = $state(null);
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
// The compare range is a draft too. It is handed to `compareCalendar` so a consumer's calendar
|
|
100
|
+
// has somewhere to write that Apply commits and Cancel discards; `compareSnapshot` is what the
|
|
101
|
+
// committed range was when the session opened, which is what Cancel restores.
|
|
102
|
+
const compareDraft: DateRangeCompareDraft = $state({ start: null, end: null });
|
|
103
|
+
let compareSnapshot: DateRangeCompareDraft = $state({ start: null, end: null });
|
|
100
104
|
|
|
101
105
|
// Tracks the label of the currently active preset, or null when the user
|
|
102
106
|
// made a custom calendar selection (or before any selection is made).
|
|
@@ -339,6 +343,67 @@
|
|
|
339
343
|
}
|
|
340
344
|
}
|
|
341
345
|
|
|
346
|
+
const sameInstant = (a: Date | null, b: Date | null): boolean =>
|
|
347
|
+
(a === null ? null : a.getTime()) === (b === null ? null : b.getTime());
|
|
348
|
+
|
|
349
|
+
// The compare calendar lives in the main panel unless a standalone `compareTrigger` gives it
|
|
350
|
+
// its own panel and its own Apply/Cancel.
|
|
351
|
+
const compareInMain: boolean = $derived(
|
|
352
|
+
typeof compareCalendar === 'function' && typeof compareTrigger !== 'function'
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
function seedCompareSession(): void {
|
|
356
|
+
compareSnapshot = { start: compareStart, end: compareEnd };
|
|
357
|
+
compareDraft.start = compareStart;
|
|
358
|
+
compareDraft.end = compareEnd;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// What Apply commits. A calendar bound to `compareDraft` edits the draft. A calendar bound
|
|
362
|
+
// straight to `compareStart`/`compareEnd` (how this component's docs wired it before the draft
|
|
363
|
+
// was exposed) edits the committed values in place and leaves the draft at its seed; Apply used
|
|
364
|
+
// to write that stale seed back over the pick, and could not be pressed at all on first use.
|
|
365
|
+
const pendingCompare = $derived.by(() => {
|
|
366
|
+
const edited =
|
|
367
|
+
!sameInstant(compareDraft.start, compareSnapshot.start) ||
|
|
368
|
+
!sameInstant(compareDraft.end, compareSnapshot.end);
|
|
369
|
+
return edited
|
|
370
|
+
? { start: compareDraft.start, end: compareDraft.end }
|
|
371
|
+
: { start: compareStart, end: compareEnd };
|
|
372
|
+
});
|
|
373
|
+
const canApplyCompare: boolean = $derived(
|
|
374
|
+
pendingCompare.start !== null && pendingCompare.end !== null
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
function commitCompare(): void {
|
|
378
|
+
const { start, end } = pendingCompare;
|
|
379
|
+
if (start === null || end === null) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
compareStart = start;
|
|
383
|
+
compareEnd = end;
|
|
384
|
+
onapplycompare?.({ compareStart: start, compareEnd: end, presetLabel: selectedPresetLabel });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Cancel, Escape and an outside press all land here: whatever the session did to the draft or
|
|
388
|
+
// to the committed range goes back to what it was when the session opened.
|
|
389
|
+
function discardCompare(): void {
|
|
390
|
+
if (!sameInstant(compareStart, compareSnapshot.start)) {
|
|
391
|
+
compareStart = compareSnapshot.start;
|
|
392
|
+
}
|
|
393
|
+
if (!sameInstant(compareEnd, compareSnapshot.end)) {
|
|
394
|
+
compareEnd = compareSnapshot.end;
|
|
395
|
+
}
|
|
396
|
+
compareDraft.start = compareSnapshot.start;
|
|
397
|
+
compareDraft.end = compareSnapshot.end;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function abandonPicker(): void {
|
|
401
|
+
if (compareInMain) {
|
|
402
|
+
discardCompare();
|
|
403
|
+
}
|
|
404
|
+
closePicker();
|
|
405
|
+
}
|
|
406
|
+
|
|
342
407
|
function openPicker(): void {
|
|
343
408
|
// Pointer activation need not focus a button in WebKit. Return to the
|
|
344
409
|
// invoking control, not an unrelated previously focused field or body.
|
|
@@ -347,8 +412,9 @@
|
|
|
347
412
|
draftStart = rangeStart;
|
|
348
413
|
draftEnd = rangeEnd;
|
|
349
414
|
draftValue = value;
|
|
350
|
-
|
|
351
|
-
|
|
415
|
+
if (compareInMain) {
|
|
416
|
+
seedCompareSession();
|
|
417
|
+
}
|
|
352
418
|
|
|
353
419
|
// Re-seed the session from the committed preset so the sidebar re-highlights it
|
|
354
420
|
// by label. A direct calendar click later clears this, reverting to date matching.
|
|
@@ -500,7 +566,7 @@
|
|
|
500
566
|
// a second click feeling dead — the panel could only be closed by clicking away.
|
|
501
567
|
function togglePicker(): void {
|
|
502
568
|
if (isOpen) {
|
|
503
|
-
|
|
569
|
+
abandonPicker();
|
|
504
570
|
} else {
|
|
505
571
|
openPicker();
|
|
506
572
|
}
|
|
@@ -508,8 +574,7 @@
|
|
|
508
574
|
|
|
509
575
|
function openComparePicker(): void {
|
|
510
576
|
compareFocusReturnEl = compareTriggerRef ? findInteractive(compareTriggerRef) : null;
|
|
511
|
-
|
|
512
|
-
draftCompareEnd = compareEnd ?? null;
|
|
577
|
+
seedCompareSession();
|
|
513
578
|
openCompare = true;
|
|
514
579
|
tick().then(() => focusFirstElement(comparePanelRef));
|
|
515
580
|
}
|
|
@@ -524,19 +589,12 @@
|
|
|
524
589
|
}
|
|
525
590
|
|
|
526
591
|
function handleApplyCompare(): void {
|
|
527
|
-
|
|
528
|
-
compareStart = draftCompareStart;
|
|
529
|
-
compareEnd = draftCompareEnd;
|
|
530
|
-
onapplycompare?.({
|
|
531
|
-
compareStart: draftCompareStart,
|
|
532
|
-
compareEnd: draftCompareEnd,
|
|
533
|
-
presetLabel: selectedPresetLabel
|
|
534
|
-
});
|
|
535
|
-
}
|
|
592
|
+
commitCompare();
|
|
536
593
|
closeComparePicker();
|
|
537
594
|
}
|
|
538
595
|
|
|
539
596
|
function handleCancelCompare(): void {
|
|
597
|
+
discardCompare();
|
|
540
598
|
closeComparePicker();
|
|
541
599
|
}
|
|
542
600
|
|
|
@@ -568,18 +626,8 @@
|
|
|
568
626
|
presetLabel: selectedPresetLabel
|
|
569
627
|
});
|
|
570
628
|
}
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
draftCompareStart !== null &&
|
|
574
|
-
draftCompareEnd !== null
|
|
575
|
-
) {
|
|
576
|
-
compareStart = draftCompareStart;
|
|
577
|
-
compareEnd = draftCompareEnd;
|
|
578
|
-
onapplycompare?.({
|
|
579
|
-
compareStart: draftCompareStart,
|
|
580
|
-
compareEnd: draftCompareEnd,
|
|
581
|
-
presetLabel: selectedPresetLabel
|
|
582
|
-
});
|
|
629
|
+
if (typeof compareCalendar === 'function') {
|
|
630
|
+
commitCompare();
|
|
583
631
|
}
|
|
584
632
|
} else {
|
|
585
633
|
if (draftValue !== null) {
|
|
@@ -601,7 +649,7 @@
|
|
|
601
649
|
|
|
602
650
|
function handleCancel(): void {
|
|
603
651
|
oncancel?.();
|
|
604
|
-
|
|
652
|
+
abandonPicker();
|
|
605
653
|
}
|
|
606
654
|
|
|
607
655
|
function handlePreset(preset: DateRangePreset): void {
|
|
@@ -893,9 +941,10 @@
|
|
|
893
941
|
outside both at once. */
|
|
894
942
|
function handleDismissOutside(): void {
|
|
895
943
|
if (isOpen) {
|
|
896
|
-
|
|
944
|
+
abandonPicker();
|
|
897
945
|
}
|
|
898
946
|
if (openCompare) {
|
|
947
|
+
discardCompare();
|
|
899
948
|
closeComparePicker();
|
|
900
949
|
}
|
|
901
950
|
}
|
|
@@ -1053,7 +1102,7 @@
|
|
|
1053
1102
|
>
|
|
1054
1103
|
{#if typeof compareCalendar === 'function'}
|
|
1055
1104
|
<div class="drp-compare-panel-body">
|
|
1056
|
-
{@render compareCalendar()}
|
|
1105
|
+
{@render compareCalendar(compareDraft)}
|
|
1057
1106
|
</div>
|
|
1058
1107
|
{/if}
|
|
1059
1108
|
<div class="drp-footer">
|
|
@@ -1066,7 +1115,7 @@
|
|
|
1066
1115
|
onclick={handleApplyCompare}
|
|
1067
1116
|
ariaLabel="Apply compare selection"
|
|
1068
1117
|
classes="drp-btn-apply"
|
|
1069
|
-
disabled={
|
|
1118
|
+
disabled={!canApplyCompare}>Apply</Button
|
|
1070
1119
|
>
|
|
1071
1120
|
</div>
|
|
1072
1121
|
</div>
|
|
@@ -1442,7 +1491,7 @@
|
|
|
1442
1491
|
causes a Svelte 5 runtime error. -->
|
|
1443
1492
|
{#if typeof compareCalendar === 'function' && typeof compareTrigger !== 'function'}
|
|
1444
1493
|
<div class="drp-compare-section">
|
|
1445
|
-
{@render compareCalendar()}
|
|
1494
|
+
{@render compareCalendar(compareDraft)}
|
|
1446
1495
|
</div>
|
|
1447
1496
|
{/if}
|
|
1448
1497
|
</div>
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* The compare range a `compareCalendar` snippet edits before Apply commits it. A plain
|
|
4
|
+
* reactive object, so a Calendar can bind to its fields.
|
|
5
|
+
*/
|
|
6
|
+
export type DateRangeCompareDraft = {
|
|
7
|
+
start: Date | null;
|
|
8
|
+
end: Date | null;
|
|
9
|
+
};
|
|
2
10
|
export type DateRangePreset = {
|
|
3
11
|
label: string;
|
|
4
12
|
getValue: () => {
|
|
@@ -102,8 +110,16 @@ export type OptionalDateRangePickerProperties = {
|
|
|
102
110
|
compareStart?: Date | null;
|
|
103
111
|
/** End of compare range (bindable). Used when compareCalendar snippet is provided. */
|
|
104
112
|
compareEnd?: Date | null;
|
|
105
|
-
/**
|
|
106
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Snippet rendered in the compare-calendar slot. Consumer controls all compare UI.
|
|
115
|
+
*
|
|
116
|
+
* It receives the compare draft, a reactive `{ start, end }` that Apply commits and that
|
|
117
|
+
* Cancel, Escape and an outside press discard. Bind a Calendar to it:
|
|
118
|
+
* `bind:rangeStart={draft.start} bind:rangeEnd={draft.end}`. A snippet that takes no
|
|
119
|
+
* argument and edits `compareStart`/`compareEnd` directly still works: Apply keeps what
|
|
120
|
+
* it wrote, and Cancel restores the range the session opened with.
|
|
121
|
+
*/
|
|
122
|
+
compareCalendar?: Snippet<[DateRangeCompareDraft]>;
|
|
107
123
|
/** Which day of the week starts the calendar. 0=Sunday, 1=Monday. */
|
|
108
124
|
weekStartsOn?: 0 | 1;
|
|
109
125
|
/** BCP-47 locale string for date formatting. */
|
|
@@ -134,13 +134,15 @@
|
|
|
134
134
|
}
|
|
135
135
|
</script>
|
|
136
136
|
|
|
137
|
-
<!--
|
|
138
|
-
|
|
137
|
+
<!-- This named content group moves with arrow keys; it can contain independent
|
|
138
|
+
controls, so button semantics would hide their accessibility semantics. -->
|
|
139
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
139
140
|
<div
|
|
140
141
|
class="draggable {classes ?? ''}"
|
|
141
142
|
class:dragging={active !== null}
|
|
142
143
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
143
144
|
tabindex={disabled ? -1 : 0}
|
|
145
|
+
role="group"
|
|
144
146
|
aria-label={dragLabel}
|
|
145
147
|
style:transform={`translate(${x}px, ${y}px)`}
|
|
146
148
|
onpointerdown={startDrag}
|