@juspay/svelte-ui-components 2.93.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.
|
@@ -305,19 +305,41 @@
|
|
|
305
305
|
{:else if column.type === 'button'}
|
|
306
306
|
{@const buttonData = asButtonCellData(value)}
|
|
307
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}
|
|
308
315
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
309
316
|
<span
|
|
310
317
|
class="builtin-interactive"
|
|
318
|
+
class:builtin-icon-button={buttonData.iconUrl && !buttonData.text}
|
|
311
319
|
onclick={stopClickPropagation}
|
|
312
320
|
onkeydown={stopKeydownPropagation}
|
|
313
321
|
>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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}
|
|
321
343
|
</span>
|
|
322
344
|
{:else}
|
|
323
345
|
{cellValueToText(value)}
|
|
@@ -333,13 +355,30 @@
|
|
|
333
355
|
>
|
|
334
356
|
{#if actionData.primaryButton}
|
|
335
357
|
{@const primary = actionData.primaryButton}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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}
|
|
343
382
|
{/if}
|
|
344
383
|
{#if actionData.menuItems && actionData.menuItems.length > 0}
|
|
345
384
|
<Menu
|
|
@@ -600,6 +639,12 @@
|
|
|
600
639
|
height: var(--table-cell-input-icon-size, 16px);
|
|
601
640
|
}
|
|
602
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
|
+
|
|
603
648
|
.builtin-action-group {
|
|
604
649
|
gap: var(--table-cell-inline-gap, 8px);
|
|
605
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') {
|
|
@@ -160,8 +186,9 @@ export const asInputCellData = (value) => {
|
|
|
160
186
|
if (typeof record.ariaLabel === 'string') {
|
|
161
187
|
inputData.ariaLabel = record.ariaLabel;
|
|
162
188
|
}
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
const inputIconUrl = asSafeIconUrl(record.iconUrl ?? null);
|
|
190
|
+
if (inputIconUrl !== null) {
|
|
191
|
+
inputData.iconUrl = inputIconUrl;
|
|
165
192
|
}
|
|
166
193
|
if (typeof record.dataType === 'string') {
|
|
167
194
|
inputData.dataType = record.dataType;
|
|
@@ -192,20 +219,38 @@ export const asInputDataType = (value) => {
|
|
|
192
219
|
};
|
|
193
220
|
export const asButtonCellData = (value) => {
|
|
194
221
|
const record = asJsonObject(value);
|
|
195
|
-
if (record === null
|
|
222
|
+
if (record === null) {
|
|
196
223
|
return null;
|
|
197
224
|
}
|
|
198
|
-
const
|
|
225
|
+
const buttonIconUrl = asSafeIconUrl(record.iconUrl ?? null);
|
|
226
|
+
const buttonAriaLabel = typeof record.ariaLabel === 'string' ? record.ariaLabel : null;
|
|
227
|
+
const common = {};
|
|
199
228
|
if (typeof record.disabled === 'boolean') {
|
|
200
|
-
|
|
229
|
+
common.disabled = record.disabled;
|
|
201
230
|
}
|
|
202
231
|
if (typeof record.classes === 'string') {
|
|
203
|
-
|
|
232
|
+
common.classes = record.classes;
|
|
204
233
|
}
|
|
205
234
|
if (typeof record.testId === 'string') {
|
|
206
|
-
|
|
235
|
+
common.testId = record.testId;
|
|
236
|
+
}
|
|
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;
|
|
207
246
|
}
|
|
208
|
-
|
|
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;
|
|
209
254
|
};
|
|
210
255
|
const asMenuItemsData = (rawItems) => {
|
|
211
256
|
if (!Array.isArray(rawItems)) {
|
|
@@ -116,6 +116,8 @@ export type TableInputCellData = {
|
|
|
116
116
|
/**
|
|
117
117
|
* URL of a passive leading icon rendered inside the input field (e.g. a
|
|
118
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.
|
|
119
121
|
*/
|
|
120
122
|
iconUrl?: string;
|
|
121
123
|
/** Input dataType forwarded to the rendered Input ('text' | 'tel' | 'number' | …). */
|
|
@@ -128,14 +130,52 @@ export type TableInputCellData = {
|
|
|
128
130
|
/** Inline error message shown when the validation pattern rejects the value. */
|
|
129
131
|
onErrorMessage?: string;
|
|
130
132
|
};
|
|
131
|
-
/**
|
|
132
|
-
export type
|
|
133
|
-
text: string;
|
|
133
|
+
/** Options shared by both button cell variants. */
|
|
134
|
+
export type TableButtonCellCommonData = {
|
|
134
135
|
disabled?: boolean;
|
|
135
136
|
/** CSS classes forwarded to the Button (the consumer owns variant mapping). */
|
|
136
137
|
classes?: string;
|
|
137
138
|
testId?: string;
|
|
138
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;
|
|
139
179
|
/** One overflow-menu entry for action-group / popup-menu cells (JSON-safe). */
|
|
140
180
|
export type TableMenuItemData = {
|
|
141
181
|
id: string;
|