@juspay/svelte-ui-components 2.92.0 → 2.94.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.
@@ -37,6 +37,7 @@
37
37
  onKeyDown = () => {},
38
38
  classes,
39
39
  role,
40
+ ariaLabel,
40
41
  ariaExpanded,
41
42
  ariaAutocomplete,
42
43
  ariaControls,
@@ -256,6 +257,7 @@
256
257
  <textarea
257
258
  {value}
258
259
  {placeholder}
260
+ aria-label={ariaLabel}
259
261
  autocomplete={autoComplete}
260
262
  inputmode={inputMode}
261
263
  {name}
@@ -287,6 +289,7 @@
287
289
  type={dataType}
288
290
  {value}
289
291
  {placeholder}
292
+ aria-label={ariaLabel}
290
293
  autocomplete={autoComplete}
291
294
  inputmode={inputMode}
292
295
  {name}
@@ -52,6 +52,8 @@ export type OptionalInputProperties = {
52
52
  testId?: string;
53
53
  classes?: string;
54
54
  role?: string;
55
+ /** Accessible name applied to the native input/textarea (aria-label). */
56
+ ariaLabel?: string | null;
55
57
  ariaExpanded?: boolean;
56
58
  ariaAutocomplete?: 'none' | 'inline' | 'list' | 'both';
57
59
  ariaControls?: string | null;
@@ -274,6 +274,9 @@
274
274
  {:else if column.type === 'input'}
275
275
  {@const inputData = asInputCellData(value)}
276
276
  {#if inputData}
277
+ {#snippet inputLeadingIcon()}
278
+ <img class="builtin-input-icon" src={inputData.iconUrl} alt="" />
279
+ {/snippet}
277
280
  <!-- svelte-ignore a11y_no_static_element_interactions -->
278
281
  <span
279
282
  class="builtin-interactive"
@@ -283,6 +286,7 @@
283
286
  <Input
284
287
  value={inputData.value ?? ''}
285
288
  placeholder={inputData.placeholder ?? ''}
289
+ ariaLabel={inputData.ariaLabel ?? null}
286
290
  disable={inputData.disabled ?? false}
287
291
  testId={inputData.testId ?? ''}
288
292
  dataType={asInputDataType(inputData.dataType ?? null)}
@@ -292,6 +296,7 @@
292
296
  onErrorMessage={inputData.onErrorMessage ?? null}
293
297
  actionInput={false}
294
298
  onInput={(newValue) => column.onInput?.(rowIndex, newValue, originalIndex)}
299
+ {...inputData.iconUrl ? { leftIcon: inputLeadingIcon } : {}}
295
300
  />
296
301
  </span>
297
302
  {:else}
@@ -300,19 +305,41 @@
300
305
  {:else if column.type === 'button'}
301
306
  {@const buttonData = asButtonCellData(value)}
302
307
  {#if buttonData}
308
+ <!-- The icon <img> is always decorative (alt=""): an icon-only button is
309
+ named by the required ariaLabel on the Button, and a text-bearing
310
+ button is named by its visible text — the image never carries the
311
+ accessible name itself. -->
312
+ {#snippet buttonCellIcon()}
313
+ <img class="builtin-button-icon" src={buttonData.iconUrl} alt="" />
314
+ {/snippet}
303
315
  <!-- svelte-ignore a11y_no_static_element_interactions -->
304
316
  <span
305
317
  class="builtin-interactive"
318
+ class:builtin-icon-button={buttonData.iconUrl && !buttonData.text}
306
319
  onclick={stopClickPropagation}
307
320
  onkeydown={stopKeydownPropagation}
308
321
  >
309
- <Button
310
- text={buttonData.text}
311
- disabled={buttonData.disabled ?? false}
312
- classes={buttonData.classes ?? ''}
313
- testId={buttonData.testId}
314
- onclick={() => column.onButtonClick?.(rowIndex, originalIndex)}
315
- />
322
+ {#if buttonData.iconUrl}
323
+ <Button
324
+ text={buttonData.text}
325
+ icon={buttonCellIcon}
326
+ iconOnly={!buttonData.text}
327
+ ariaLabel={buttonData.ariaLabel}
328
+ disabled={buttonData.disabled ?? false}
329
+ classes={buttonData.classes ?? ''}
330
+ testId={buttonData.testId}
331
+ onclick={() => column.onButtonClick?.(rowIndex, originalIndex)}
332
+ />
333
+ {:else}
334
+ <Button
335
+ text={buttonData.text}
336
+ ariaLabel={buttonData.ariaLabel}
337
+ disabled={buttonData.disabled ?? false}
338
+ classes={buttonData.classes ?? ''}
339
+ testId={buttonData.testId}
340
+ onclick={() => column.onButtonClick?.(rowIndex, originalIndex)}
341
+ />
342
+ {/if}
316
343
  </span>
317
344
  {:else}
318
345
  {cellValueToText(value)}
@@ -328,13 +355,30 @@
328
355
  >
329
356
  {#if actionData.primaryButton}
330
357
  {@const primary = actionData.primaryButton}
331
- <Button
332
- text={primary.text}
333
- disabled={primary.disabled ?? false}
334
- classes={primary.classes ?? ''}
335
- testId={primary.testId}
336
- onclick={() => column.onPrimaryAction?.(rowIndex, originalIndex)}
337
- />
358
+ {#snippet primaryButtonIcon()}
359
+ <img class="builtin-button-icon" src={primary.iconUrl} alt="" />
360
+ {/snippet}
361
+ {#if primary.iconUrl}
362
+ <Button
363
+ text={primary.text}
364
+ icon={primaryButtonIcon}
365
+ iconOnly={!primary.text}
366
+ ariaLabel={primary.ariaLabel}
367
+ disabled={primary.disabled ?? false}
368
+ classes={primary.classes ?? ''}
369
+ testId={primary.testId}
370
+ onclick={() => column.onPrimaryAction?.(rowIndex, originalIndex)}
371
+ />
372
+ {:else}
373
+ <Button
374
+ text={primary.text}
375
+ ariaLabel={primary.ariaLabel}
376
+ disabled={primary.disabled ?? false}
377
+ classes={primary.classes ?? ''}
378
+ testId={primary.testId}
379
+ onclick={() => column.onPrimaryAction?.(rowIndex, originalIndex)}
380
+ />
381
+ {/if}
338
382
  {/if}
339
383
  {#if actionData.menuItems && actionData.menuItems.length > 0}
340
384
  <Menu
@@ -589,6 +633,18 @@
589
633
  width: var(--table-interactive-width, auto);
590
634
  }
591
635
 
636
+ .builtin-input-icon {
637
+ display: block;
638
+ width: var(--table-cell-input-icon-size, 16px);
639
+ height: var(--table-cell-input-icon-size, 16px);
640
+ }
641
+
642
+ .builtin-button-icon {
643
+ display: block;
644
+ width: var(--table-cell-icon-size, 16px);
645
+ height: var(--table-cell-icon-size, 16px);
646
+ }
647
+
592
648
  .builtin-action-group {
593
649
  gap: var(--table-cell-inline-gap, 8px);
594
650
  }
@@ -12,6 +12,13 @@ import type { TableActionGroupCellData, TableAvatarStackCellData, TableButtonCel
12
12
  export declare const asJsonObject: (value: TableCellValue) => {
13
13
  [key: string]: JSONValue;
14
14
  } | null;
15
+ /**
16
+ * Accepts an icon URL only when its scheme is safe to place in an `<img src>`
17
+ * — http(s), a `data:image/*` payload, or a scheme-less (relative) path. Cell
18
+ * data is consumer-supplied JSON, so `javascript:`/`vbscript:`/non-image
19
+ * `data:` URIs must never reach the DOM.
20
+ */
21
+ export declare const asSafeIconUrl: (value: TableCellValue) => string | null;
15
22
  export declare const asTagCellData: (value: TableCellValue) => TableTagCellData | null;
16
23
  export declare const asTagArrayItems: (value: TableCellValue) => TableTagArrayCellItem[] | null;
17
24
  export declare const asAvatarStackData: (value: TableCellValue) => TableAvatarStackCellData | null;
@@ -11,6 +11,32 @@ export const asJsonObject = (value) => {
11
11
  }
12
12
  return null;
13
13
  };
14
+ /**
15
+ * Accepts an icon URL only when its scheme is safe to place in an `<img src>`
16
+ * — http(s), a `data:image/*` payload, or a scheme-less (relative) path. Cell
17
+ * data is consumer-supplied JSON, so `javascript:`/`vbscript:`/non-image
18
+ * `data:` URIs must never reach the DOM.
19
+ */
20
+ export const asSafeIconUrl = (value) => {
21
+ if (typeof value !== 'string' || value.trim().length === 0) {
22
+ return null;
23
+ }
24
+ const candidate = value.trim();
25
+ const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(candidate);
26
+ if (schemeMatch === null) {
27
+ return candidate;
28
+ }
29
+ const scheme = schemeMatch[1].toLowerCase();
30
+ if (scheme === 'http' || scheme === 'https') {
31
+ return candidate;
32
+ }
33
+ // Image data URIs only, from an explicit subtype allowlist — a data: URI
34
+ // continues with ';' (parameters like base64) or ',' (payload).
35
+ if (scheme === 'data' && /^data:image\/(svg\+xml|png|jpeg|jpg|gif|webp)[;,]/i.test(candidate)) {
36
+ return candidate;
37
+ }
38
+ return null;
39
+ };
14
40
  export const asTagCellData = (value) => {
15
41
  const record = asJsonObject(value);
16
42
  if (record === null || typeof record.text !== 'string') {
@@ -157,6 +183,13 @@ export const asInputCellData = (value) => {
157
183
  if (typeof record.testId === 'string') {
158
184
  inputData.testId = record.testId;
159
185
  }
186
+ if (typeof record.ariaLabel === 'string') {
187
+ inputData.ariaLabel = record.ariaLabel;
188
+ }
189
+ const inputIconUrl = asSafeIconUrl(record.iconUrl ?? null);
190
+ if (inputIconUrl !== null) {
191
+ inputData.iconUrl = inputIconUrl;
192
+ }
160
193
  if (typeof record.dataType === 'string') {
161
194
  inputData.dataType = record.dataType;
162
195
  }
@@ -186,20 +219,38 @@ export const asInputDataType = (value) => {
186
219
  };
187
220
  export const asButtonCellData = (value) => {
188
221
  const record = asJsonObject(value);
189
- if (record === null || typeof record.text !== 'string') {
222
+ if (record === null) {
190
223
  return null;
191
224
  }
192
- const buttonData = { text: record.text };
225
+ const buttonIconUrl = asSafeIconUrl(record.iconUrl ?? null);
226
+ const buttonAriaLabel = typeof record.ariaLabel === 'string' ? record.ariaLabel : null;
227
+ const common = {};
193
228
  if (typeof record.disabled === 'boolean') {
194
- buttonData.disabled = record.disabled;
229
+ common.disabled = record.disabled;
195
230
  }
196
231
  if (typeof record.classes === 'string') {
197
- buttonData.classes = record.classes;
232
+ common.classes = record.classes;
198
233
  }
199
234
  if (typeof record.testId === 'string') {
200
- buttonData.testId = record.testId;
235
+ common.testId = record.testId;
201
236
  }
202
- return buttonData;
237
+ if (typeof record.text === 'string') {
238
+ const textButton = { text: record.text, ...common };
239
+ if (buttonIconUrl !== null) {
240
+ textButton.iconUrl = buttonIconUrl;
241
+ }
242
+ if (buttonAriaLabel !== null) {
243
+ textButton.ariaLabel = buttonAriaLabel;
244
+ }
245
+ return textButton;
246
+ }
247
+ // Icon-only buttons require BOTH a safe icon and an accessible name — a
248
+ // button with no visible text and no aria-label is a WCAG 4.1.2 failure,
249
+ // so such data fails narrowing and the cell falls back to plain text.
250
+ if (buttonIconUrl !== null && buttonAriaLabel !== null) {
251
+ return { iconUrl: buttonIconUrl, ariaLabel: buttonAriaLabel, ...common };
252
+ }
253
+ return null;
203
254
  };
204
255
  const asMenuItemsData = (rawItems) => {
205
256
  if (!Array.isArray(rawItems)) {
@@ -107,6 +107,19 @@ export type TableInputCellData = {
107
107
  placeholder?: string;
108
108
  disabled?: boolean;
109
109
  testId?: string;
110
+ /**
111
+ * Accessible name forwarded to the rendered Input's native element
112
+ * (aria-label). Recommended whenever the cell has no visible label —
113
+ * required in practice for icon-bearing inputs.
114
+ */
115
+ ariaLabel?: string;
116
+ /**
117
+ * URL of a passive leading icon rendered inside the input field (e.g. a
118
+ * currency glyph). Sized via `--table-cell-input-icon-size` (default 16px).
119
+ * Scheme-validated at narrowing time: only http(s), `data:image/*`, and
120
+ * relative URLs are accepted; anything else is dropped.
121
+ */
122
+ iconUrl?: string;
110
123
  /** Input dataType forwarded to the rendered Input ('text' | 'tel' | 'number' | …). */
111
124
  dataType?: string;
112
125
  /**
@@ -117,14 +130,52 @@ export type TableInputCellData = {
117
130
  /** Inline error message shown when the validation pattern rejects the value. */
118
131
  onErrorMessage?: string;
119
132
  };
120
- /** Cell shape for `type: 'button'` the handler lives on the column. */
121
- export type TableButtonCellData = {
122
- text: string;
133
+ /** Options shared by both button cell variants. */
134
+ export type TableButtonCellCommonData = {
123
135
  disabled?: boolean;
124
136
  /** CSS classes forwarded to the Button (the consumer owns variant mapping). */
125
137
  classes?: string;
126
138
  testId?: string;
127
139
  };
140
+ /**
141
+ * Text-bearing button cell — `text` stays required exactly as before this
142
+ * type became a union, so existing consumers are unaffected. An optional
143
+ * `iconUrl` renders a leading icon; `ariaLabel` may override the accessible
144
+ * name (the visible text already names the button).
145
+ */
146
+ export type TableTextButtonCellData = TableButtonCellCommonData & {
147
+ text: string;
148
+ /**
149
+ * URL of a leading icon rendered next to the text. Sized via
150
+ * `--table-cell-icon-size` (default 16px). Scheme-validated at narrowing
151
+ * time: only http(s), `data:image/*`, and relative URLs are accepted;
152
+ * anything else is dropped.
153
+ */
154
+ iconUrl?: string;
155
+ ariaLabel?: string;
156
+ };
157
+ /**
158
+ * Icon-only button cell — renders as a bare ghost control. `ariaLabel` is
159
+ * required (the button has no visible text, so it MUST carry an accessible
160
+ * name); the decoder enforces the same rule at runtime for untyped data.
161
+ */
162
+ export type TableIconOnlyButtonCellData = TableButtonCellCommonData & {
163
+ text?: never;
164
+ /**
165
+ * URL of the button's icon. Sized via `--table-cell-icon-size` (default
166
+ * 16px). Scheme-validated at narrowing time: only http(s), `data:image/*`,
167
+ * and relative URLs are accepted; anything else is dropped (the cell then
168
+ * fails narrowing and falls back to plain text of the raw value).
169
+ */
170
+ iconUrl: string;
171
+ ariaLabel: string;
172
+ };
173
+ /**
174
+ * Cell shape for `type: 'button'` — the handler lives on the column. A
175
+ * discriminated union: text-bearing buttons keep `text` required (unchanged
176
+ * public contract), icon-only buttons require `iconUrl` + `ariaLabel`.
177
+ */
178
+ export type TableButtonCellData = TableTextButtonCellData | TableIconOnlyButtonCellData;
128
179
  /** One overflow-menu entry for action-group / popup-menu cells (JSON-safe). */
129
180
  export type TableMenuItemData = {
130
181
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.92.0",
3
+ "version": "2.94.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",