@juspay/svelte-ui-components 2.80.6 → 2.80.7
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.
|
@@ -213,6 +213,11 @@
|
|
|
213
213
|
gap: var(--button-content-gap, 16px);
|
|
214
214
|
visibility: var(--button-visibility, visible);
|
|
215
215
|
box-shadow: var(--button-box-shadow, none);
|
|
216
|
+
|
|
217
|
+
/* A button label should never soft-wrap onto a second line as the label text
|
|
218
|
+
changes (e.g. "Select" -> "Select (1)") — that jitters the layout around it.
|
|
219
|
+
Consumers that genuinely want multi-line buttons opt back in via the var. */
|
|
220
|
+
white-space: var(--button-white-space, nowrap);
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
.button-el.disabled,
|
package/dist/Menu/Menu.svelte
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
onopen,
|
|
14
14
|
onclose,
|
|
15
15
|
classes,
|
|
16
|
+
selectedValue = null,
|
|
16
17
|
role: menuRole = 'menu',
|
|
17
18
|
ariaLabel: menuAriaLabel,
|
|
18
19
|
id: menuId
|
|
@@ -43,12 +44,20 @@
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function openMenu(startIndex: number =
|
|
47
|
+
function openMenu(startIndex: number | null = null) {
|
|
47
48
|
open = true;
|
|
48
|
-
|
|
49
|
+
// With a known selection, opening focuses the selected option (listbox
|
|
50
|
+
// convention) instead of always parking the focus highlight on item 0.
|
|
51
|
+
const selectedItem =
|
|
52
|
+
selectedValue != null
|
|
53
|
+
? (items.find((item) => item.value === selectedValue && item.disabled !== true) ?? null)
|
|
54
|
+
: null;
|
|
55
|
+
const initialIndex =
|
|
56
|
+
startIndex ?? (selectedItem !== null ? getSelectableIndex(selectedItem) : 0);
|
|
57
|
+
focusedIndex = initialIndex;
|
|
49
58
|
onopen?.();
|
|
50
59
|
tick().then(() => {
|
|
51
|
-
focusItem(
|
|
60
|
+
focusItem(initialIndex);
|
|
52
61
|
});
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -232,12 +241,17 @@
|
|
|
232
241
|
class="menu-item"
|
|
233
242
|
class:menu-item-danger={item.danger === true}
|
|
234
243
|
class:menu-item-disabled={item.disabled === true}
|
|
244
|
+
class:menu-item-selected={selectedValue != null && item.value === selectedValue}
|
|
235
245
|
role={itemRole}
|
|
236
246
|
id={item.id}
|
|
237
247
|
tabindex={item.disabled === true || menuRole === 'listbox' ? -1 : 0}
|
|
238
248
|
aria-disabled={item.disabled === true ? 'true' : null}
|
|
239
|
-
aria-selected={menuRole === 'listbox'
|
|
240
|
-
?
|
|
249
|
+
aria-selected={menuRole === 'listbox'
|
|
250
|
+
? selectedValue != null
|
|
251
|
+
? item.value === selectedValue
|
|
252
|
+
: focusedIndex === getSelectableIndex(item)
|
|
253
|
+
? true
|
|
254
|
+
: null
|
|
241
255
|
: null}
|
|
242
256
|
onclick={() => selectItem(item)}
|
|
243
257
|
onfocus={() => {
|
|
@@ -318,6 +332,13 @@
|
|
|
318
332
|
outline: var(--menu-item-focus-outline, none);
|
|
319
333
|
}
|
|
320
334
|
|
|
335
|
+
/* True selection (selectedValue match) — distinct from transient focus so the
|
|
336
|
+
highlight follows the chosen option, not wherever keyboard focus landed. */
|
|
337
|
+
.menu-item-selected {
|
|
338
|
+
background-color: var(--menu-item-selected-background-color, transparent);
|
|
339
|
+
color: var(--menu-item-selected-color, inherit);
|
|
340
|
+
}
|
|
341
|
+
|
|
321
342
|
.menu-item-danger {
|
|
322
343
|
color: var(--menu-item-danger-color, #dc3545);
|
|
323
344
|
}
|
|
@@ -17,6 +17,11 @@ export type OptionalMenuProperties = {
|
|
|
17
17
|
testId?: string;
|
|
18
18
|
trigger?: Snippet;
|
|
19
19
|
classes?: string;
|
|
20
|
+
/** Value of the currently selected item. When set, opening the menu focuses the
|
|
21
|
+
* selected option instead of the first item, the matching item gets the
|
|
22
|
+
* `menu-item-selected` class (stylable via --menu-item-selected-*), and
|
|
23
|
+
* listbox aria-selected reflects the real selection. */
|
|
24
|
+
selectedValue?: string | null;
|
|
20
25
|
role?: 'menu' | 'listbox';
|
|
21
26
|
ariaLabel?: string;
|
|
22
27
|
id?: string;
|
|
@@ -52,11 +52,39 @@
|
|
|
52
52
|
let format = $derived(valueFormat ?? formatNumber);
|
|
53
53
|
let isEmpty = $derived(nodes.length === 0);
|
|
54
54
|
const MARGIN = 40;
|
|
55
|
+
const LABEL_CHAR_PX = 7.2; // ≈ 0.6em at the 12px default label size
|
|
56
|
+
|
|
57
|
+
// Final-column labels render to the RIGHT of their node; the bare 40px margin is
|
|
58
|
+
// nowhere near enough for real funnel labels ("PARTIALLY_FAILED (1,234)"), so they
|
|
59
|
+
// used to run past the svg edge and clip. Reserve a capped gutter sized from the
|
|
60
|
+
// sink-node labels (sinks are what land in the final column) and lay the diagram
|
|
61
|
+
// out in the remaining width instead.
|
|
62
|
+
let lastColumnLabelGutter = $derived.by(() => {
|
|
63
|
+
if (!showLabels || nodes.length === 0 || chartWidth <= 0) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
const sourceIds = new Set(links.map((link) => link.source));
|
|
67
|
+
const sinkLabels = nodes
|
|
68
|
+
.filter((node) => !sourceIds.has(node.id))
|
|
69
|
+
.map((node) => node.label ?? node.id);
|
|
70
|
+
if (sinkLabels.length === 0) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const longestChars =
|
|
74
|
+
Math.max(...sinkLabels.map((label) => label.length)) + (showValues ? 9 : 0);
|
|
75
|
+
const wanted = longestChars * LABEL_CHAR_PX + 10 + dataLabelOffsetX;
|
|
76
|
+
// Cap the reservation so labels can never squeeze the diagram below 3/4 width,
|
|
77
|
+
// and floor at 0 — a negative dataLabelOffsetX must not inflate the plot
|
|
78
|
+
// past the right margin.
|
|
79
|
+
return Math.max(0, Math.min(chartWidth * 0.25, wanted));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let plotWidth = $derived(Math.max(0, chartWidth - MARGIN * 2 - lastColumnLabelGutter));
|
|
55
83
|
let layout = $derived(
|
|
56
84
|
computeSankeyLayout(
|
|
57
85
|
nodes,
|
|
58
86
|
links,
|
|
59
|
-
|
|
87
|
+
plotWidth,
|
|
60
88
|
Math.max(0, chartHeight - MARGIN * 2),
|
|
61
89
|
nodeWidth,
|
|
62
90
|
nodePadding,
|
|
@@ -87,21 +115,31 @@
|
|
|
87
115
|
layout.nodes.length > 0 ? Math.max(...layout.nodes.map((n) => n.column)) + 1 : 0
|
|
88
116
|
);
|
|
89
117
|
let colWidth = $derived(
|
|
90
|
-
columnCount <= 1
|
|
91
|
-
? Math.max(0, chartWidth - MARGIN * 2)
|
|
92
|
-
: (Math.max(0, chartWidth - MARGIN * 2) - nodeWidth) / (columnCount - 1)
|
|
118
|
+
columnCount <= 1 ? plotWidth : (plotWidth - nodeWidth) / (columnCount - 1)
|
|
93
119
|
);
|
|
94
120
|
|
|
95
121
|
// Node labels render alongside their bar; long labels used to overflow into the
|
|
96
122
|
// next column and collide. Clip each to the horizontal room its column actually
|
|
97
123
|
// has and append an ellipsis (the full text stays available via the node tooltip).
|
|
98
|
-
|
|
124
|
+
// The final column's room is the reserved right gutter (plus the margin), NOT
|
|
125
|
+
// colWidth — budgeting it at colWidth is what used to let edge labels clip.
|
|
126
|
+
// Column headers are centred over columns that narrow as the label gutter and
|
|
127
|
+
// column count grow; untruncated they collide into one unreadable run. Clip to
|
|
128
|
+
// the column pitch with an ellipsis — the full text stays on the <title>.
|
|
129
|
+
const truncateColumnLabel = (text: string): string => {
|
|
130
|
+
const maxChars = Math.floor(Math.max(0, colWidth - 6) / LABEL_CHAR_PX);
|
|
131
|
+
if (maxChars < 3) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
return text.length > maxChars ? text.slice(0, maxChars - 1) + '…' : text;
|
|
135
|
+
};
|
|
136
|
+
|
|
99
137
|
const truncateLabel = (text: string, column: number): string => {
|
|
100
138
|
const available =
|
|
101
139
|
column === 0
|
|
102
140
|
? MARGIN + 16
|
|
103
141
|
: column === columnCount - 1
|
|
104
|
-
? Math.max(
|
|
142
|
+
? Math.max(0, lastColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
|
|
105
143
|
: Math.max(0, colWidth - nodeWidth - 12);
|
|
106
144
|
const maxChars = Math.floor(available / LABEL_CHAR_PX);
|
|
107
145
|
// No usable room — hide the label rather than force text that would overflow;
|
|
@@ -308,7 +346,7 @@
|
|
|
308
346
|
x={ci * colWidth + nodeWidth / 2}
|
|
309
347
|
y={-8}
|
|
310
348
|
text-anchor="middle"
|
|
311
|
-
dominant-baseline="auto">{label}</text
|
|
349
|
+
dominant-baseline="auto">{truncateColumnLabel(label)}<title>{label}</title></text
|
|
312
350
|
>
|
|
313
351
|
{/each}
|
|
314
352
|
{/if}
|