@juspay/svelte-ui-components 4.22.0 → 4.24.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/dist/Checkbox/Checkbox.svelte +44 -6
- package/dist/Checkbox/properties.d.ts +18 -2
- package/dist/MarkdownText/MarkdownText.svelte +3 -2
- package/dist/MarkdownText/markdown.d.ts +7 -2
- package/dist/MarkdownText/markdown.js +61 -1
- package/dist/MarkdownText/properties.d.ts +17 -0
- package/dist/Select/Select.svelte +12 -1
- package/dist/Select/properties.d.ts +11 -0
- package/dist/Table/Table.svelte +8 -1
- package/dist/Toggle/Toggle.svelte +6 -2
- package/dist/Toggle/properties.d.ts +9 -0
- package/dist-wc/index.js +316 -220
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { flushSync } from 'svelte';
|
|
2
3
|
import type { CheckboxProperties } from './properties';
|
|
3
4
|
import checkmarkSvg from '../assets/checkmark.svg?raw';
|
|
4
5
|
import minusSvg from '../assets/minus.svg?raw';
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
|
-
text,
|
|
8
|
+
text = '',
|
|
8
9
|
checked = $bindable(false),
|
|
9
10
|
disabled = false,
|
|
10
11
|
indeterminate = $bindable(false),
|
|
@@ -16,9 +17,13 @@
|
|
|
16
17
|
ariaControls,
|
|
17
18
|
ariaLabel,
|
|
18
19
|
controlled = false,
|
|
19
|
-
attributes
|
|
20
|
+
attributes,
|
|
21
|
+
name,
|
|
22
|
+
value
|
|
20
23
|
}: CheckboxProperties = $props();
|
|
21
24
|
|
|
25
|
+
let nativeInputEl: HTMLInputElement | null = $state(null);
|
|
26
|
+
|
|
22
27
|
// Visible text wins over `ariaLabel`, which is what `properties.ts` has always
|
|
23
28
|
// promised and what WCAG 2.5.3 requires: a control showing "Accept terms" must
|
|
24
29
|
// carry that text in its accessible name, so letting an explicit label replace
|
|
@@ -44,15 +49,45 @@
|
|
|
44
49
|
// for. Nothing is flipped locally, so a parent that declines the change
|
|
45
50
|
// (a controlled table selection that rejects a row) keeps the box, the
|
|
46
51
|
// native input and aria-checked all agreeing with the value it passed.
|
|
52
|
+
// No `input`/`change` dispatch here either: the input's checked state
|
|
53
|
+
// genuinely has not changed, so firing one would tell listeners (a
|
|
54
|
+
// parent `<form>`, a consumer's own `onchange`) about a flip that did
|
|
55
|
+
// not happen.
|
|
47
56
|
if (controlled) {
|
|
48
57
|
onclick?.(!checked);
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
// `flushSync` (rather than `await tick()`) applies the reactive
|
|
61
|
+
// `input.checked` write before the dispatch below runs, without handing
|
|
62
|
+
// control back to the event loop first. A keyboard repeat or a rapid
|
|
63
|
+
// double click stays a single synchronous call stack from gesture to
|
|
64
|
+
// dispatch: nothing can interleave a second `handleClick` in the middle
|
|
65
|
+
// (the race an `await` gap would allow), a screen reader sees the
|
|
66
|
+
// `input`/`change` pair land in the same tick as the key press it
|
|
67
|
+
// expects them tied to, and there is no unmount gap for `nativeInputEl`
|
|
68
|
+
// to go stale in.
|
|
69
|
+
flushSync(() => {
|
|
70
|
+
checked = !checked;
|
|
71
|
+
if (indeterminate) {
|
|
72
|
+
indeterminate = false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
55
75
|
onclick?.(checked);
|
|
76
|
+
// The box (role="checkbox") is the only element the user can actually
|
|
77
|
+
// reach — the native input is aria-hidden, tabindex="-1" and
|
|
78
|
+
// pointer-events: none, purely a form-submission mirror. Because it
|
|
79
|
+
// never receives a real user click, the browser never fires a native
|
|
80
|
+
// `change` on it either, so a parent `<form>` listener (or a consumer's
|
|
81
|
+
// own `input.addEventListener('change', ...)`) never learns the value
|
|
82
|
+
// moved. `input`/`change` are dispatched manually to restore that signal.
|
|
83
|
+
// `composed: true` matters as much as `bubbles: true` here: native `input`/
|
|
84
|
+
// `change` are composed, and this component ships inside a shadow root
|
|
85
|
+
// (Checkbox.wc.svelte's `shadow: 'open'`) for every <sui-checkbox> consumer.
|
|
86
|
+
// `bubbles` alone stops at the shadow boundary; without `composed` the event
|
|
87
|
+
// never reaches a light-DOM `<form>`, silently defeating the whole point of
|
|
88
|
+
// wiring `name`/`value` onto this input for every web-component consumer.
|
|
89
|
+
nativeInputEl?.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
90
|
+
nativeInputEl?.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
56
91
|
}
|
|
57
92
|
|
|
58
93
|
function handleKeyDown(e: KeyboardEvent): void {
|
|
@@ -84,9 +119,12 @@
|
|
|
84
119
|
class="native-checkbox"
|
|
85
120
|
{checked}
|
|
86
121
|
{disabled}
|
|
122
|
+
name={name ?? null}
|
|
123
|
+
value={value ?? null}
|
|
87
124
|
tabindex={-1}
|
|
88
125
|
aria-hidden="true"
|
|
89
126
|
onclick={(e: MouseEvent) => e.stopPropagation()}
|
|
127
|
+
bind:this={nativeInputEl}
|
|
90
128
|
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
91
129
|
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
92
130
|
/>
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
export type CheckboxProperties =
|
|
2
|
+
export type CheckboxProperties = OptionalCheckboxProperties & CheckboxEventProperties;
|
|
3
3
|
export type MandatoryCheckboxProperties = {
|
|
4
|
-
text
|
|
4
|
+
text?: string;
|
|
5
5
|
};
|
|
6
6
|
export type OptionalCheckboxProperties = {
|
|
7
|
+
/** Visible label. Optional, defaulting to `''` (unlabelled) -- matching Toggle's
|
|
8
|
+
* own `text` contract (Toggle/properties.ts) -- so a form-only checkbox
|
|
9
|
+
* (`<Checkbox name="agree" />`) does not need an empty string just to compile.
|
|
10
|
+
* A checkbox with no visible text needs `ariaLabel` for an accessible name;
|
|
11
|
+
* see `ariaLabel` below. */
|
|
12
|
+
text?: string;
|
|
7
13
|
checked?: boolean;
|
|
8
14
|
disabled?: boolean;
|
|
9
15
|
indeterminate?: boolean;
|
|
@@ -27,6 +33,16 @@ export type OptionalCheckboxProperties = {
|
|
|
27
33
|
* `aria-controls` to point at, or a consumer's own test attribute. Spread
|
|
28
34
|
* last, so a value here wins over the component's own `data-pw`. */
|
|
29
35
|
attributes?: Record<string, string>;
|
|
36
|
+
/** Native `name` on the underlying `<input type="checkbox">`, so the control
|
|
37
|
+
* participates in a surrounding `<form>`'s submission. Omitted by default:
|
|
38
|
+
* the input stays nameless and outside `FormData`, exactly as before this
|
|
39
|
+
* prop existed. */
|
|
40
|
+
name?: string;
|
|
41
|
+
/** Native `value` submitted when checked. Defaults to `"on"` — the native
|
|
42
|
+
* checkbox default — when omitted, so setting `name` alone is enough to
|
|
43
|
+
* form-associate a plain "checked or not" checkbox. An unchecked checkbox
|
|
44
|
+
* never appears in `FormData`, matching native behaviour. */
|
|
45
|
+
value?: string;
|
|
30
46
|
};
|
|
31
47
|
export type CheckboxEventProperties = {
|
|
32
48
|
onclick?: (checked: boolean) => void;
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
classes,
|
|
10
10
|
tableLabel,
|
|
11
11
|
tableWrapperClass,
|
|
12
|
-
sanitize
|
|
12
|
+
sanitize,
|
|
13
|
+
unwrapFence = false
|
|
13
14
|
}: MarkdownTextProperties = $props();
|
|
14
15
|
|
|
15
16
|
let html = $derived(
|
|
16
|
-
renderMarkdown(markdown, { breaks, tableLabel, tableWrapperClass, sanitize })
|
|
17
|
+
renderMarkdown(markdown, { breaks, tableLabel, tableWrapperClass, sanitize, unwrapFence })
|
|
17
18
|
);
|
|
18
19
|
</script>
|
|
19
20
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { RenderMarkdownOptions } from './properties';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Removes a fence that wraps the entire body and is labelled `markdown` or
|
|
4
|
+
* `md`, which is how models overwhelmingly emit a markdown answer. Returns the
|
|
5
|
+
* source untouched in every other case.
|
|
6
|
+
*
|
|
7
|
+
* An unlabelled fence is deliberately not unwrapped: it may be code the author
|
|
8
|
+
* meant to show, and there is nothing in the source to tell the two apart.
|
|
5
9
|
*/
|
|
10
|
+
export declare function unwrapMarkdownFence(source: string): string;
|
|
6
11
|
export declare function renderMarkdown(markdown: string, options?: RenderMarkdownOptions): string;
|
|
@@ -302,9 +302,69 @@ function instanceFor(options) {
|
|
|
302
302
|
* Markdown → HTML with the sanitizing pipeline above. Pure string transform —
|
|
303
303
|
* no DOM involved — so it renders identically on server and client.
|
|
304
304
|
*/
|
|
305
|
+
/**
|
|
306
|
+
* Matches a body that is ENTIRELY one fence labelled as markdown.
|
|
307
|
+
*
|
|
308
|
+
* Anchored at both ends on purpose. A peer's live implementation used an
|
|
309
|
+
* unanchored, global regex, so a message that *explained* markdown by quoting a
|
|
310
|
+
* fenced sample had the sample unwrapped and rendered as live markup. Anchoring
|
|
311
|
+
* means a fence with anything before or after it is left alone.
|
|
312
|
+
*
|
|
313
|
+
* The trailing whitespace is `\s*`, not `[ \t]*\s*` -- `[ \t]` is already a
|
|
314
|
+
* subset of `\s`, so pairing them let the engine try every split between the
|
|
315
|
+
* two quantifiers over the same characters. On uncontrolled input (this runs
|
|
316
|
+
* on chat/model text) that is a polynomial-time blow-up: a closing delimiter
|
|
317
|
+
* followed by many tabs and then one stray character made a single `.exec`
|
|
318
|
+
* call take seconds instead of a fraction of a millisecond.
|
|
319
|
+
*
|
|
320
|
+
* Groups: 1 = the opening run, 2 = the info string, 3 = the body, 4 = the closing run.
|
|
321
|
+
*/
|
|
322
|
+
/*
|
|
323
|
+
* Hoisted rather than built per call. Both carry only `m` -- no `g` or `y` --
|
|
324
|
+
* so they hold no `lastIndex` between calls and are safe to share.
|
|
325
|
+
*/
|
|
326
|
+
const INNER_BACKTICK_FENCE = /^[ \t]*`{3,}/m;
|
|
327
|
+
const INNER_TILDE_FENCE = /^[ \t]*~{3,}/m;
|
|
328
|
+
const WHOLE_BODY_MARKDOWN_FENCE = /^\s*(`{3,}|~{3,})[ \t]*(markdown|md)[ \t]*\r?\n([\s\S]*?)\r?\n?(`{3,}|~{3,})\s*$/i;
|
|
329
|
+
/**
|
|
330
|
+
* Removes a fence that wraps the entire body and is labelled `markdown` or
|
|
331
|
+
* `md`, which is how models overwhelmingly emit a markdown answer. Returns the
|
|
332
|
+
* source untouched in every other case.
|
|
333
|
+
*
|
|
334
|
+
* An unlabelled fence is deliberately not unwrapped: it may be code the author
|
|
335
|
+
* meant to show, and there is nothing in the source to tell the two apart.
|
|
336
|
+
*/
|
|
337
|
+
export function unwrapMarkdownFence(source) {
|
|
338
|
+
const match = WHOLE_BODY_MARKDOWN_FENCE.exec(source);
|
|
339
|
+
if (match === null) {
|
|
340
|
+
return source;
|
|
341
|
+
}
|
|
342
|
+
const opening = match[1];
|
|
343
|
+
const body = match[3];
|
|
344
|
+
const closing = match[4];
|
|
345
|
+
// CommonMark lets a closing fence be LONGER than its opening, so the closing
|
|
346
|
+
// run is its own group and is checked here rather than by a `\1`
|
|
347
|
+
// backreference. The backreference did not refuse a longer close -- it
|
|
348
|
+
// matched the last three ticks and swept the extra one into the body, so
|
|
349
|
+
// `# Heading` came back as "# Heading\n`". Content corruption, not a refusal.
|
|
350
|
+
// Same character, and at least as long, is the actual rule.
|
|
351
|
+
if (closing[0] !== opening[0] || closing.length < opening.length) {
|
|
352
|
+
return source;
|
|
353
|
+
}
|
|
354
|
+
// A body still containing a fence run means the outer delimiters may not be a
|
|
355
|
+
// wrapper at all -- the regex is lazy to the LAST delimiter, so this shape is
|
|
356
|
+
// ambiguous. Refusing keeps a message quoting fenced code from being
|
|
357
|
+
// reinterpreted as markup, which is the failure this feature exists to avoid.
|
|
358
|
+
const innerFence = opening[0] === '~' ? INNER_TILDE_FENCE : INNER_BACKTICK_FENCE;
|
|
359
|
+
if (innerFence.test(body)) {
|
|
360
|
+
return source;
|
|
361
|
+
}
|
|
362
|
+
return body;
|
|
363
|
+
}
|
|
305
364
|
export function renderMarkdown(markdown, options = {}) {
|
|
365
|
+
const source = options.unwrapFence === true ? unwrapMarkdownFence(markdown) : markdown;
|
|
306
366
|
const instance = instanceFor(options);
|
|
307
|
-
const output = options.inline === true ? instance.parseInline(
|
|
367
|
+
const output = options.inline === true ? instance.parseInline(source) : instance.parse(source);
|
|
308
368
|
if (typeof output !== 'string') {
|
|
309
369
|
return '';
|
|
310
370
|
}
|
|
@@ -29,6 +29,21 @@ export type OptionalMarkdownTextProperties = {
|
|
|
29
29
|
* whitespace-only string, keeps only `markdown-table-wrapper`.
|
|
30
30
|
*/
|
|
31
31
|
tableWrapperClass?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Remove a fence that wraps the ENTIRE value and is labelled `markdown` or
|
|
34
|
+
* `md`, then render what was inside it as markdown.
|
|
35
|
+
*
|
|
36
|
+
* Models very often return a whole answer wrapped that way, which renders as
|
|
37
|
+
* a code block showing source rather than as a document. Off by default,
|
|
38
|
+
* because unwrapping is not always right: a message quoting a fenced sample
|
|
39
|
+
* would be reinterpreted as live markup, which is exactly what an
|
|
40
|
+
* unanchored implementation elsewhere did.
|
|
41
|
+
*
|
|
42
|
+
* Only the whole-body case is unwrapped. A fence with anything before or
|
|
43
|
+
* after it, an unlabelled fence, a fence labelled as another language, and a
|
|
44
|
+
* body that still contains a fence delimiter are all left alone.
|
|
45
|
+
*/
|
|
46
|
+
unwrapFence?: boolean;
|
|
32
47
|
/**
|
|
33
48
|
* Narrow which URL protocols survive on rendered links/images. See
|
|
34
49
|
* `MarkdownSanitizeOptions`.
|
|
@@ -115,6 +130,8 @@ export type RenderMarkdownOptions = {
|
|
|
115
130
|
tableLabel?: string;
|
|
116
131
|
/** Class name for the scroll region wrapping each table. See `MarkdownTextProperties.tableWrapperClass`. */
|
|
117
132
|
tableWrapperClass?: string;
|
|
133
|
+
/** Unwrap a whole-body ```markdown fence. See `MarkdownTextProperties.unwrapFence`. */
|
|
134
|
+
unwrapFence?: boolean;
|
|
118
135
|
/** Narrow the protocol allow-list. See `MarkdownSanitizeOptions`. */
|
|
119
136
|
sanitize?: MarkdownSanitizeOptions;
|
|
120
137
|
};
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
hierarchy = 'default',
|
|
37
37
|
leftIcon,
|
|
38
38
|
leftIconTestId,
|
|
39
|
-
usePortal = false
|
|
39
|
+
usePortal = false,
|
|
40
|
+
name
|
|
40
41
|
}: SelectProperties = $props();
|
|
41
42
|
|
|
42
43
|
function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
|
|
@@ -518,6 +519,16 @@
|
|
|
518
519
|
bind:this={containerEl}
|
|
519
520
|
{...typeof testId === 'string' ? { 'data-pw': testId, testID: testId } : {}}
|
|
520
521
|
>
|
|
522
|
+
{#if typeof name === 'string' && name.length > 0}
|
|
523
|
+
<!-- No native <select> exists to carry `name`/`value` -- this is a custom
|
|
524
|
+
combobox/listbox. One hidden input per selected id mirrors a native
|
|
525
|
+
`<select multiple>`, which likewise submits one FormData entry per
|
|
526
|
+
selected <option> under the same name; `disabled` matches native
|
|
527
|
+
behaviour by excluding all of them from the submission. -->
|
|
528
|
+
{#each value as id (id)}
|
|
529
|
+
<input type="hidden" {name} value={id} {disabled} />
|
|
530
|
+
{/each}
|
|
531
|
+
{/if}
|
|
521
532
|
<div class="select-control" class:has-clear={showClearButton}>
|
|
522
533
|
<div
|
|
523
534
|
class="select-trigger"
|
|
@@ -134,6 +134,17 @@ export type OptionalSelectProperties = {
|
|
|
134
134
|
* `--select-dropdown-z-index` if it must sit above an even higher overlay.
|
|
135
135
|
*/
|
|
136
136
|
usePortal?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Native `name` for form submission. Select has no underlying `<select>` —
|
|
139
|
+
* it is a custom combobox/listbox — so setting this renders one
|
|
140
|
+
* `<input type="hidden">` per entry in `value`, each carrying the
|
|
141
|
+
* selected item's `id` as its value. Omitted by default: no hidden inputs
|
|
142
|
+
* are rendered and nothing changes for an existing consumer. Mirrors a
|
|
143
|
+
* native `<select multiple>`, which likewise submits one entry per
|
|
144
|
+
* selected `<option>` under the same name; an empty `value` array
|
|
145
|
+
* submits nothing, matching a native select with no selection.
|
|
146
|
+
*/
|
|
147
|
+
name?: string;
|
|
137
148
|
};
|
|
138
149
|
export type SelectEventProperties = {
|
|
139
150
|
onchange?: (value: string[]) => void;
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -1188,11 +1188,18 @@
|
|
|
1188
1188
|
}
|
|
1189
1189
|
|
|
1190
1190
|
/* ── Container ──────────────────────────────────────────────────────────── */
|
|
1191
|
+
/* `hidden` is the default because it is what clips the table's corners to
|
|
1192
|
+
this element's border-radius; without it the first and last cells square
|
|
1193
|
+
off over the rounded border. A consumer who needs the container itself to
|
|
1194
|
+
scroll sideways sets this to `auto` and accepts that trade. Note the
|
|
1195
|
+
scrollable variant below sets `overflow-y`, and per the CSS overflow spec
|
|
1196
|
+
a `visible` on one axis computes to `auto` when the other is not visible,
|
|
1197
|
+
so `visible` here does not survive on a scrollable table. */
|
|
1191
1198
|
.table-container {
|
|
1192
1199
|
border: var(--table-border, 1px solid #e5e7eb);
|
|
1193
1200
|
border-radius: var(--table-border-radius, var(--radius, 4px));
|
|
1194
1201
|
width: var(--table-container-width, 100%);
|
|
1195
|
-
overflow: hidden;
|
|
1202
|
+
overflow: var(--table-container-overflow, hidden);
|
|
1196
1203
|
}
|
|
1197
1204
|
|
|
1198
1205
|
.scrollable-table {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
id,
|
|
11
11
|
ariaLabel,
|
|
12
12
|
ariaLabelledby,
|
|
13
|
-
onclick
|
|
13
|
+
onclick,
|
|
14
|
+
name,
|
|
15
|
+
value
|
|
14
16
|
}: ToggleProperties = $props();
|
|
15
17
|
|
|
16
18
|
// Stable across hydration so the hidden input stays named and text remains clickable.
|
|
@@ -40,6 +42,8 @@
|
|
|
40
42
|
type="checkbox"
|
|
41
43
|
{checked}
|
|
42
44
|
{disabled}
|
|
45
|
+
name={name ?? null}
|
|
46
|
+
value={value ?? null}
|
|
43
47
|
aria-label={ariaLabel?.trim() || null}
|
|
44
48
|
aria-labelledby={ariaLabelledby?.trim() || null}
|
|
45
49
|
onclick={handleCheckboxClick}
|
|
@@ -112,7 +116,7 @@
|
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
.input-checkbox:focus + .slider {
|
|
115
|
-
box-shadow: 0 0
|
|
119
|
+
box-shadow: var(--toggle-focus-ring, 0 0 0 3px rgba(33, 150, 243, 0.3));
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
.input-checkbox:checked + .slider:before {
|
|
@@ -19,6 +19,15 @@ export type OptionalToggleProperties = {
|
|
|
19
19
|
* `text` or `inputAriaLabel` for labels outside the shadow root.
|
|
20
20
|
*/
|
|
21
21
|
ariaLabelledby?: string;
|
|
22
|
+
/** Native `name` on the underlying `<input type="checkbox">`, so the switch
|
|
23
|
+
* participates in a surrounding `<form>`'s submission. Omitted by default:
|
|
24
|
+
* the input stays nameless and outside `FormData`, exactly as before this
|
|
25
|
+
* prop existed. */
|
|
26
|
+
name?: string;
|
|
27
|
+
/** Native `value` submitted when on. Defaults to `"on"` — the native
|
|
28
|
+
* checkbox default — when omitted. An off toggle never appears in
|
|
29
|
+
* `FormData`, matching native behaviour. */
|
|
30
|
+
value?: string;
|
|
22
31
|
};
|
|
23
32
|
export type ToggleEventProperties = {
|
|
24
33
|
onclick?: (checked: boolean) => void;
|