@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.
package/dist/Input/Input.svelte
CHANGED
|
@@ -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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
}
|
package/dist/Table/cellData.d.ts
CHANGED
|
@@ -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;
|
package/dist/Table/cellData.js
CHANGED
|
@@ -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
|
|
222
|
+
if (record === null) {
|
|
190
223
|
return null;
|
|
191
224
|
}
|
|
192
|
-
const
|
|
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
|
-
|
|
229
|
+
common.disabled = record.disabled;
|
|
195
230
|
}
|
|
196
231
|
if (typeof record.classes === 'string') {
|
|
197
|
-
|
|
232
|
+
common.classes = record.classes;
|
|
198
233
|
}
|
|
199
234
|
if (typeof record.testId === 'string') {
|
|
200
|
-
|
|
235
|
+
common.testId = record.testId;
|
|
201
236
|
}
|
|
202
|
-
|
|
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
|
-
/**
|
|
121
|
-
export type
|
|
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;
|