@juspay/svelte-ui-components 3.4.1 → 3.5.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.
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
testId,
|
|
23
23
|
ariaLabel,
|
|
24
24
|
ariaExpanded,
|
|
25
|
+
ariaControls,
|
|
25
26
|
ariaHaspopup,
|
|
26
27
|
ariaSelected,
|
|
27
28
|
ariaBusy,
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
role={role ?? null}
|
|
92
93
|
aria-label={ariaLabel ?? null}
|
|
93
94
|
aria-expanded={ariaExpanded ?? null}
|
|
95
|
+
aria-controls={ariaControls ?? null}
|
|
94
96
|
aria-haspopup={ariaHaspopup ?? null}
|
|
95
97
|
aria-selected={ariaSelected ?? null}
|
|
96
98
|
aria-busy={isBusy || ariaBusy || null}
|
|
@@ -53,6 +53,11 @@ export type OptionalButtonProperties = {
|
|
|
53
53
|
children?: Snippet;
|
|
54
54
|
ariaLabel?: string;
|
|
55
55
|
ariaExpanded?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* `aria-controls`: the id of the region this button governs. Pair with
|
|
58
|
+
* `ariaExpanded` on a disclosure button so assistive tech can reach what it opens.
|
|
59
|
+
*/
|
|
60
|
+
ariaControls?: string;
|
|
56
61
|
/**
|
|
57
62
|
* Native `aria-haspopup`. Needed when the button is the trigger for a menu, listbox or
|
|
58
63
|
* dialog — Menu hands exactly this to its `trigger` snippet.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
markdown,
|
|
18
18
|
body,
|
|
19
19
|
streaming = false,
|
|
20
|
+
clampLines = 0,
|
|
20
21
|
status,
|
|
21
22
|
avatar,
|
|
22
23
|
header,
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
actions,
|
|
26
27
|
copyLabel = 'Copy',
|
|
27
28
|
retryLabel = 'Retry',
|
|
29
|
+
expandLabel = 'Expand message',
|
|
30
|
+
collapseLabel = 'Collapse message',
|
|
28
31
|
feedbackUpLabel = 'Good response',
|
|
29
32
|
feedbackDownLabel = 'Bad response',
|
|
30
33
|
onretry: onretryLegacy,
|
|
@@ -37,6 +40,37 @@
|
|
|
37
40
|
classes
|
|
38
41
|
}: ChatMessageProperties = $props();
|
|
39
42
|
|
|
43
|
+
// A long message can be collapsed to a few lines and opened by click or keyboard.
|
|
44
|
+
// Consumers had been rebuilding this around the `body` snippet; it belongs here so
|
|
45
|
+
// the chrome, the clamp and the expanded state stay one component's concern.
|
|
46
|
+
let expanded = $state(false);
|
|
47
|
+
let clampable = $derived(typeof clampLines === 'number' && clampLines > 0);
|
|
48
|
+
let clampActive = $derived(clampable && !expanded);
|
|
49
|
+
// Turning clamping off must not leave the message stuck open: without this, a parent
|
|
50
|
+
// that sets clampLines to 0 and later restores it would get an already-expanded bubble
|
|
51
|
+
// instead of the clamped default. A prop going away cannot be observed from a $derived
|
|
52
|
+
// without writing to state, so this is the sanctioned escape hatch rather than a
|
|
53
|
+
// reactive convenience.
|
|
54
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
55
|
+
$effect(() => {
|
|
56
|
+
if (!clampable) {
|
|
57
|
+
expanded = false;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Ties the toggle to the region it expands. Per-instance, so two clamped messages
|
|
61
|
+
// on one page never collide.
|
|
62
|
+
const uid = $props.id();
|
|
63
|
+
const bodyId = `chat-message-body-${uid}`;
|
|
64
|
+
// Spread rather than a ternary: Button's testId is `string | undefined` and the
|
|
65
|
+
// repo forbids the `undefined` literal, so the prop is omitted instead of blanked.
|
|
66
|
+
let clampToggleTestId = $derived(
|
|
67
|
+
typeof testId === 'string' ? { testId: `${testId}-clamp-toggle` } : {}
|
|
68
|
+
);
|
|
69
|
+
const toggleExpanded = () => {
|
|
70
|
+
if (clampable) {
|
|
71
|
+
expanded = !expanded;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
40
74
|
// Event-casing phase 1: both spellings accepted, the correct one wins.
|
|
41
75
|
const oncopy = $derived(onCopy ?? oncopyLegacy);
|
|
42
76
|
const onfeedback = $derived(onFeedback ?? onfeedbackLegacy);
|
|
@@ -136,14 +170,38 @@
|
|
|
136
170
|
<div class="header">{@render header()}</div>
|
|
137
171
|
{/if}
|
|
138
172
|
|
|
139
|
-
|
|
173
|
+
<!-- The bubble deliberately carries no role and no handler of its own. A button has
|
|
174
|
+
presentational children, so making the bubble one would flatten the paragraphs and
|
|
175
|
+
lists a message body renders; and a click target that is not focusable is a control
|
|
176
|
+
keyboard users cannot reach. The Button below is the whole control. -->
|
|
177
|
+
<div
|
|
178
|
+
class="bubble"
|
|
179
|
+
class:clampable
|
|
180
|
+
style={clampable ? `--chat-message-clamp-lines-prop: ${clampLines};` : null}
|
|
181
|
+
data-clamped={clampActive ? 'true' : null}
|
|
182
|
+
data-expanded={clampable ? String(expanded) : null}
|
|
183
|
+
>
|
|
140
184
|
{#if hasBody}
|
|
141
|
-
<div class="body">{@render body?.()}</div>
|
|
185
|
+
<div class="body" id={bodyId}>{@render body?.()}</div>
|
|
142
186
|
{:else if hasHtml}
|
|
143
187
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
144
|
-
<div class="body">{@html effectiveHtml}</div>
|
|
188
|
+
<div class="body" id={bodyId}>{@html effectiveHtml}</div>
|
|
145
189
|
{:else if content.length > 0}
|
|
146
|
-
<div class="body text">{content}</div>
|
|
190
|
+
<div class="body text" id={bodyId}>{content}</div>
|
|
191
|
+
{/if}
|
|
192
|
+
|
|
193
|
+
{#if clampable}
|
|
194
|
+
<div class="clamp-toggle">
|
|
195
|
+
<Button
|
|
196
|
+
variant="ghost"
|
|
197
|
+
size="sm"
|
|
198
|
+
onclick={toggleExpanded}
|
|
199
|
+
ariaExpanded={expanded}
|
|
200
|
+
ariaControls={bodyId}
|
|
201
|
+
text={expanded ? collapseLabel : expandLabel}
|
|
202
|
+
{...clampToggleTestId}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
147
205
|
{/if}
|
|
148
206
|
|
|
149
207
|
{#if showTyping}
|
|
@@ -341,6 +399,21 @@
|
|
|
341
399
|
}
|
|
342
400
|
}
|
|
343
401
|
|
|
402
|
+
.clamp-toggle {
|
|
403
|
+
display: flex;
|
|
404
|
+
margin-top: var(--chat-message-clamp-toggle-margin-top, 4px);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Clamp the rendered body, not the source: the message keeps its markup and its
|
|
408
|
+
copy text, and expanding is a state change rather than a re-render. */
|
|
409
|
+
.bubble[data-clamped='true'] .body {
|
|
410
|
+
display: -webkit-box;
|
|
411
|
+
line-clamp: var(--chat-message-clamp-lines, var(--chat-message-clamp-lines-prop, 2));
|
|
412
|
+
-webkit-line-clamp: var(--chat-message-clamp-lines, var(--chat-message-clamp-lines-prop, 2));
|
|
413
|
+
-webkit-box-orient: vertical;
|
|
414
|
+
overflow: hidden;
|
|
415
|
+
}
|
|
416
|
+
|
|
344
417
|
.body :global(p) {
|
|
345
418
|
margin: var(--chat-message-paragraph-margin, 0 0 0.5em 0);
|
|
346
419
|
}
|
|
@@ -27,6 +27,16 @@ export type OptionalChatMessageProperties = {
|
|
|
27
27
|
* action still has something to copy.
|
|
28
28
|
*/
|
|
29
29
|
body?: Snippet | null;
|
|
30
|
+
/**
|
|
31
|
+
* Collapse the rendered body to this many lines and render a `Button` below it
|
|
32
|
+
* that expands and re-collapses the message; that button is the only control.
|
|
33
|
+
* `0` or omitted leaves the message uncollapsed and renders no control.
|
|
34
|
+
* The clamp applies to the rendered body, so `content` — and therefore the copy
|
|
35
|
+
* action — always carries the whole message. A consumer stylesheet setting
|
|
36
|
+
* `--chat-message-clamp-lines` takes priority over this value, so a theme can set
|
|
37
|
+
* the line count for every message without touching each call site.
|
|
38
|
+
*/
|
|
39
|
+
clampLines?: number;
|
|
30
40
|
streaming?: boolean;
|
|
31
41
|
status?: ChatMessageStatus;
|
|
32
42
|
avatar?: Snippet;
|
|
@@ -36,6 +46,12 @@ export type OptionalChatMessageProperties = {
|
|
|
36
46
|
actions?: Snippet;
|
|
37
47
|
copyLabel?: string;
|
|
38
48
|
retryLabel?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Label on the expand/collapse button rendered when `clampLines` is set, swapped
|
|
51
|
+
* on the expanded state.
|
|
52
|
+
*/
|
|
53
|
+
expandLabel?: string;
|
|
54
|
+
collapseLabel?: string;
|
|
39
55
|
feedbackUpLabel?: string;
|
|
40
56
|
feedbackDownLabel?: string;
|
|
41
57
|
testId?: string;
|