@juspay/svelte-ui-components 2.121.0 → 2.123.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.
@@ -22,6 +22,7 @@
22
22
  testId,
23
23
  ariaLabel,
24
24
  ariaExpanded,
25
+ ariaHaspopup,
25
26
  ariaSelected,
26
27
  role,
27
28
  onclick,
@@ -88,6 +89,7 @@
88
89
  role={role ?? null}
89
90
  aria-label={ariaLabel ?? null}
90
91
  aria-expanded={ariaExpanded ?? null}
92
+ aria-haspopup={ariaHaspopup ?? null}
91
93
  aria-selected={ariaSelected ?? null}
92
94
  aria-busy={isBusy || null}
93
95
  type={href ? null : type}
@@ -53,6 +53,11 @@ 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;
57
62
  role?: string;
58
63
  disabled?: boolean;
@@ -5,6 +5,8 @@
5
5
  children,
6
6
  title,
7
7
  description,
8
+ titleSnippet,
9
+ descriptionSnippet,
8
10
  classes,
9
11
  testId,
10
12
  onclick,
@@ -64,13 +66,17 @@
64
66
  onclick={onclick ?? null}
65
67
  onkeydown={!isAnchor && isInteractive ? handleKeydown : null}
66
68
  >
67
- {#if (typeof title === 'string' && title.length > 0) || typeof headerRight === 'function'}
69
+ {#if (typeof title === 'string' && title.length > 0) || typeof titleSnippet === 'function' || typeof headerRight === 'function'}
68
70
  <div class="card-header" class:card-header-split={typeof headerRight === 'function'}>
69
71
  <div class="card-header-main">
70
- {#if typeof title === 'string' && title.length > 0}
72
+ {#if typeof titleSnippet === 'function'}
73
+ <div class="card-title">{@render titleSnippet()}</div>
74
+ {:else if typeof title === 'string' && title.length > 0}
71
75
  <div class="card-title">{title}</div>
72
76
  {/if}
73
- {#if typeof description === 'string' && description.length > 0}
77
+ {#if typeof descriptionSnippet === 'function'}
78
+ <div class="card-description">{@render descriptionSnippet()}</div>
79
+ {:else if typeof description === 'string' && description.length > 0}
74
80
  <div class="card-description">{description}</div>
75
81
  {/if}
76
82
  </div>
@@ -8,6 +8,22 @@ export type OptionalCardProperties = {
8
8
  children?: Snippet;
9
9
  title?: string;
10
10
  description?: string;
11
+ /**
12
+ * Optional snippet rendered in place of the `title` string, inside the same
13
+ * `.card-title` container. When provided, `title` is not rendered — the snippet
14
+ * takes priority — and the header row is shown even if `title` is omitted
15
+ * entirely. Use this when the title needs markup a string prop cannot carry:
16
+ * rich text, an inline icon, or a test hook such as `data-pw` /
17
+ * `use:testAttributes` that must sit on the title element itself.
18
+ */
19
+ titleSnippet?: Snippet;
20
+ /**
21
+ * Optional snippet rendered in place of the `description` string, inside the
22
+ * same `.card-description` container. When provided, `description` is not
23
+ * rendered. Like `titleSnippet`, this exists for descriptions that need markup
24
+ * or a test hook on the element itself.
25
+ */
26
+ descriptionSnippet?: Snippet;
11
27
  classes?: string;
12
28
  /** Renders as `data-pw` on the root element for Playwright test selection. */
13
29
  testId?: string;
@@ -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,
@@ -189,6 +191,15 @@
189
191
  return;
190
192
  }
191
193
 
194
+ // Everything below the tel branch is tel-specific digit normalisation, and
195
+ // onPaste was only ever invoked from inside it — so a non-tel field (notably
196
+ // useTextArea) had no way to observe a paste at all. Hand the event over
197
+ // before that branch and return, leaving tel's behaviour byte-identical.
198
+ if (dataType !== 'tel') {
199
+ onPaste(event);
200
+ return;
201
+ }
202
+
192
203
  if (event.clipboardData) {
193
204
  if (dataType === 'tel') {
194
205
  let unfilteredNumber = event.clipboardData.getData('text');
@@ -281,6 +292,8 @@
281
292
  style:resize={effectiveResize}
282
293
  rows={rows ?? null}
283
294
  disabled={disable}
295
+ readonly={readonly || null}
296
+ {spellcheck}
284
297
  bind:this={inputElement}
285
298
  maxlength={dataType === 'tel' ? null : maxLength}
286
299
  minlength={minLength}
@@ -311,6 +324,8 @@
311
324
  testID={testId}
312
325
  class:action-input={actionInput}
313
326
  disabled={disable}
327
+ readonly={readonly || null}
328
+ {spellcheck}
314
329
  bind:this={inputElement}
315
330
  maxlength={dataType === 'tel' ? null : maxLength}
316
331
  minlength={minLength}
@@ -13,6 +13,19 @@ 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;
@@ -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,
@@ -345,20 +346,45 @@
345
346
  data-pw={typeof testId === 'string' ? testId : null}
346
347
  testID={typeof testId === 'string' ? testId : null}
347
348
  >
348
- <div
349
- class="menu-trigger"
350
- bind:this={triggerEl}
351
- onclick={toggle}
352
- onkeydown={handleTriggerKeydown}
353
- role="button"
354
- tabindex="0"
355
- aria-haspopup="menu"
356
- aria-expanded={open}
357
- >
358
- {#if typeof trigger === 'function'}
359
- {@render trigger()}
360
- {/if}
361
- </div>
349
+ <!-- Two shapes on purpose. When the snippet renders its own interactive element this
350
+ wrapper must NOT be a second one: two focusable nodes for one conceptual trigger
351
+ means two Tab stops, both announcing as a button, and interactive content nested
352
+ inside interactive content. The wiring is handed to the snippet instead, for it to
353
+ spread onto the one real control. Written as two branches rather than conditional
354
+ attributes so role/tabindex stay statically paired — Svelte's a11y check reads them
355
+ together, and a dynamic pair trips a11y_no_noninteractive_tabindex. -->
356
+ {#if interactiveTrigger}
357
+ <div class="menu-trigger" bind:this={triggerEl}>
358
+ {#if typeof trigger === 'function'}
359
+ {@render trigger({
360
+ onclick: toggle,
361
+ onkeydown: handleTriggerKeydown,
362
+ ariaHaspopup: 'menu',
363
+ ariaExpanded: open
364
+ })}
365
+ {/if}
366
+ </div>
367
+ {:else}
368
+ <div
369
+ class="menu-trigger"
370
+ bind:this={triggerEl}
371
+ onclick={toggle}
372
+ onkeydown={handleTriggerKeydown}
373
+ role="button"
374
+ tabindex="0"
375
+ aria-haspopup="menu"
376
+ aria-expanded={open}
377
+ >
378
+ {#if typeof trigger === 'function'}
379
+ {@render trigger({
380
+ onclick: toggle,
381
+ onkeydown: handleTriggerKeydown,
382
+ ariaHaspopup: 'menu',
383
+ ariaExpanded: open
384
+ })}
385
+ {/if}
386
+ </div>
387
+ {/if}
362
388
 
363
389
  {#if open}
364
390
  <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
- trigger?: Snippet;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.121.0",
3
+ "version": "2.123.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",