@juspay/svelte-ui-components 4.54.2 → 4.55.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/dist/ChatComposer/ChatComposer.svelte +269 -12
- package/dist/ChatComposer/properties.d.ts +8 -0
- package/dist/_interaction/focus.d.ts +2 -1
- package/dist/_interaction/focus.js +10 -2
- package/dist-wc/index.js +1628 -1531
- package/package.json +1 -1
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
statusText,
|
|
66
66
|
statusTestId,
|
|
67
67
|
slashCommands = null,
|
|
68
|
+
inlineTriggers = null,
|
|
68
69
|
slashMenuAriaLabel = 'slash commands',
|
|
69
70
|
slashMenuHint = '↑↓ move · tab complete · ↵ select · esc dismiss',
|
|
70
71
|
slashMenuClasses,
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
|
|
97
98
|
let fileInput: HTMLInputElement | null = $state(null);
|
|
98
99
|
let inputElement: HTMLTextAreaElement | null = $state(null);
|
|
100
|
+
let menuRaised = $state(false);
|
|
99
101
|
|
|
100
102
|
export const focus = (options?: FocusOptions): void => {
|
|
101
103
|
inputElement?.focus(options);
|
|
@@ -133,6 +135,10 @@
|
|
|
133
135
|
let slashDismissed = $state(false);
|
|
134
136
|
let slashSelected = $state(0);
|
|
135
137
|
let slashList: HTMLUListElement | null = $state(null);
|
|
138
|
+
let highlightLayer: HTMLDivElement | null = $state(null);
|
|
139
|
+
|
|
140
|
+
// Where the caret sits, so an inline trigger can open the menu on the word it is in.
|
|
141
|
+
let caret = $state(0);
|
|
136
142
|
|
|
137
143
|
// A command opens on the first character of its name: "/model" on "/",
|
|
138
144
|
// "@support" on "@". Letters, digits and whitespace never trigger, so a
|
|
@@ -143,12 +149,31 @@
|
|
|
143
149
|
return /^[^\p{L}\p{N}\s]$/u.test(first) ? first : null;
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
// The word ending at the caret when it starts with an inline trigger: "@ge" in
|
|
153
|
+
// "ask @ge|". Null unless the caret sits at the end of that word.
|
|
154
|
+
const inlineWord = $derived.by((): { start: number; text: string } | null => {
|
|
155
|
+
if (inlineTriggers === null || slashCommands === null) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const text = /(?:^|\s)(\S*)$/.exec(value.slice(0, caret))?.[1] ?? '';
|
|
159
|
+
const trigger = slashTrigger(text);
|
|
160
|
+
if (trigger === null || !inlineTriggers.includes(trigger) || /^\S/.test(value.slice(caret))) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return { start: caret - text.length, text };
|
|
164
|
+
});
|
|
165
|
+
|
|
146
166
|
const slashMatches = $derived.by((): ChatComposerSlashCommand[] => {
|
|
147
|
-
const
|
|
148
|
-
|
|
167
|
+
const typed = inlineWord?.text ?? value;
|
|
168
|
+
const trigger = slashTrigger(typed);
|
|
169
|
+
if (slashCommands === null || slashDismissed || trigger === null || /\s/.test(typed)) {
|
|
149
170
|
return [];
|
|
150
171
|
}
|
|
151
|
-
|
|
172
|
+
// An inline trigger opens only on the word at the caret.
|
|
173
|
+
if (inlineWord === null && inlineTriggers?.includes(trigger)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const query = typed.toLowerCase();
|
|
152
177
|
return slashCommands.filter(
|
|
153
178
|
(command) =>
|
|
154
179
|
slashTrigger(command.name) === trigger &&
|
|
@@ -163,7 +188,8 @@
|
|
|
163
188
|
return null;
|
|
164
189
|
}
|
|
165
190
|
const parsed = /^(\S+)\s(\S*)$/.exec(value);
|
|
166
|
-
|
|
191
|
+
const trigger = parsed === null ? null : slashTrigger(parsed[1] ?? '');
|
|
192
|
+
if (parsed === null || trigger === null || inlineTriggers?.includes(trigger)) {
|
|
167
193
|
return null;
|
|
168
194
|
}
|
|
169
195
|
const name = (parsed[1] ?? '').toLowerCase();
|
|
@@ -229,6 +255,15 @@
|
|
|
229
255
|
if (typeof command !== 'object') {
|
|
230
256
|
return;
|
|
231
257
|
}
|
|
258
|
+
// Inline: replace only the word at the caret and keep the rest of the text.
|
|
259
|
+
const word = inlineWord;
|
|
260
|
+
if (word !== null) {
|
|
261
|
+
const rest = value.slice(caret).replace(/^\s/, '');
|
|
262
|
+
value = `${value.slice(0, word.start)}${command.name} ${rest}`;
|
|
263
|
+
placeCaret(word.start + command.name.length + 1);
|
|
264
|
+
slashSelected = 0;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
232
267
|
// A command that takes arguments gets a trailing space and waits for
|
|
233
268
|
// them; with enumerable choices the menu stays open for stage two.
|
|
234
269
|
value = `${command.name} `;
|
|
@@ -236,6 +271,111 @@
|
|
|
236
271
|
slashSelected = 0;
|
|
237
272
|
}
|
|
238
273
|
|
|
274
|
+
function placeCaret(position: number): void {
|
|
275
|
+
caret = position;
|
|
276
|
+
void tick().then(() => {
|
|
277
|
+
inputElement?.focus();
|
|
278
|
+
inputElement?.setSelectionRange(position, position);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function trackCaret(event: Event & { currentTarget: HTMLTextAreaElement }): void {
|
|
283
|
+
caret = event.currentTarget.selectionStart ?? value.length;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Command names with an inline trigger, drawn in the mention colour wherever they sit.
|
|
287
|
+
const mentionNames = $derived.by((): Set<string> => {
|
|
288
|
+
if (inlineTriggers === null || slashCommands === null) {
|
|
289
|
+
return new Set();
|
|
290
|
+
}
|
|
291
|
+
const triggers = inlineTriggers;
|
|
292
|
+
return new Set(
|
|
293
|
+
slashCommands
|
|
294
|
+
.map((command) => command.name.toLowerCase())
|
|
295
|
+
.filter((name) => {
|
|
296
|
+
const trigger = slashTrigger(name);
|
|
297
|
+
return trigger !== null && triggers.includes(trigger);
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
function syncHighlightScroll(): void {
|
|
303
|
+
if (highlightLayer !== null && inputElement !== null) {
|
|
304
|
+
highlightLayer.scrollTop = inputElement.scrollTop;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Painted from script, not the template: the layer must hold the text exactly as typed,
|
|
309
|
+
// and template whitespace would be collapsed. The trailing space keeps a final newline
|
|
310
|
+
// its own line, as the textarea shows it.
|
|
311
|
+
const paintMentions: Action<HTMLDivElement, { text: string; names: Set<string> }> = (
|
|
312
|
+
node,
|
|
313
|
+
initial
|
|
314
|
+
) => {
|
|
315
|
+
function paint({ text, names }: { text: string; names: Set<string> }): void {
|
|
316
|
+
node.replaceChildren(
|
|
317
|
+
...text.split(/(\s+)/).map((part) => {
|
|
318
|
+
if (!names.has(part.toLowerCase())) {
|
|
319
|
+
return document.createTextNode(part);
|
|
320
|
+
}
|
|
321
|
+
const mention = document.createElement('span');
|
|
322
|
+
mention.className = 'mention';
|
|
323
|
+
mention.textContent = part;
|
|
324
|
+
return mention;
|
|
325
|
+
}),
|
|
326
|
+
document.createTextNode(' ')
|
|
327
|
+
);
|
|
328
|
+
syncHighlightScroll();
|
|
329
|
+
}
|
|
330
|
+
paint(initial);
|
|
331
|
+
return { update: paint };
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// The menu opens in the browser's top layer where it can, so no ancestor's overflow or
|
|
335
|
+
// mask clips it, and is then placed against the composer exactly where it sat before.
|
|
336
|
+
// Without popover support it stays absolutely positioned inside the composer.
|
|
337
|
+
const raiseMenu: Action<HTMLDivElement> = (node) => {
|
|
338
|
+
// The menu is the composer's own child, so its parent is the box it sits above.
|
|
339
|
+
const anchor = node.parentElement;
|
|
340
|
+
if (anchor === null || typeof node.showPopover !== 'function') {
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
node.setAttribute('popover', 'manual');
|
|
344
|
+
try {
|
|
345
|
+
node.showPopover();
|
|
346
|
+
} catch {
|
|
347
|
+
node.removeAttribute('popover');
|
|
348
|
+
return {};
|
|
349
|
+
}
|
|
350
|
+
menuRaised = true;
|
|
351
|
+
function place(): void {
|
|
352
|
+
if (anchor === null) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const rect = anchor.getBoundingClientRect();
|
|
356
|
+
const outset =
|
|
357
|
+
parseFloat(
|
|
358
|
+
getComputedStyle(anchor).getPropertyValue('--chat-composer-slash-menu-outset')
|
|
359
|
+
) || 0;
|
|
360
|
+
node.style.left = `${rect.left + anchor.clientLeft - outset}px`;
|
|
361
|
+
node.style.width = `${anchor.clientWidth + outset * 2}px`;
|
|
362
|
+
node.style.bottom = `${document.documentElement.clientHeight - (rect.top + anchor.clientTop) + outset}px`;
|
|
363
|
+
}
|
|
364
|
+
place();
|
|
365
|
+
const resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(place) : null;
|
|
366
|
+
resizeObserver?.observe(anchor);
|
|
367
|
+
window.addEventListener('resize', place);
|
|
368
|
+
window.addEventListener('scroll', place, true);
|
|
369
|
+
return {
|
|
370
|
+
destroy() {
|
|
371
|
+
resizeObserver?.disconnect();
|
|
372
|
+
window.removeEventListener('resize', place);
|
|
373
|
+
window.removeEventListener('scroll', place, true);
|
|
374
|
+
menuRaised = false;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
|
|
239
379
|
// Keeps the highlighted row in view while the list scrolls.
|
|
240
380
|
function revealSlash(): void {
|
|
241
381
|
void tick().then(() => {
|
|
@@ -265,8 +405,8 @@
|
|
|
265
405
|
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
266
406
|
event.preventDefault();
|
|
267
407
|
// Enter on a complete match submits it; otherwise it completes and
|
|
268
|
-
// the next Enter submits.
|
|
269
|
-
if (value.trim() === slashCompletion(slashActive)) {
|
|
408
|
+
// the next Enter submits. An inline word always completes.
|
|
409
|
+
if (inlineWord === null && value.trim() === slashCompletion(slashActive)) {
|
|
270
410
|
submit();
|
|
271
411
|
} else {
|
|
272
412
|
pickSlash(slashActive);
|
|
@@ -342,6 +482,7 @@
|
|
|
342
482
|
// A fresh edit un-dismisses the slash menu and resets its highlight.
|
|
343
483
|
slashDismissed = false;
|
|
344
484
|
slashSelected = 0;
|
|
485
|
+
trackCaret(event);
|
|
345
486
|
oninput?.(event.currentTarget.value, event);
|
|
346
487
|
}
|
|
347
488
|
|
|
@@ -409,8 +550,25 @@
|
|
|
409
550
|
{/snippet}
|
|
410
551
|
|
|
411
552
|
{#snippet textInput()}
|
|
553
|
+
{#if inlineTriggers !== null}
|
|
554
|
+
<div class="input-field">
|
|
555
|
+
<div
|
|
556
|
+
class="input-highlights"
|
|
557
|
+
aria-hidden="true"
|
|
558
|
+
bind:this={highlightLayer}
|
|
559
|
+
use:paintMentions={{ text: value, names: mentionNames }}
|
|
560
|
+
></div>
|
|
561
|
+
{@render textArea()}
|
|
562
|
+
</div>
|
|
563
|
+
{:else}
|
|
564
|
+
{@render textArea()}
|
|
565
|
+
{/if}
|
|
566
|
+
{/snippet}
|
|
567
|
+
|
|
568
|
+
{#snippet textArea()}
|
|
412
569
|
<textarea
|
|
413
570
|
class="input"
|
|
571
|
+
class:inline-mentions={inlineTriggers !== null}
|
|
414
572
|
bind:this={inputElement}
|
|
415
573
|
data-pw={inputTestId ?? null}
|
|
416
574
|
bind:value
|
|
@@ -427,6 +585,9 @@
|
|
|
427
585
|
oninput={handleInput}
|
|
428
586
|
onfocus={(event) => oninputfocus?.(event)}
|
|
429
587
|
onkeydown={handleKeydown}
|
|
588
|
+
onkeyup={trackCaret}
|
|
589
|
+
onclick={trackCaret}
|
|
590
|
+
onscroll={syncHighlightScroll}
|
|
430
591
|
onpaste={(event) => onpaste?.(event)}
|
|
431
592
|
use:autoGrow={value}
|
|
432
593
|
></textarea>
|
|
@@ -518,10 +679,12 @@
|
|
|
518
679
|
{#if slashOpen}
|
|
519
680
|
<div
|
|
520
681
|
class="slash-menu {slashMenuClasses ?? ''}"
|
|
682
|
+
class:top-layer={menuRaised}
|
|
521
683
|
role="listbox"
|
|
522
684
|
id={slashListboxId}
|
|
523
685
|
aria-label={slashMenuAriaLabel}
|
|
524
686
|
use:slashDismissal
|
|
687
|
+
use:raiseMenu
|
|
525
688
|
>
|
|
526
689
|
<!-- role="presentation": a listbox's accessible children must be its own role=option
|
|
527
690
|
rows. Left as a plain <ul>, this list's implicit role=list would sit between the
|
|
@@ -541,6 +704,7 @@
|
|
|
541
704
|
>
|
|
542
705
|
<Button
|
|
543
706
|
type="button"
|
|
707
|
+
variant="ghost"
|
|
544
708
|
classes={[slashItemClasses, index === slashActive ? slashSelectedItemClasses : '']
|
|
545
709
|
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
546
710
|
.join(' ')}
|
|
@@ -716,6 +880,58 @@
|
|
|
716
880
|
overflow-y: auto;
|
|
717
881
|
}
|
|
718
882
|
|
|
883
|
+
/* Inline mentions: an identical text layer sits behind a transparent textarea and
|
|
884
|
+
colours the mentions, so the caret, selection and editing stay the textarea's own. */
|
|
885
|
+
.input-field {
|
|
886
|
+
position: relative;
|
|
887
|
+
flex: 1;
|
|
888
|
+
min-width: 0;
|
|
889
|
+
display: flex;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.input-field .input {
|
|
893
|
+
position: relative;
|
|
894
|
+
width: 100%;
|
|
895
|
+
scrollbar-gutter: stable;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.input.inline-mentions {
|
|
899
|
+
color: transparent;
|
|
900
|
+
caret-color: var(--chat-composer-color, #18181b);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.input.inline-mentions::selection {
|
|
904
|
+
color: var(--chat-composer-color, #18181b);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.input-highlights {
|
|
908
|
+
position: absolute;
|
|
909
|
+
inset: 0;
|
|
910
|
+
box-sizing: border-box;
|
|
911
|
+
overflow: hidden;
|
|
912
|
+
scrollbar-gutter: stable;
|
|
913
|
+
pointer-events: none;
|
|
914
|
+
white-space: pre-wrap;
|
|
915
|
+
overflow-wrap: break-word;
|
|
916
|
+
letter-spacing: normal;
|
|
917
|
+
word-spacing: normal;
|
|
918
|
+
font-family: var(--chat-composer-font-family, inherit);
|
|
919
|
+
font-size: var(--chat-composer-font-size, 0.9375rem);
|
|
920
|
+
font-weight: var(--chat-composer-font-weight, 400);
|
|
921
|
+
line-height: var(--chat-composer-line-height, 1.5);
|
|
922
|
+
color: var(--chat-composer-color, #18181b);
|
|
923
|
+
padding: var(--chat-composer-input-padding, 6px 4px);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/* The mention must lay out exactly like the textarea's text, so it inherits every font
|
|
927
|
+
property: a consumer's global span styles would otherwise shift it off the caret. */
|
|
928
|
+
.input-highlights :global(.mention) {
|
|
929
|
+
font: inherit;
|
|
930
|
+
letter-spacing: inherit;
|
|
931
|
+
word-spacing: inherit;
|
|
932
|
+
color: var(--chat-composer-mention-color, var(--chat-composer-slash-name-color, #2563eb));
|
|
933
|
+
}
|
|
934
|
+
|
|
719
935
|
.input::placeholder {
|
|
720
936
|
color: var(--chat-composer-placeholder-color, #52525b);
|
|
721
937
|
}
|
|
@@ -792,10 +1008,24 @@
|
|
|
792
1008
|
margin-bottom: var(--chat-composer-slash-menu-gap, 8px);
|
|
793
1009
|
overflow: hidden;
|
|
794
1010
|
z-index: var(--chat-composer-slash-menu-z-index, 30);
|
|
795
|
-
background: var(--chat-composer-slash-menu-background, #ffffff);
|
|
796
|
-
border: var(--chat-composer-slash-menu-border, 1px solid #
|
|
1011
|
+
background: var(--chat-composer-slash-menu-background, var(--menu-background-color, #ffffff));
|
|
1012
|
+
border: var(--chat-composer-slash-menu-border, var(--menu-border, 1px solid #e0e0e0));
|
|
797
1013
|
border-radius: var(--chat-composer-slash-menu-border-radius, 12px);
|
|
798
|
-
box-shadow: var(
|
|
1014
|
+
box-shadow: var(
|
|
1015
|
+
--chat-composer-slash-menu-box-shadow,
|
|
1016
|
+
var(--menu-box-shadow, 0px 4px 16px rgba(0, 0, 0, 0.12))
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/* In the top layer the menu is placed by script against the composer, so the popover's
|
|
1021
|
+
own defaults (inset, auto margin, padding, overflow) are cleared. */
|
|
1022
|
+
.chat-composer > .slash-menu.top-layer {
|
|
1023
|
+
position: fixed;
|
|
1024
|
+
inset: auto;
|
|
1025
|
+
box-sizing: border-box;
|
|
1026
|
+
margin: 0 0 var(--chat-composer-slash-menu-gap, 8px);
|
|
1027
|
+
padding: 0;
|
|
1028
|
+
color: inherit;
|
|
799
1029
|
}
|
|
800
1030
|
|
|
801
1031
|
.slash-menu > .slash-list {
|
|
@@ -808,17 +1038,40 @@
|
|
|
808
1038
|
gap: var(--chat-composer-slash-list-gap, 2px);
|
|
809
1039
|
}
|
|
810
1040
|
|
|
1041
|
+
/* Rows are menu items, not buttons: the row sets its Button's tokens itself so a
|
|
1042
|
+
consumer's app-wide button fill never reaches it, and the highlighted row is
|
|
1043
|
+
tinted by the <li> beneath a transparent Button. */
|
|
811
1044
|
.slash-menu .slash-list li {
|
|
812
1045
|
min-width: 0;
|
|
1046
|
+
border-radius: var(--chat-composer-slash-item-border-radius, 8px);
|
|
1047
|
+
--button-color: var(--chat-composer-slash-item-background, transparent);
|
|
1048
|
+
--button-background: none;
|
|
1049
|
+
--button-hover-color: var(--chat-composer-slash-item-hover-background, transparent);
|
|
1050
|
+
--button-text-color: var(--chat-composer-slash-item-color, var(--menu-item-color, #333333));
|
|
1051
|
+
--button-hover-text-color: var(
|
|
1052
|
+
--chat-composer-slash-item-color,
|
|
1053
|
+
var(--menu-item-color, #333333)
|
|
1054
|
+
);
|
|
1055
|
+
--button-border: none;
|
|
1056
|
+
--button-box-shadow: none;
|
|
1057
|
+
--button-border-radius: var(--chat-composer-slash-item-border-radius, 8px);
|
|
1058
|
+
--button-width: 100%;
|
|
1059
|
+
--button-height: auto;
|
|
1060
|
+
--button-justify-content: flex-start;
|
|
1061
|
+
--button-padding: var(--chat-composer-slash-item-padding, 8px 12px);
|
|
1062
|
+
--button-content-gap: var(--chat-composer-slash-item-gap, 8px);
|
|
813
1063
|
}
|
|
814
1064
|
|
|
815
1065
|
.slash-menu > .slash-foot {
|
|
816
1066
|
padding: var(--chat-composer-slash-foot-padding, 4px 12px);
|
|
817
|
-
border-top: var(
|
|
1067
|
+
border-top: var(
|
|
1068
|
+
--chat-composer-slash-foot-border,
|
|
1069
|
+
1px solid var(--menu-separator-color, #e0e0e0)
|
|
1070
|
+
);
|
|
818
1071
|
font-family: var(--chat-composer-slash-foot-font-family, inherit);
|
|
819
1072
|
font-variant-ligatures: var(--chat-composer-slash-foot-font-variant-ligatures, inherit);
|
|
820
1073
|
font-size: var(--chat-composer-slash-foot-font-size, 12px);
|
|
821
|
-
color: var(--chat-composer-slash-foot-color, #
|
|
1074
|
+
color: var(--chat-composer-slash-foot-color, var(--menu-item-color, #333333));
|
|
822
1075
|
}
|
|
823
1076
|
|
|
824
1077
|
/* The default row content (a consumer's slashItem snippet replaces it). */
|
|
@@ -837,6 +1090,7 @@
|
|
|
837
1090
|
.slash-menu .slash-desc {
|
|
838
1091
|
flex: 1;
|
|
839
1092
|
min-width: 0;
|
|
1093
|
+
text-align: start;
|
|
840
1094
|
overflow: hidden;
|
|
841
1095
|
text-overflow: ellipsis;
|
|
842
1096
|
white-space: nowrap;
|
|
@@ -849,7 +1103,10 @@
|
|
|
849
1103
|
Scoped so a consumer's own class stays authoritative the moment it's
|
|
850
1104
|
supplied: this rule simply never applies then. */
|
|
851
1105
|
.slash-menu .slash-item-selected-default {
|
|
852
|
-
background: var(
|
|
1106
|
+
background: var(
|
|
1107
|
+
--chat-composer-slash-item-selected-background,
|
|
1108
|
+
var(--menu-item-hover-background-color, #f5f5f5)
|
|
1109
|
+
);
|
|
853
1110
|
}
|
|
854
1111
|
|
|
855
1112
|
.slash-menu .slash-badge {
|
|
@@ -111,6 +111,14 @@ export type OptionalChatComposerProperties = {
|
|
|
111
111
|
* stage listing them. `null` (the default) turns the whole feature off.
|
|
112
112
|
*/
|
|
113
113
|
slashCommands?: ChatComposerSlashCommand[] | null;
|
|
114
|
+
/**
|
|
115
|
+
* Triggers that open the command menu on the word at the caret, anywhere in the text,
|
|
116
|
+
* instead of only when the whole input is one command: `['@']` makes "@geo" mid-sentence
|
|
117
|
+
* list the `@` commands, and picking one replaces just that word. Each command name with
|
|
118
|
+
* one of these triggers is drawn in `--chat-composer-mention-color` wherever it appears in
|
|
119
|
+
* the input. `null` (the default) keeps every trigger start-of-input only.
|
|
120
|
+
*/
|
|
121
|
+
inlineTriggers?: string[] | null;
|
|
114
122
|
/** Accessible name of the slash-command listbox. Defaults to 'slash commands'. */
|
|
115
123
|
slashMenuAriaLabel?: string;
|
|
116
124
|
/** Key-hint line under the slash-command list. */
|
|
@@ -43,7 +43,8 @@ export declare function getFocusTrapBoundary(container: ParentNode | null): Focu
|
|
|
43
43
|
* element focus should move to, or null when the default browser behavior
|
|
44
44
|
* (move to the next/previous focusable element) should be left alone.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
46
|
+
* An empty panel keeps focus on itself when it is already the active element.
|
|
47
|
+
* Otherwise wrapping only happens exactly at an edge: focus sits on the last focusable
|
|
47
48
|
* element and Tab (not Shift+Tab) was pressed, or focus sits on the first and
|
|
48
49
|
* Shift+Tab was pressed. Anywhere else in the middle of the tab order, the
|
|
49
50
|
* browser's own handling is correct and this returns null.
|
|
@@ -95,7 +95,8 @@ export function getFocusTrapBoundary(container) {
|
|
|
95
95
|
* element focus should move to, or null when the default browser behavior
|
|
96
96
|
* (move to the next/previous focusable element) should be left alone.
|
|
97
97
|
*
|
|
98
|
-
*
|
|
98
|
+
* An empty panel keeps focus on itself when it is already the active element.
|
|
99
|
+
* Otherwise wrapping only happens exactly at an edge: focus sits on the last focusable
|
|
99
100
|
* element and Tab (not Shift+Tab) was pressed, or focus sits on the first and
|
|
100
101
|
* Shift+Tab was pressed. Anywhere else in the middle of the tab order, the
|
|
101
102
|
* browser's own handling is correct and this returns null.
|
|
@@ -103,7 +104,14 @@ export function getFocusTrapBoundary(container) {
|
|
|
103
104
|
export function focusTrapTabTarget(params) {
|
|
104
105
|
const { first, last } = getFocusTrapBoundary(params.container);
|
|
105
106
|
if (first === null || last === null) {
|
|
106
|
-
|
|
107
|
+
// focusEntryPoint focuses the panel itself when it has no controls. Keep
|
|
108
|
+
// that panel, but never take focus from another (possibly nested) layer.
|
|
109
|
+
const active = params.activeElement;
|
|
110
|
+
return active === params.container &&
|
|
111
|
+
typeof HTMLElement === 'function' &&
|
|
112
|
+
active instanceof HTMLElement
|
|
113
|
+
? active
|
|
114
|
+
: null;
|
|
107
115
|
}
|
|
108
116
|
const edge = params.shiftKey ? first : last;
|
|
109
117
|
if (params.activeElement !== edge) {
|