@juspay/svelte-ui-components 2.122.0 → 2.124.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/Button/Button.svelte +6 -1
- package/dist/Button/properties.d.ts +18 -0
- package/dist/Input/Input.svelte +47 -8
- package/dist/Input/properties.d.ts +19 -1
- package/dist/Menu/Menu.svelte +78 -16
- package/dist/Menu/properties.d.ts +28 -1
- package/dist/types.d.ts +13 -2
- package/package.json +1 -1
|
@@ -22,8 +22,11 @@
|
|
|
22
22
|
testId,
|
|
23
23
|
ariaLabel,
|
|
24
24
|
ariaExpanded,
|
|
25
|
+
ariaHaspopup,
|
|
25
26
|
ariaSelected,
|
|
27
|
+
ariaBusy,
|
|
26
28
|
role,
|
|
29
|
+
title,
|
|
27
30
|
onclick,
|
|
28
31
|
onkeydown = () => {},
|
|
29
32
|
onkeyup = () => {},
|
|
@@ -88,8 +91,10 @@
|
|
|
88
91
|
role={role ?? null}
|
|
89
92
|
aria-label={ariaLabel ?? null}
|
|
90
93
|
aria-expanded={ariaExpanded ?? null}
|
|
94
|
+
aria-haspopup={ariaHaspopup ?? null}
|
|
91
95
|
aria-selected={ariaSelected ?? null}
|
|
92
|
-
aria-busy={isBusy || null}
|
|
96
|
+
aria-busy={isBusy || ariaBusy || null}
|
|
97
|
+
title={title ?? null}
|
|
93
98
|
type={href ? null : type}
|
|
94
99
|
disabled={href ? null : isDisabled}
|
|
95
100
|
href={href ? (isDisabled ? null : href) : null}
|
|
@@ -53,8 +53,26 @@ export type OptionalButtonProperties = {
|
|
|
53
53
|
children?: Snippet;
|
|
54
54
|
ariaLabel?: string;
|
|
55
55
|
ariaExpanded?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Native `aria-haspopup`. Needed when the button is the trigger for a menu, listbox or
|
|
58
|
+
* dialog — Menu hands exactly this to its `trigger` snippet.
|
|
59
|
+
*/
|
|
60
|
+
ariaHaspopup?: 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | boolean;
|
|
56
61
|
ariaSelected?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Native `aria-busy`, for a control that stays usable while related data loads.
|
|
64
|
+
* Deliberately separate from `loading`, which also renders the spinner and
|
|
65
|
+
* disables the button — a trigger whose *contents* are still loading must stay
|
|
66
|
+
* clickable, so it cannot express that state through `loading`.
|
|
67
|
+
*/
|
|
68
|
+
ariaBusy?: boolean;
|
|
57
69
|
role?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Native `title`, rendered as the browser's own hover tooltip. Distinct from
|
|
72
|
+
* `ariaLabel`, which names the control for assistive tech without any visible
|
|
73
|
+
* affordance; an icon-only button generally wants both.
|
|
74
|
+
*/
|
|
75
|
+
title?: string;
|
|
58
76
|
disabled?: boolean;
|
|
59
77
|
classes?: string;
|
|
60
78
|
/**
|
package/dist/Input/Input.svelte
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
infoMessage = '',
|
|
13
13
|
validators = [],
|
|
14
14
|
disable = false,
|
|
15
|
+
readonly = false,
|
|
16
|
+
spellcheck = null,
|
|
15
17
|
validationPattern = null,
|
|
16
18
|
inProgressPattern = null,
|
|
17
19
|
addFocusColor = false,
|
|
@@ -113,6 +115,9 @@
|
|
|
113
115
|
const hasRightIcon = $derived(typeof rightIcon === 'function');
|
|
114
116
|
|
|
115
117
|
const charCount = $derived(value?.length ?? 0);
|
|
118
|
+
// Every numeric use below is either tel-only normalisation or the character
|
|
119
|
+
// counter; `null` means "no attribute", not "no ceiling on those paths".
|
|
120
|
+
const effectiveMaxLength = $derived(maxLength ?? 1000);
|
|
116
121
|
const effectiveResize = $derived(autoResize ? 'none' : resize);
|
|
117
122
|
|
|
118
123
|
// Grow the textarea to fit its content between minRows and maxRows.
|
|
@@ -128,8 +133,16 @@
|
|
|
128
133
|
const border = parseFloat(styles.borderTopWidth) + parseFloat(styles.borderBottomWidth);
|
|
129
134
|
const lower = minRows ?? rows ?? 2;
|
|
130
135
|
const minHeight = lower * lineHeight + verticalPadding + border;
|
|
131
|
-
const
|
|
136
|
+
const rowsCeiling =
|
|
132
137
|
maxRows != null ? maxRows * lineHeight + verticalPadding + border : Number.POSITIVE_INFINITY;
|
|
138
|
+
// --input-max-height is a ceiling too. Reading only maxRows left it at Infinity, so the
|
|
139
|
+
// inline height grew past the CSS clamp and overflowY was set to `hidden` — the box
|
|
140
|
+
// stopped at the right size but its overflow became unreachable instead of scrollable.
|
|
141
|
+
const styleCeiling = parseFloat(styles.maxHeight);
|
|
142
|
+
const maxHeight = Math.min(
|
|
143
|
+
rowsCeiling,
|
|
144
|
+
Number.isFinite(styleCeiling) ? styleCeiling : Number.POSITIVE_INFINITY
|
|
145
|
+
);
|
|
133
146
|
const nextHeight = Math.min(Math.max(el.scrollHeight, minHeight), maxHeight);
|
|
134
147
|
el.style.height = `${nextHeight}px`;
|
|
135
148
|
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
@@ -161,16 +174,16 @@
|
|
|
161
174
|
inputElement.value = value;
|
|
162
175
|
return;
|
|
163
176
|
}
|
|
164
|
-
if (numberLength >
|
|
177
|
+
if (numberLength > effectiveMaxLength) {
|
|
165
178
|
const existingInput = value;
|
|
166
|
-
if (existingInput.length ===
|
|
179
|
+
if (existingInput.length === effectiveMaxLength) {
|
|
167
180
|
inputElement.value = applyTextPresentation(value);
|
|
168
181
|
return;
|
|
169
182
|
}
|
|
170
183
|
/**
|
|
171
184
|
* choose last max length number of digits if length is bigger than max length passed in props
|
|
172
185
|
*/
|
|
173
|
-
currentValue = currentValue.substring(numberLength -
|
|
186
|
+
currentValue = currentValue.substring(numberLength - effectiveMaxLength);
|
|
174
187
|
}
|
|
175
188
|
currentValue = applyTextPresentation(currentValue);
|
|
176
189
|
inputElement.value = currentValue;
|
|
@@ -189,6 +202,15 @@
|
|
|
189
202
|
return;
|
|
190
203
|
}
|
|
191
204
|
|
|
205
|
+
// Everything below the tel branch is tel-specific digit normalisation, and
|
|
206
|
+
// onPaste was only ever invoked from inside it — so a non-tel field (notably
|
|
207
|
+
// useTextArea) had no way to observe a paste at all. Hand the event over
|
|
208
|
+
// before that branch and return, leaving tel's behaviour byte-identical.
|
|
209
|
+
if (dataType !== 'tel') {
|
|
210
|
+
onPaste(event);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
192
214
|
if (event.clipboardData) {
|
|
193
215
|
if (dataType === 'tel') {
|
|
194
216
|
let unfilteredNumber = event.clipboardData.getData('text');
|
|
@@ -210,12 +232,12 @@
|
|
|
210
232
|
/**
|
|
211
233
|
* user pasted 10+ digit number , overrides all cases
|
|
212
234
|
*/
|
|
213
|
-
if (filteredNumber.length >
|
|
235
|
+
if (filteredNumber.length > effectiveMaxLength) {
|
|
214
236
|
/**
|
|
215
237
|
* choose last max length number of digits if length is bigger than max length passed in props
|
|
216
238
|
*/
|
|
217
239
|
const finalValue = applyTextPresentation(
|
|
218
|
-
filteredNumber.substring(filteredNumberLength -
|
|
240
|
+
filteredNumber.substring(filteredNumberLength - effectiveMaxLength)
|
|
219
241
|
);
|
|
220
242
|
// Adding reactivity
|
|
221
243
|
value = finalValue;
|
|
@@ -281,6 +303,8 @@
|
|
|
281
303
|
style:resize={effectiveResize}
|
|
282
304
|
rows={rows ?? null}
|
|
283
305
|
disabled={disable}
|
|
306
|
+
readonly={readonly || null}
|
|
307
|
+
{spellcheck}
|
|
284
308
|
bind:this={inputElement}
|
|
285
309
|
maxlength={dataType === 'tel' ? null : maxLength}
|
|
286
310
|
minlength={minLength}
|
|
@@ -311,6 +335,8 @@
|
|
|
311
335
|
testID={testId}
|
|
312
336
|
class:action-input={actionInput}
|
|
313
337
|
disabled={disable}
|
|
338
|
+
readonly={readonly || null}
|
|
339
|
+
{spellcheck}
|
|
314
340
|
bind:this={inputElement}
|
|
315
341
|
maxlength={dataType === 'tel' ? null : maxLength}
|
|
316
342
|
minlength={minLength}
|
|
@@ -375,8 +401,8 @@
|
|
|
375
401
|
</div>
|
|
376
402
|
{/if}
|
|
377
403
|
{#if useTextArea && showCount && !actionInput}
|
|
378
|
-
<div class="input-char-count" class:at-limit={charCount >=
|
|
379
|
-
{charCount}/{
|
|
404
|
+
<div class="input-char-count" class:at-limit={charCount >= effectiveMaxLength}>
|
|
405
|
+
{charCount}/{effectiveMaxLength}
|
|
380
406
|
</div>
|
|
381
407
|
{/if}
|
|
382
408
|
</div>
|
|
@@ -386,6 +412,13 @@
|
|
|
386
412
|
input {
|
|
387
413
|
box-sizing: var(--input-box-sizing, border-box);
|
|
388
414
|
height: var(--input-height, fit-content);
|
|
415
|
+
|
|
416
|
+
/* Both default to the CSS initial value, so a consumer that sets neither is
|
|
417
|
+
byte-identical to before. A textarea that grows with its content needs a
|
|
418
|
+
ceiling before it can scroll, and one used as a paste target needs a floor;
|
|
419
|
+
neither was reachable through the --input-* surface. */
|
|
420
|
+
min-height: var(--input-min-height, auto);
|
|
421
|
+
max-height: var(--input-max-height, none);
|
|
389
422
|
background-color: var(--input-background, white);
|
|
390
423
|
font-size: var(--input-font-size, 16px) !important;
|
|
391
424
|
font-family: var(--input-font-family, inherit);
|
|
@@ -393,6 +426,12 @@
|
|
|
393
426
|
outline: none;
|
|
394
427
|
padding: var(--input-padding, 16px);
|
|
395
428
|
font-weight: var(--input-font-weight, 500);
|
|
429
|
+
|
|
430
|
+
/* `normal` is what a textarea/input computes today regardless of any inherited
|
|
431
|
+
value — the UA sheet sets it, and inheritance loses to a UA declaration on the
|
|
432
|
+
element itself. So the default here is byte-identical for existing consumers,
|
|
433
|
+
and this is the only way a consumer can set it at all. */
|
|
434
|
+
line-height: var(--input-line-height, normal);
|
|
396
435
|
width: var(--input-width, fit-content);
|
|
397
436
|
margin: var(--input-margin, 0);
|
|
398
437
|
appearance: none !important;
|
|
@@ -13,10 +13,28 @@ export type OptionalInputProperties = {
|
|
|
13
13
|
infoMessage?: string | null;
|
|
14
14
|
validators?: CustomValidator[];
|
|
15
15
|
disable?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Renders the field read-only: the value can be focused, selected and copied but not
|
|
18
|
+
* edited. Deliberately distinct from `disable`, which also removes the element from
|
|
19
|
+
* the focus order and so cannot serve a select-all-to-copy affordance.
|
|
20
|
+
*/
|
|
21
|
+
readonly?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Native `spellcheck`. Defaults to `null`, which Svelte renders as "attribute
|
|
24
|
+
* absent", so the browser default is unchanged for every existing consumer.
|
|
25
|
+
* Pass `false` for fields holding code, JSON or identifiers, where red
|
|
26
|
+
* squiggles are noise.
|
|
27
|
+
*/
|
|
28
|
+
spellcheck?: boolean | null;
|
|
16
29
|
validationPattern?: RegExp | null;
|
|
17
30
|
inProgressPattern?: RegExp | null;
|
|
18
31
|
addFocusColor?: boolean;
|
|
19
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Native `maxlength`. Defaults to 1000. Pass `null` for no limit — a composer or
|
|
34
|
+
* paste target that silently truncates long input is worse than an unbounded one,
|
|
35
|
+
* and the attribute is rendered unconditionally otherwise.
|
|
36
|
+
*/
|
|
37
|
+
maxLength?: number | null;
|
|
20
38
|
minLength?: number;
|
|
21
39
|
min?: number;
|
|
22
40
|
max?: number;
|
package/dist/Menu/Menu.svelte
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
open = $bindable(false),
|
|
11
11
|
testId,
|
|
12
12
|
trigger,
|
|
13
|
+
interactiveTrigger = false,
|
|
13
14
|
onselect,
|
|
14
15
|
onopen,
|
|
15
16
|
onclose,
|
|
@@ -199,9 +200,28 @@
|
|
|
199
200
|
focusedIndex = -1;
|
|
200
201
|
typeaheadQuery = '';
|
|
201
202
|
onclose?.();
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
focusTrigger();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns focus to whatever is actually focusable for this trigger. Under
|
|
208
|
+
* `interactiveTrigger` the wrapper carries no tabindex, so focusing it is a no-op and
|
|
209
|
+
* focus falls to <body> — the control the snippet rendered is the real target.
|
|
210
|
+
*/
|
|
211
|
+
function focusTrigger() {
|
|
212
|
+
if (triggerEl === null) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (interactiveTrigger) {
|
|
216
|
+
const control = triggerEl.querySelector(
|
|
217
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
218
|
+
);
|
|
219
|
+
if (control instanceof HTMLElement) {
|
|
220
|
+
control.focus({ preventScroll: true });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
204
223
|
}
|
|
224
|
+
triggerEl.focus({ preventScroll: true });
|
|
205
225
|
}
|
|
206
226
|
|
|
207
227
|
function selectItem(item: MenuItem) {
|
|
@@ -239,6 +259,23 @@
|
|
|
239
259
|
}
|
|
240
260
|
}
|
|
241
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Keydown wiring handed to an `interactiveTrigger` snippet. Enter and Space are
|
|
264
|
+
* deliberately NOT handled here: the snippet owns a real `<button>`, which already
|
|
265
|
+
* synthesises a click from both, and that click is already wired to `toggle`. Handling
|
|
266
|
+
* them again would open a menu the click then immediately closes. Only the arrow keys —
|
|
267
|
+
* which no native button implements — are added.
|
|
268
|
+
*/
|
|
269
|
+
function handleInteractiveTriggerKeydown(event: KeyboardEvent) {
|
|
270
|
+
if (event.key === 'ArrowDown') {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
openMenu();
|
|
273
|
+
} else if (event.key === 'ArrowUp') {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
openMenu(selectableItems.length - 1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
242
279
|
function handleMenuKeydown(event: KeyboardEvent) {
|
|
243
280
|
switch (event.key) {
|
|
244
281
|
case 'ArrowDown': {
|
|
@@ -345,20 +382,45 @@
|
|
|
345
382
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
346
383
|
testID={typeof testId === 'string' ? testId : null}
|
|
347
384
|
>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
385
|
+
<!-- Two shapes on purpose. When the snippet renders its own interactive element this
|
|
386
|
+
wrapper must NOT be a second one: two focusable nodes for one conceptual trigger
|
|
387
|
+
means two Tab stops, both announcing as a button, and interactive content nested
|
|
388
|
+
inside interactive content. The wiring is handed to the snippet instead, for it to
|
|
389
|
+
spread onto the one real control. Written as two branches rather than conditional
|
|
390
|
+
attributes so role/tabindex stay statically paired — Svelte's a11y check reads them
|
|
391
|
+
together, and a dynamic pair trips a11y_no_noninteractive_tabindex. -->
|
|
392
|
+
{#if interactiveTrigger}
|
|
393
|
+
<div class="menu-trigger" bind:this={triggerEl}>
|
|
394
|
+
{#if typeof trigger === 'function'}
|
|
395
|
+
{@render trigger({
|
|
396
|
+
onclick: toggle,
|
|
397
|
+
onkeydown: handleInteractiveTriggerKeydown,
|
|
398
|
+
ariaHaspopup: 'menu',
|
|
399
|
+
ariaExpanded: open
|
|
400
|
+
})}
|
|
401
|
+
{/if}
|
|
402
|
+
</div>
|
|
403
|
+
{:else}
|
|
404
|
+
<div
|
|
405
|
+
class="menu-trigger"
|
|
406
|
+
bind:this={triggerEl}
|
|
407
|
+
onclick={toggle}
|
|
408
|
+
onkeydown={handleTriggerKeydown}
|
|
409
|
+
role="button"
|
|
410
|
+
tabindex="0"
|
|
411
|
+
aria-haspopup="menu"
|
|
412
|
+
aria-expanded={open}
|
|
413
|
+
>
|
|
414
|
+
{#if typeof trigger === 'function'}
|
|
415
|
+
{@render trigger({
|
|
416
|
+
onclick: toggle,
|
|
417
|
+
onkeydown: handleTriggerKeydown,
|
|
418
|
+
ariaHaspopup: 'menu',
|
|
419
|
+
ariaExpanded: open
|
|
420
|
+
})}
|
|
421
|
+
{/if}
|
|
422
|
+
</div>
|
|
423
|
+
{/if}
|
|
362
424
|
|
|
363
425
|
{#if open}
|
|
364
426
|
<div
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
/** The interaction wiring Menu hands to its `trigger` snippet. */
|
|
3
|
+
export type MenuTriggerProps = {
|
|
4
|
+
onclick: (event: MouseEvent) => void;
|
|
5
|
+
onkeydown: (event: KeyboardEvent) => void;
|
|
6
|
+
/** camelCase to match the library's own prop convention, so it spreads onto Button. */
|
|
7
|
+
ariaHaspopup: 'menu';
|
|
8
|
+
ariaExpanded: boolean;
|
|
9
|
+
};
|
|
2
10
|
export type MenuItem = {
|
|
3
11
|
label: string;
|
|
4
12
|
value: string;
|
|
@@ -23,7 +31,26 @@ export type MandatoryMenuProperties = {
|
|
|
23
31
|
export type OptionalMenuProperties = {
|
|
24
32
|
open?: boolean;
|
|
25
33
|
testId?: string;
|
|
26
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Renders the control that opens the menu. Receives Menu's interaction wiring, so a
|
|
36
|
+
* consumer whose trigger is ITSELF an interactive element (a Button, say) can spread
|
|
37
|
+
* it onto that element and set `interactiveTrigger` — see below. A snippet that
|
|
38
|
+
* declares no parameters simply ignores what it is handed, so existing triggers are
|
|
39
|
+
* unaffected.
|
|
40
|
+
*/
|
|
41
|
+
trigger?: Snippet<[MenuTriggerProps]>;
|
|
42
|
+
/**
|
|
43
|
+
* Set when the trigger snippet renders its own interactive element. Menu then stops
|
|
44
|
+
* making its wrapper a second one.
|
|
45
|
+
*
|
|
46
|
+
* By default Menu wraps the trigger in a `role="button" tabindex="0"` div carrying
|
|
47
|
+
* the click/keydown handlers. That is correct for inert trigger content, but if the
|
|
48
|
+
* snippet renders a real control the result is two focusable elements for one
|
|
49
|
+
* conceptual trigger — two Tab stops, both announcing as a button, and interactive
|
|
50
|
+
* content nested inside interactive content. Defaults to `false`, preserving the
|
|
51
|
+
* existing behaviour for every current consumer.
|
|
52
|
+
*/
|
|
53
|
+
interactiveTrigger?: boolean;
|
|
27
54
|
classes?: string;
|
|
28
55
|
/** Value of the currently selected item. When set, opening the menu focuses the
|
|
29
56
|
* selected option instead of the first item, the matching item gets the
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { FlyParams } from 'svelte/transition';
|
|
2
2
|
/**
|
|
3
3
|
* @name InputDataType
|
|
4
|
-
* @description Different types of input data which can be passed to the Input Component
|
|
4
|
+
* @description Different types of input data which can be passed to the Input Component.
|
|
5
|
+
*
|
|
6
|
+
* Input renders `<input type={dataType}>`, so this union is the only thing deciding
|
|
7
|
+
* which native types a consumer may ask for. The four added beyond the original
|
|
8
|
+
* text/tel/password/email/number all keep `value` a plain string with no parallel
|
|
9
|
+
* `checked`/`files` model, so Input's existing value plumbing covers them unchanged.
|
|
10
|
+
* `validateInput()` switches on dataType with no default branch, and `number` has
|
|
11
|
+
* always fallen through it unvalidated — these fall through identically.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT included: checkbox and radio (driven by `checked`, which Input has
|
|
14
|
+
* no prop for), and file (rejects scripted `value` writes outright). Those need new
|
|
15
|
+
* props, not a wider union.
|
|
5
16
|
*/
|
|
6
|
-
export type InputDataType = 'text' | 'tel' | 'password' | 'email' | 'number';
|
|
17
|
+
export type InputDataType = 'text' | 'tel' | 'password' | 'email' | 'number' | 'time' | 'date' | 'search' | 'url';
|
|
7
18
|
export type ModalTransition = 'IN' | 'ALL';
|
|
8
19
|
/**
|
|
9
20
|
* @name CustomValidator
|