@juspay/svelte-ui-components 4.8.0 → 4.9.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/Card/Card.svelte +21 -3
- package/dist/Card/properties.d.ts +32 -0
- package/dist/ChatComposer/ChatComposer.svelte +48 -7
- package/dist/ChatComposer/controlDisabled.d.ts +10 -0
- package/dist/ChatComposer/controlDisabled.js +12 -0
- package/dist/ChatComposer/properties.d.ts +23 -1
- package/dist/ChatComposer/submitResult.d.ts +34 -0
- package/dist/ChatComposer/submitResult.js +20 -0
- package/dist/Combobox/Combobox.svelte.d.ts +1 -1
- package/dist/MarkdownText/markdown.js +24 -3
- package/dist/Modal/Modal.svelte +121 -6
- package/dist/Modal/focus-trap.d.ts +42 -0
- package/dist/Modal/focus-trap.js +57 -0
- package/dist/Modal/properties.d.ts +44 -0
- package/dist/Select/Select.svelte.d.ts +1 -1
- package/dist/Sheet/Sheet.svelte +38 -1
- package/dist/Sheet/properties.d.ts +9 -1
- package/dist/Toolbar/Toolbar.svelte +24 -4
- package/dist/Toolbar/properties.d.ts +11 -0
- package/dist-wc/index.js +1921 -1646
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus-trap utility for Modal (#529). Pure DOM-reading functions, no Svelte
|
|
3
|
+
* runtime dependency, so they can be unit tested without mounting the
|
|
4
|
+
* component -- mirrors the split Tooltip already uses for its action logic
|
|
5
|
+
* (see tooltip-action.test.ts).
|
|
6
|
+
*
|
|
7
|
+
* Modal.svelte owns the event wiring (which key was pressed, calling
|
|
8
|
+
* .focus()); this module owns the two decisions that wiring needs answered:
|
|
9
|
+
* "what are the edges of the tab order in here" and "given where focus is and
|
|
10
|
+
* which way Tab was pressed, should it wrap, and to where".
|
|
11
|
+
*/
|
|
12
|
+
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
13
|
+
/**
|
|
14
|
+
* The first and last focusable descendants of `container`, in DOM order.
|
|
15
|
+
* Both are null when the container is null or holds nothing focusable.
|
|
16
|
+
*/
|
|
17
|
+
export function getFocusTrapBoundary(container) {
|
|
18
|
+
if (container === null) {
|
|
19
|
+
return { first: null, last: null };
|
|
20
|
+
}
|
|
21
|
+
const focusable = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
22
|
+
return { first: focusable.item(0), last: focusable.item(focusable.length - 1) };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Given a Tab/Shift+Tab keydown inside a focus-trapped container, returns the
|
|
26
|
+
* element focus should move to, or null when the default browser behavior
|
|
27
|
+
* (move to the next/previous focusable element) should be left alone.
|
|
28
|
+
*
|
|
29
|
+
* Wrapping only happens exactly at an edge: focus sits on the last focusable
|
|
30
|
+
* element and Tab (not Shift+Tab) was pressed, or focus sits on the first and
|
|
31
|
+
* Shift+Tab was pressed. Anywhere else in the middle of the tab order, the
|
|
32
|
+
* browser's own handling is correct and this returns null.
|
|
33
|
+
*/
|
|
34
|
+
export function focusTrapTabTarget(params) {
|
|
35
|
+
const { first, last } = getFocusTrapBoundary(params.container);
|
|
36
|
+
if (first === null || last === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const edge = params.shiftKey ? first : last;
|
|
40
|
+
if (params.activeElement !== edge) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return params.shiftKey ? last : first;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Moves focus into `container` -- its first focusable descendant, or the
|
|
47
|
+
* container itself as a fallback for a modal with no focusable content (it
|
|
48
|
+
* must carry tabindex="-1" for that fallback to actually take focus). A no-op
|
|
49
|
+
* when the container is null.
|
|
50
|
+
*/
|
|
51
|
+
export function focusEntryPoint(container) {
|
|
52
|
+
if (container === null) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const { first } = getFocusTrapBoundary(container);
|
|
56
|
+
(first ?? container).focus();
|
|
57
|
+
}
|
|
@@ -11,6 +11,12 @@ export type ModalAlign = 'top' | 'center' | 'bottom';
|
|
|
11
11
|
* Leave unset to keep the existing per-align default.
|
|
12
12
|
*/
|
|
13
13
|
export type ModalEntryAnimation = 'fade' | 'slide-up' | 'slide-down';
|
|
14
|
+
/**
|
|
15
|
+
* 'alertdialog' is for a dialog that interrupts to demand an immediate
|
|
16
|
+
* response (e.g. a destructive confirmation) -- semantically distinct from an
|
|
17
|
+
* ordinary 'dialog' in how assistive tech announces it. See the `role` prop.
|
|
18
|
+
*/
|
|
19
|
+
export type ModalRole = 'dialog' | 'alertdialog';
|
|
14
20
|
export type ModalProperties = OptionalModalProperties & ModalEventProperties;
|
|
15
21
|
export type OptionalModalProperties = {
|
|
16
22
|
size?: ModalSize;
|
|
@@ -38,6 +44,15 @@ export type OptionalModalProperties = {
|
|
|
38
44
|
leftImageTestId?: string;
|
|
39
45
|
/** Accessible name for the left header image's role="button" wrapper (e.g. a back control). Rendered as aria-label. */
|
|
40
46
|
leftImageAriaLabel?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Accessible name for the overlay's `role="button"` wrapper, rendered as
|
|
49
|
+
* `aria-label` -- same pattern as `leftImageAriaLabel` and
|
|
50
|
+
* `header.buttonAriaLabel`. Only meaningful when the overlay is dismissible
|
|
51
|
+
* (an `onoverlayclick` or `ondismiss` handler is supplied), which is the
|
|
52
|
+
* only state where the overlay is announced as a focusable "button" at all.
|
|
53
|
+
* Default: unset (no accessible name).
|
|
54
|
+
*/
|
|
55
|
+
overlayAriaLabel?: string;
|
|
41
56
|
testId?: string;
|
|
42
57
|
content?: Snippet;
|
|
43
58
|
footerSnippet?: Snippet;
|
|
@@ -52,6 +67,24 @@ export type OptionalModalProperties = {
|
|
|
52
67
|
lockScroll?: boolean;
|
|
53
68
|
/** Milliseconds after mount to automatically fire onclose, e.g. for a transient success/confirmation modal. null (default) disables auto-dismiss. */
|
|
54
69
|
autoDismissAfter?: number | null;
|
|
70
|
+
/**
|
|
71
|
+
* Accessible name for the modal, rendered as `aria-label` on the modal
|
|
72
|
+
* content panel (the element carrying `role`/`aria-modal` when `role` is
|
|
73
|
+
* set). Without it a screen reader announces an unnamed dialog. Default:
|
|
74
|
+
* unset (no aria-label) -- matches today's behavior, where the panel has no
|
|
75
|
+
* accessible name at all.
|
|
76
|
+
*/
|
|
77
|
+
ariaLabel?: string;
|
|
78
|
+
/**
|
|
79
|
+
* ARIA role for the modal content panel. Default: unset -- the panel
|
|
80
|
+
* renders no `role` and no `aria-modal` at all, byte-for-byte matching
|
|
81
|
+
* behavior prior to this prop's introduction, so an existing consumer who
|
|
82
|
+
* never touches `role` sees no DOM/ARIA change. Set it explicitly (e.g.
|
|
83
|
+
* `'dialog'` or `'alertdialog'`) to opt in: the panel then also carries
|
|
84
|
+
* `aria-modal="true"`, since a role without aria-modal is not a complete
|
|
85
|
+
* dialog announcement.
|
|
86
|
+
*/
|
|
87
|
+
role?: ModalRole;
|
|
55
88
|
};
|
|
56
89
|
export type ModalEventProperties = {
|
|
57
90
|
onclose?: () => void;
|
|
@@ -61,4 +94,15 @@ export type ModalEventProperties = {
|
|
|
61
94
|
onsecondarybuttonclick?: (event: MouseEvent) => void;
|
|
62
95
|
onoverlayclick?: () => void;
|
|
63
96
|
onkeydown?: (event: KeyboardEvent) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Fires on every path that dismisses the modal from the OUTSIDE: an overlay
|
|
99
|
+
* click, the Escape key, and a hardware back-press. A superset of
|
|
100
|
+
* onoverlayclick (backdrop click + Escape only) and onclose (hardware
|
|
101
|
+
* back-press + autoDismissAfter only) -- wire this single callback instead
|
|
102
|
+
* of both when all a consumer needs is "the modal wants to close". Both
|
|
103
|
+
* existing events keep firing unchanged alongside it; autoDismissAfter's
|
|
104
|
+
* timer still fires only onclose, not ondismiss, since that path is the
|
|
105
|
+
* modal closing itself rather than something dismissing it.
|
|
106
|
+
*/
|
|
107
|
+
ondismiss?: () => void;
|
|
64
108
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { SelectProperties } from './properties';
|
|
2
|
-
declare const Select: import("svelte").Component<SelectProperties, {}, "
|
|
2
|
+
declare const Select: import("svelte").Component<SelectProperties, {}, "open" | "value">;
|
|
3
3
|
type Select = ReturnType<typeof Select>;
|
|
4
4
|
export default Select;
|
package/dist/Sheet/Sheet.svelte
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
showOverlay = true,
|
|
13
13
|
dismissOnOutsideClick = showOverlay,
|
|
14
14
|
showCloseButton = true,
|
|
15
|
+
headingLevel,
|
|
15
16
|
testId,
|
|
16
17
|
content,
|
|
17
18
|
footer,
|
|
@@ -34,6 +35,10 @@
|
|
|
34
35
|
return { x: 0, y: -400, duration: 300 };
|
|
35
36
|
case 'bottom':
|
|
36
37
|
return { x: 0, y: 400, duration: 300 };
|
|
38
|
+
// No edge to fly in from — a centered dialog only fades, so x/y stay
|
|
39
|
+
// at 0 and `fly` degrades to exactly the overlay's own fade transition.
|
|
40
|
+
case 'center':
|
|
41
|
+
return { x: 0, y: 0, duration: 300 };
|
|
37
42
|
}
|
|
38
43
|
});
|
|
39
44
|
|
|
@@ -101,6 +106,20 @@
|
|
|
101
106
|
onafterclose();
|
|
102
107
|
}
|
|
103
108
|
}
|
|
109
|
+
|
|
110
|
+
// headingLevel's `1 | 2 | 3 | 4 | 5 | 6` union only constrains Svelte-import
|
|
111
|
+
// consumers. Through the web component, attributes are untyped strings
|
|
112
|
+
// coerced to Number — heading-level="7"/"0"/"-1"/"foo" would otherwise reach
|
|
113
|
+
// `h${headingLevel}` unchecked and render an invalid tag (<h7>, <h0>, <h-1>,
|
|
114
|
+
// <hNaN>). Validate at runtime and fall back to the existing <span>.
|
|
115
|
+
const headingTag = $derived(
|
|
116
|
+
typeof headingLevel === 'number' &&
|
|
117
|
+
Number.isInteger(headingLevel) &&
|
|
118
|
+
headingLevel >= 1 &&
|
|
119
|
+
headingLevel <= 6
|
|
120
|
+
? `h${headingLevel}`
|
|
121
|
+
: null
|
|
122
|
+
);
|
|
104
123
|
</script>
|
|
105
124
|
|
|
106
125
|
{#if open}
|
|
@@ -135,7 +154,11 @@
|
|
|
135
154
|
{#if typeof title === 'string' || showCloseButton}
|
|
136
155
|
<div class="sheet-header">
|
|
137
156
|
{#if typeof title === 'string'}
|
|
138
|
-
|
|
157
|
+
{#if headingTag !== null}
|
|
158
|
+
<svelte:element this={headingTag} class="sheet-title">{title}</svelte:element>
|
|
159
|
+
{:else}
|
|
160
|
+
<span class="sheet-title">{title}</span>
|
|
161
|
+
{/if}
|
|
139
162
|
{/if}
|
|
140
163
|
{#if showCloseButton}
|
|
141
164
|
<div class="sheet-close-button">
|
|
@@ -241,6 +264,20 @@
|
|
|
241
264
|
border-top: var(--sheet-border, none);
|
|
242
265
|
}
|
|
243
266
|
|
|
267
|
+
/* `side="center"` is a fixed-size floating dialog, not an edge-anchored
|
|
268
|
+
panel: no top/bottom/left/right offset, sized by its own tokens rather
|
|
269
|
+
than --sheet-width/--sheet-height (so an existing left/right/top/bottom
|
|
270
|
+
theme is untouched by adding this variant), and centered with a
|
|
271
|
+
transform instead of stretching to an edge. */
|
|
272
|
+
.sheet-panel.center {
|
|
273
|
+
top: 50%;
|
|
274
|
+
left: 50%;
|
|
275
|
+
transform: translate(-50%, -50%);
|
|
276
|
+
width: var(--sheet-center-width, 480px);
|
|
277
|
+
max-width: var(--sheet-center-max-width, calc(100vw - 32px));
|
|
278
|
+
max-height: var(--sheet-center-max-height, 90vh);
|
|
279
|
+
}
|
|
280
|
+
|
|
244
281
|
.sheet-header {
|
|
245
282
|
display: flex;
|
|
246
283
|
align-items: center;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
export type SheetSide = 'left' | 'right' | 'top' | 'bottom';
|
|
2
|
+
export type SheetSide = 'left' | 'right' | 'top' | 'bottom' | 'center';
|
|
3
3
|
export type SheetProperties = MandatorySheetProperties & OptionalSheetProperties & SheetEventProperties;
|
|
4
4
|
export type MandatorySheetProperties = {
|
|
5
5
|
content: Snippet;
|
|
@@ -18,6 +18,14 @@ export type OptionalSheetProperties = {
|
|
|
18
18
|
*/
|
|
19
19
|
dismissOnOutsideClick?: boolean;
|
|
20
20
|
showCloseButton?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Renders `title` through a real `<h1>`-`<h6>` heading element instead of
|
|
23
|
+
* the default `<span>`. Omit to keep the existing `<span>` markup
|
|
24
|
+
* unchanged — the default when this prop is not set. Set it to the level
|
|
25
|
+
* that is correct in the surrounding document outline (a sheet opened from
|
|
26
|
+
* a page whose top-level heading is an `<h1>` typically wants `2`).
|
|
27
|
+
*/
|
|
28
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
21
29
|
testId?: string;
|
|
22
30
|
footer?: Snippet;
|
|
23
31
|
classes?: string;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
text,
|
|
7
7
|
backIcon,
|
|
8
8
|
backLabel = 'Back',
|
|
9
|
+
backHref,
|
|
9
10
|
leftContent,
|
|
10
11
|
centerContent,
|
|
11
12
|
rightContent,
|
|
@@ -23,6 +24,22 @@
|
|
|
23
24
|
// The icon is decorative, so the label is the button's only accessible name — an empty or
|
|
24
25
|
// whitespace-only value must not strip it.
|
|
25
26
|
const backAccessibleName = $derived(backLabel.trim().length > 0 ? backLabel : 'Back');
|
|
27
|
+
// A real link only when a non-empty href is actually given — same emptiness check the
|
|
28
|
+
// rest of the component already applies to backIcon, so `backHref=""` behaves like "unset"
|
|
29
|
+
// rather than rendering a same-page anchor.
|
|
30
|
+
const hasBackHref = $derived(typeof backHref === 'string' && backHref.length > 0);
|
|
31
|
+
|
|
32
|
+
// A native <a> does not fire `click` on Space — only Enter — so the keyboard activation the
|
|
33
|
+
// old <button> gave for free would silently regress when backHref swaps the tag. Enter already
|
|
34
|
+
// reaches `onbackclick` through the anchor's native click, so only Space needs help here; the
|
|
35
|
+
// consumer's own `onkeydown` still runs unconditionally, same as it always has.
|
|
36
|
+
const handleBackKeydown = (event: KeyboardEvent): void => {
|
|
37
|
+
onkeydown?.(event);
|
|
38
|
+
if (hasBackHref && event.key === ' ') {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
onbackclick?.();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
26
43
|
</script>
|
|
27
44
|
|
|
28
45
|
<div class="toolbar {classes ?? ''}" data-pw={testId} testID={testId}>
|
|
@@ -34,12 +51,15 @@
|
|
|
34
51
|
{#if typeof leftContent === 'function'}
|
|
35
52
|
{@render leftContent()}
|
|
36
53
|
{:else if showBackControl}
|
|
37
|
-
<
|
|
38
|
-
|
|
54
|
+
<svelte:element
|
|
55
|
+
this={hasBackHref ? 'a' : 'button'}
|
|
56
|
+
type={hasBackHref ? null : 'button'}
|
|
57
|
+
href={hasBackHref ? backHref : null}
|
|
58
|
+
role={null}
|
|
39
59
|
class="back"
|
|
40
60
|
aria-label={backAccessibleName}
|
|
41
61
|
onclick={onbackclick}
|
|
42
|
-
{
|
|
62
|
+
onkeydown={handleBackKeydown}
|
|
43
63
|
>
|
|
44
64
|
{#if typeof backIcon === 'string'}
|
|
45
65
|
<img src={backIcon} alt="" />
|
|
@@ -56,7 +76,7 @@
|
|
|
56
76
|
/>
|
|
57
77
|
</svg>
|
|
58
78
|
{/if}
|
|
59
|
-
</
|
|
79
|
+
</svelte:element>
|
|
60
80
|
{/if}
|
|
61
81
|
{#if typeof centerContent === 'function'}
|
|
62
82
|
<div class="center-content">
|
|
@@ -4,6 +4,17 @@ export type ToolbarProperties = ToolbarEventProperties & {
|
|
|
4
4
|
text?: string | null;
|
|
5
5
|
backIcon?: string | null;
|
|
6
6
|
backLabel?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Renders the built-in back control as a real `<a href={backHref}>` instead of a
|
|
9
|
+
* `<button>` — for back navigation that is a real link (browser back-forward, open-in-new-tab,
|
|
10
|
+
* a working href with JS disabled) rather than only reachable through `onbackclick`.
|
|
11
|
+
* Omit it (the default) for the existing callback-only `<button>`. `onbackclick`
|
|
12
|
+
* still fires as the anchor's `click` handler when both are supplied, so navigation and any
|
|
13
|
+
* side effect (e.g. analytics) coexist exactly as they do on the library's own `Button`
|
|
14
|
+
* `href` + `onclick`. Ignored when `leftContent` is provided or the back control is hidden
|
|
15
|
+
* (`showBackButton={false}` or `backIcon={null}`).
|
|
16
|
+
*/
|
|
17
|
+
backHref?: string;
|
|
7
18
|
leftContent?: Snippet;
|
|
8
19
|
centerContent?: Snippet;
|
|
9
20
|
rightContent?: Snippet;
|