@juspay/svelte-ui-components 4.7.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/Snippet/Snippet.svelte +16 -3
- package/dist/Snippet/copyResetTimer.d.ts +21 -0
- package/dist/Snippet/copyResetTimer.js +20 -0
- package/dist/Snippet/properties.d.ts +4 -0
- package/dist/Toolbar/Toolbar.svelte +24 -4
- package/dist/Toolbar/properties.d.ts +11 -0
- package/dist-wc/index.js +1985 -1666
- 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;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
2
3
|
import type { SnippetProperties } from './properties';
|
|
3
4
|
import Button from '../Button/Button.svelte';
|
|
4
5
|
import copySvg from '../assets/copy.svg?raw';
|
|
6
|
+
import { createCopyResetTimer } from './copyResetTimer';
|
|
5
7
|
|
|
6
8
|
let {
|
|
7
9
|
text,
|
|
@@ -9,20 +11,31 @@
|
|
|
9
11
|
showCopyButton = true,
|
|
10
12
|
testId,
|
|
11
13
|
copyIcon,
|
|
14
|
+
copiedLabel = 'Copied!',
|
|
15
|
+
copyResetMs = 2000,
|
|
12
16
|
oncopy,
|
|
13
17
|
classes
|
|
14
18
|
}: SnippetProperties = $props();
|
|
15
19
|
|
|
16
20
|
let copied = $state(false);
|
|
17
21
|
|
|
22
|
+
// The reset timer used to be a bare setTimeout with nothing clearing it: a
|
|
23
|
+
// second copy before the first one's timer fired left two timers racing to
|
|
24
|
+
// flip `copied`, and navigating away mid-flash left the timer armed against
|
|
25
|
+
// a destroyed component. `createCopyResetTimer` (unit-tested on its own in
|
|
26
|
+
// copyResetTimer.test.ts) guarantees only one timer is ever pending, and
|
|
27
|
+
// onDestroy guarantees it never outlives the component it targets.
|
|
28
|
+
const resetTimer = createCopyResetTimer();
|
|
29
|
+
onDestroy(resetTimer.cancel);
|
|
30
|
+
|
|
18
31
|
async function handleCopy() {
|
|
19
32
|
try {
|
|
20
33
|
await navigator.clipboard.writeText(text);
|
|
21
34
|
copied = true;
|
|
22
35
|
oncopy?.();
|
|
23
|
-
|
|
36
|
+
resetTimer.arm(() => {
|
|
24
37
|
copied = false;
|
|
25
|
-
},
|
|
38
|
+
}, copyResetMs);
|
|
26
39
|
} catch {
|
|
27
40
|
// Clipboard API unavailable (non-secure context, iframe restrictions)
|
|
28
41
|
}
|
|
@@ -42,7 +55,7 @@
|
|
|
42
55
|
<div class="snippet-copy">
|
|
43
56
|
<Button onclick={handleCopy} ariaLabel="Copy to clipboard">
|
|
44
57
|
{#if copied}
|
|
45
|
-
<span class="snippet-copied">
|
|
58
|
+
<span class="snippet-copied" aria-live="polite">{copiedLabel}</span>
|
|
46
59
|
{:else if typeof copyIcon === 'function'}
|
|
47
60
|
{@render copyIcon()}
|
|
48
61
|
{:else}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages the single "revert the copied-feedback flag" timer behind Snippet's
|
|
3
|
+
* copy button (#530). Extracted as a plain, Svelte-free module so the timer
|
|
4
|
+
* safety this fixes -- never leaving two timers racing after a fast double
|
|
5
|
+
* copy, never firing against a component that has already unmounted -- is
|
|
6
|
+
* unit-testable on its own, the same way `Tooltip/tooltip-action.ts` keeps
|
|
7
|
+
* its DOM-timing logic outside the component.
|
|
8
|
+
*/
|
|
9
|
+
export type CopyResetTimer = {
|
|
10
|
+
/**
|
|
11
|
+
* (Re)arms the timer: clears any timer already pending, then schedules
|
|
12
|
+
* `onReset` to run after `delayMs`.
|
|
13
|
+
*/
|
|
14
|
+
arm: (onReset: () => void, delayMs: number) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Cancels a pending timer, if any. Safe to call when nothing is pending --
|
|
17
|
+
* this is what Snippet calls from `onDestroy`.
|
|
18
|
+
*/
|
|
19
|
+
cancel: () => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function createCopyResetTimer(): CopyResetTimer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function createCopyResetTimer() {
|
|
2
|
+
let handle = null;
|
|
3
|
+
return {
|
|
4
|
+
arm(onReset, delayMs) {
|
|
5
|
+
if (handle !== null) {
|
|
6
|
+
clearTimeout(handle);
|
|
7
|
+
}
|
|
8
|
+
handle = setTimeout(() => {
|
|
9
|
+
handle = null;
|
|
10
|
+
onReset();
|
|
11
|
+
}, delayMs);
|
|
12
|
+
},
|
|
13
|
+
cancel() {
|
|
14
|
+
if (handle !== null) {
|
|
15
|
+
clearTimeout(handle);
|
|
16
|
+
handle = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -8,6 +8,10 @@ export type OptionalSnippetProperties = {
|
|
|
8
8
|
showCopyButton?: boolean;
|
|
9
9
|
testId?: string;
|
|
10
10
|
copyIcon?: Snippet;
|
|
11
|
+
/** Text shown in place of the copy icon after a successful copy. Defaults to `'Copied!'`. */
|
|
12
|
+
copiedLabel?: string;
|
|
13
|
+
/** Milliseconds before the copied feedback reverts to the copy icon. Defaults to `2000`. */
|
|
14
|
+
copyResetMs?: number;
|
|
11
15
|
classes?: string;
|
|
12
16
|
};
|
|
13
17
|
export type SnippetEventProperties = {
|
|
@@ -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;
|