@juspay/svelte-ui-components 4.25.0 → 4.27.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/Select/Select.svelte +144 -6
- package/dist/Select/dropdownPosition.d.ts +10 -2
- package/dist/Select/dropdownPosition.js +29 -4
- package/dist/Select/properties.d.ts +21 -1
- package/dist/VoiceOrb/VoiceOrb.svelte +400 -0
- package/dist/VoiceOrb/VoiceOrb.svelte.d.ts +4 -0
- package/dist/VoiceOrb/orbMath.d.ts +131 -0
- package/dist/VoiceOrb/orbMath.js +267 -0
- package/dist/VoiceOrb/properties.d.ts +105 -0
- package/dist/VoiceOrb/properties.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist-wc/index.js +736 -345
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, tick } from 'svelte';
|
|
3
|
-
import type { SelectItem, SelectProperties } from './properties';
|
|
3
|
+
import type { SelectItem, SelectPlacement, SelectProperties } from './properties';
|
|
4
4
|
import Pill from '../Pill/Pill.svelte';
|
|
5
5
|
import Img from '../Img/Img.svelte';
|
|
6
6
|
import { computeSelectDropdownPosition } from './dropdownPosition';
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
classes,
|
|
34
34
|
open = $bindable(false),
|
|
35
35
|
dropdownAlign = 'left',
|
|
36
|
+
placement,
|
|
36
37
|
hierarchy = 'default',
|
|
37
38
|
leftIcon,
|
|
38
39
|
leftIconTestId,
|
|
@@ -62,6 +63,67 @@
|
|
|
62
63
|
// default. The in-flow panel still honours the CSS var via its margin-top.
|
|
63
64
|
const PORTAL_DROPDOWN_GAP = 4;
|
|
64
65
|
|
|
66
|
+
/** Viewport padding the `'auto'` placement keeps between the panel and the edges. */
|
|
67
|
+
const AUTO_PLACEMENT_VIEWPORT_MARGIN = 8;
|
|
68
|
+
|
|
69
|
+
/* An unset `placement` is the pre-5.0 contract, kept exactly: the in-flow panel
|
|
70
|
+
always opens downward and only the portaled one flips on fit. Setting it opts
|
|
71
|
+
both paths into one resolved corner, which is the whole point of the prop. */
|
|
72
|
+
let placementRequest: SelectPlacement = $derived(
|
|
73
|
+
placement ?? (dropdownAlign === 'right' ? 'bottom-right' : 'bottom-left')
|
|
74
|
+
);
|
|
75
|
+
let placementPinned: boolean = $derived(placement != null);
|
|
76
|
+
|
|
77
|
+
/** Corner an `'auto'` measuring pass settled on; null until it has run. */
|
|
78
|
+
let autoResolved: Exclude<SelectPlacement, 'auto'> | null = $state(null);
|
|
79
|
+
|
|
80
|
+
/* Derived rather than assigned when the dropdown opens: `open` is bindable and
|
|
81
|
+
documented as something a parent can drive, so the panel can be rendered
|
|
82
|
+
without `openDropdown` ever running. Assigning on the click path left an
|
|
83
|
+
explicitly placed Select rendering at the default corner whenever its parent
|
|
84
|
+
opened it. */
|
|
85
|
+
let resolvedPlacement: Exclude<SelectPlacement, 'auto'> = $derived(
|
|
86
|
+
placementRequest === 'auto' ? (autoResolved ?? 'bottom-left') : placementRequest
|
|
87
|
+
);
|
|
88
|
+
/** True while an `'auto'` open measures the hidden panel -- suppresses paint. */
|
|
89
|
+
let measuringPlacement: boolean = $derived(placementRequest === 'auto' && autoResolved === null);
|
|
90
|
+
|
|
91
|
+
/* `$derived.by` rather than `$derived`: a bare expression here is evaluated in
|
|
92
|
+
the declaration's control flow, where TypeScript has `resolvedPlacement`
|
|
93
|
+
still narrowed to its initialiser literal and calls the comparison
|
|
94
|
+
unreachable. The closure resets that narrowing. */
|
|
95
|
+
let placementAlign: 'left' | 'right' = $derived.by(() =>
|
|
96
|
+
resolvedPlacement === 'bottom-right' || resolvedPlacement === 'top-right' ? 'right' : 'left'
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolves `'auto'` against live geometry, mirroring Menu's pass: the panel is
|
|
101
|
+
* rendered hidden at the default corner first so it has real dimensions, then
|
|
102
|
+
* flips right/up only where the default overflows and the opposite side fits.
|
|
103
|
+
*/
|
|
104
|
+
function resolveAutoPlacement(panel: HTMLElement): Exclude<SelectPlacement, 'auto'> {
|
|
105
|
+
if (containerEl === null || typeof window === 'undefined') {
|
|
106
|
+
return 'bottom-left';
|
|
107
|
+
}
|
|
108
|
+
const containerRect = containerEl.getBoundingClientRect();
|
|
109
|
+
const panelRect = panel.getBoundingClientRect();
|
|
110
|
+
const viewportWidth = window.innerWidth;
|
|
111
|
+
const viewportHeight = window.innerHeight;
|
|
112
|
+
|
|
113
|
+
const overflowsRight =
|
|
114
|
+
containerRect.left + panelRect.width > viewportWidth - AUTO_PLACEMENT_VIEWPORT_MARGIN;
|
|
115
|
+
const fitsRightAnchored =
|
|
116
|
+
containerRect.right - panelRect.width >= AUTO_PLACEMENT_VIEWPORT_MARGIN;
|
|
117
|
+
const horizontal = overflowsRight && fitsRightAnchored ? 'right' : 'left';
|
|
118
|
+
|
|
119
|
+
const overflowsBottom =
|
|
120
|
+
containerRect.bottom + panelRect.height > viewportHeight - AUTO_PLACEMENT_VIEWPORT_MARGIN;
|
|
121
|
+
const fitsAbove = containerRect.top - panelRect.height >= AUTO_PLACEMENT_VIEWPORT_MARGIN;
|
|
122
|
+
const vertical = overflowsBottom && fitsAbove ? 'top' : 'bottom';
|
|
123
|
+
|
|
124
|
+
return `${vertical}-${horizontal}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
65
127
|
const instanceId = $props.id();
|
|
66
128
|
const listboxId = `select-listbox-${instanceId}`;
|
|
67
129
|
const errorMessageId = `${listboxId}-error`;
|
|
@@ -195,9 +257,9 @@
|
|
|
195
257
|
return { destroy: () => node.remove() };
|
|
196
258
|
};
|
|
197
259
|
|
|
198
|
-
let
|
|
260
|
+
let portalPlacement = $derived.by(() => {
|
|
199
261
|
if (!usePortal || !open || triggerEl === null) {
|
|
200
|
-
return
|
|
262
|
+
return null;
|
|
201
263
|
}
|
|
202
264
|
void portalTick;
|
|
203
265
|
const rect = triggerEl.getBoundingClientRect();
|
|
@@ -215,13 +277,62 @@
|
|
|
215
277
|
},
|
|
216
278
|
dropdown: { width: dropdownWidth, height: dropdownHeight },
|
|
217
279
|
viewport,
|
|
218
|
-
align:
|
|
280
|
+
align: placementAlign,
|
|
281
|
+
/* Unset `placement` keeps the fit-based flip this path always had; a pinned
|
|
282
|
+
corner is honoured on both axes instead. */
|
|
283
|
+
vertical: placementPinned
|
|
284
|
+
? resolvedPlacement === 'top-left' || resolvedPlacement === 'top-right'
|
|
285
|
+
? 'top'
|
|
286
|
+
: 'bottom'
|
|
287
|
+
: 'auto',
|
|
219
288
|
gap: PORTAL_DROPDOWN_GAP
|
|
220
289
|
});
|
|
290
|
+
return placement;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
let portalStyle: string = $derived.by(() => {
|
|
294
|
+
const placement = portalPlacement;
|
|
295
|
+
if (placement === null) {
|
|
296
|
+
return '';
|
|
297
|
+
}
|
|
221
298
|
const widthRule = placement.width === null ? '' : `width:${placement.width}px;`;
|
|
222
299
|
return `top:${placement.top}px;left:${placement.left}px;min-width:${placement.minWidth}px;${widthRule}`;
|
|
223
300
|
});
|
|
224
301
|
|
|
302
|
+
/* A portaled panel's side comes from the geometry, which still flips on fit
|
|
303
|
+
when `placement` is unset. Reporting the requested corner there would make
|
|
304
|
+
`data-placement` disagree with where the panel actually is -- the exact
|
|
305
|
+
thing the attribute exists to tell a consumer. */
|
|
306
|
+
let effectivePlacement: Exclude<SelectPlacement, 'auto'> = $derived.by(() => {
|
|
307
|
+
const placement = portalPlacement;
|
|
308
|
+
if (placement === null) {
|
|
309
|
+
return resolvedPlacement;
|
|
310
|
+
}
|
|
311
|
+
if (placement.flippedUp) {
|
|
312
|
+
return placementAlign === 'right' ? 'top-right' : 'top-left';
|
|
313
|
+
}
|
|
314
|
+
return placementAlign === 'right' ? 'bottom-right' : 'bottom-left';
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Svelte action on the panel itself: `openDropdown` is not the only way the
|
|
319
|
+
* panel appears, so the `'auto'` measuring pass is driven by the panel's own
|
|
320
|
+
* lifecycle rather than by the open handler. Resetting on destroy is what
|
|
321
|
+
* makes a reopen re-measure against the trigger's current position.
|
|
322
|
+
*/
|
|
323
|
+
const resolveAutoPlacementOnMount = (node: HTMLElement) => {
|
|
324
|
+
if (placementRequest === 'auto') {
|
|
325
|
+
void tick().then(() => {
|
|
326
|
+
autoResolved = resolveAutoPlacement(node);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
destroy: () => {
|
|
331
|
+
autoResolved = null;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
|
|
225
336
|
async function openDropdown(): Promise<void> {
|
|
226
337
|
if (disabled || open) {
|
|
227
338
|
return;
|
|
@@ -792,8 +903,13 @@
|
|
|
792
903
|
the inner element that actually holds the options. -->
|
|
793
904
|
<div
|
|
794
905
|
class="select-dropdown"
|
|
795
|
-
class:select-dropdown-right={
|
|
906
|
+
class:select-dropdown-right={placementAlign === 'right'}
|
|
907
|
+
class:select-dropdown-up={!usePortal &&
|
|
908
|
+
(effectivePlacement === 'top-left' || effectivePlacement === 'top-right')}
|
|
909
|
+
class:select-dropdown-measuring={measuringPlacement}
|
|
796
910
|
class:select-dropdown-portal={usePortal}
|
|
911
|
+
data-placement={effectivePlacement}
|
|
912
|
+
use:resolveAutoPlacementOnMount
|
|
797
913
|
bind:this={dropdownEl}
|
|
798
914
|
bind:clientWidth={dropdownWidth}
|
|
799
915
|
bind:clientHeight={dropdownHeight}
|
|
@@ -841,8 +957,13 @@
|
|
|
841
957
|
{:else}
|
|
842
958
|
<div
|
|
843
959
|
class="select-dropdown"
|
|
844
|
-
class:select-dropdown-right={
|
|
960
|
+
class:select-dropdown-right={placementAlign === 'right'}
|
|
961
|
+
class:select-dropdown-up={!usePortal &&
|
|
962
|
+
(effectivePlacement === 'top-left' || effectivePlacement === 'top-right')}
|
|
963
|
+
class:select-dropdown-measuring={measuringPlacement}
|
|
845
964
|
class:select-dropdown-portal={usePortal}
|
|
965
|
+
data-placement={effectivePlacement}
|
|
966
|
+
use:resolveAutoPlacementOnMount
|
|
846
967
|
bind:this={dropdownEl}
|
|
847
968
|
bind:clientWidth={dropdownWidth}
|
|
848
969
|
bind:clientHeight={dropdownHeight}
|
|
@@ -1045,6 +1166,23 @@
|
|
|
1045
1166
|
height: 100%;
|
|
1046
1167
|
}
|
|
1047
1168
|
|
|
1169
|
+
/* Anchors the in-flow panel above the trigger. `margin-top` is what the
|
|
1170
|
+
downward panel uses for the gap, so it has to be unset here or the panel
|
|
1171
|
+
sits a gap's width too high. */
|
|
1172
|
+
.select-dropdown.select-dropdown-up {
|
|
1173
|
+
top: auto;
|
|
1174
|
+
bottom: 100%;
|
|
1175
|
+
margin-top: 0;
|
|
1176
|
+
margin-bottom: var(--select-dropdown-gap, 4px);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/* One-tick measuring pass for placement="auto": the panel needs rendered
|
|
1180
|
+
dimensions before the corner is chosen, without a visible flash at the
|
|
1181
|
+
wrong corner. */
|
|
1182
|
+
.select-dropdown.select-dropdown-measuring {
|
|
1183
|
+
visibility: hidden;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1048
1186
|
.select-dropdown {
|
|
1049
1187
|
position: absolute;
|
|
1050
1188
|
top: 100%;
|
|
@@ -9,6 +9,12 @@ type Size = {
|
|
|
9
9
|
width: number;
|
|
10
10
|
height: number;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Which side of the trigger the panel opens on. `'auto'` picks the side that
|
|
14
|
+
* fits, which is what a portaled Select did before `placement` existed and so
|
|
15
|
+
* stays the default when the option is omitted.
|
|
16
|
+
*/
|
|
17
|
+
export type SelectVerticalPlacement = 'bottom' | 'top' | 'auto';
|
|
12
18
|
export type SelectDropdownPlacement = {
|
|
13
19
|
left: number;
|
|
14
20
|
top: number;
|
|
@@ -28,14 +34,16 @@ export type SelectDropdownPlacement = {
|
|
|
28
34
|
* coordinates for a `position: fixed` panel anchored to `trigger`. Left-aligned
|
|
29
35
|
* panels match the trigger width; right-aligned panels hang leftward from the
|
|
30
36
|
* trigger's right edge using their measured content width. The panel flips above
|
|
31
|
-
* the trigger only when it cannot fit below and there is more room above
|
|
32
|
-
* clamped horizontally to a
|
|
37
|
+
* the trigger only when it cannot fit below and there is more room above — or
|
|
38
|
+
* unconditionally when `vertical` pins a side — and is clamped horizontally to a
|
|
39
|
+
* viewport margin.
|
|
33
40
|
*/
|
|
34
41
|
export declare function computeSelectDropdownPosition(opts: {
|
|
35
42
|
trigger: TriggerRect;
|
|
36
43
|
dropdown: Size;
|
|
37
44
|
viewport: Size;
|
|
38
45
|
align: 'left' | 'right';
|
|
46
|
+
vertical?: SelectVerticalPlacement;
|
|
39
47
|
gap: number;
|
|
40
48
|
margin?: number;
|
|
41
49
|
}): SelectDropdownPlacement;
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* coordinates for a `position: fixed` panel anchored to `trigger`. Left-aligned
|
|
4
4
|
* panels match the trigger width; right-aligned panels hang leftward from the
|
|
5
5
|
* trigger's right edge using their measured content width. The panel flips above
|
|
6
|
-
* the trigger only when it cannot fit below and there is more room above
|
|
7
|
-
* clamped horizontally to a
|
|
6
|
+
* the trigger only when it cannot fit below and there is more room above — or
|
|
7
|
+
* unconditionally when `vertical` pins a side — and is clamped horizontally to a
|
|
8
|
+
* viewport margin.
|
|
8
9
|
*/
|
|
9
10
|
export function computeSelectDropdownPosition(opts) {
|
|
10
11
|
const margin = opts.margin ?? 8;
|
|
12
|
+
const vertical = opts.vertical ?? 'auto';
|
|
11
13
|
const { trigger, dropdown, viewport, align, gap } = opts;
|
|
12
14
|
const minWidth = trigger.width;
|
|
13
15
|
const width = align === 'left' ? trigger.width : null;
|
|
@@ -19,7 +21,30 @@ export function computeSelectDropdownPosition(opts) {
|
|
|
19
21
|
}
|
|
20
22
|
const spaceBelow = viewport.height - trigger.bottom;
|
|
21
23
|
const spaceAbove = trigger.top;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
/* A pinned side is honoured even where the panel will not fit: the caller
|
|
25
|
+
asked for that side, and silently overriding it is the behaviour
|
|
26
|
+
`placement` exists to escape. Only `'auto'` consults the geometry. */
|
|
27
|
+
const fitsBelow = !(dropdown.height > 0 && spaceBelow < dropdown.height + gap);
|
|
28
|
+
const flippedUp = vertical === 'auto' ? !fitsBelow && spaceAbove > spaceBelow : vertical === 'top';
|
|
29
|
+
const unclamped = flippedUp ? trigger.top - gap - dropdown.height : trigger.bottom + gap;
|
|
30
|
+
/* The panel is `position: fixed` when portaled, so whatever a viewport edge
|
|
31
|
+
cuts off cannot be scrolled back into view -- hence clamping at all. The two
|
|
32
|
+
edges are not symmetric, though:
|
|
33
|
+
|
|
34
|
+
The top edge is always held. A negative top is unreachable however the side
|
|
35
|
+
was chosen.
|
|
36
|
+
|
|
37
|
+
The bottom edge is held only for an auto-resolved side. `'auto'` picks the
|
|
38
|
+
side with more room and can still overflow in a short viewport, where
|
|
39
|
+
pulling the panel up is right. A *pinned* side is the caller overriding the
|
|
40
|
+
fit, and dragging it back would park the panel over the very trigger it was
|
|
41
|
+
pinned below -- the silent override `placement` exists to escape. */
|
|
42
|
+
const bottomLimit = viewport.height - dropdown.height - margin;
|
|
43
|
+
const withinViewport = vertical === 'auto' && Number.isFinite(viewport.height)
|
|
44
|
+
? Math.min(unclamped, bottomLimit)
|
|
45
|
+
: unclamped;
|
|
46
|
+
const top = Number.isFinite(viewport.height) && dropdown.height > 0
|
|
47
|
+
? Math.max(margin, withinViewport)
|
|
48
|
+
: unclamped;
|
|
24
49
|
return { left, top, minWidth, width, flippedUp };
|
|
25
50
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Corner the dropdown anchors to. Mirrors `MenuPlacement` so the two components
|
|
4
|
+
* that open a panel from a trigger describe placement the same way.
|
|
5
|
+
*/
|
|
6
|
+
export type SelectPlacement = 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'auto';
|
|
2
7
|
export type SelectProperties = MandatorySelectProperties & OptionalSelectProperties & SelectEventProperties;
|
|
3
8
|
export type SelectItem = {
|
|
4
9
|
id: string;
|
|
@@ -103,8 +108,23 @@ export type OptionalSelectProperties = {
|
|
|
103
108
|
classes?: string;
|
|
104
109
|
/** Bindable. Controls whether the dropdown is open; the component writes back on open/close so parents can `bind:open` to observe or drive it. Unbound, the component manages its own state. */
|
|
105
110
|
open?: boolean;
|
|
106
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* @deprecated Use `placement`, which controls both axes. `dropdownAlign: 'left'`
|
|
113
|
+
* is `placement: 'bottom-left'` and `'right'` is `'bottom-right'`. Still honoured
|
|
114
|
+
* when `placement` is not set; scheduled for removal in 5.0.0.
|
|
115
|
+
*/
|
|
107
116
|
dropdownAlign?: 'left' | 'right';
|
|
117
|
+
/**
|
|
118
|
+
* Corner the dropdown panel anchors to, matching `Menu`'s prop of the same name.
|
|
119
|
+
* `'auto'` resolves the best-fitting corner against the viewport on open, so a
|
|
120
|
+
* Select sitting just above a sticky footer opens upward instead of into it.
|
|
121
|
+
*
|
|
122
|
+
* Unset is the pre-5.0 contract and is what `dropdownAlign` still drives: the
|
|
123
|
+
* in-flow panel always opens downward, and a portaled one flips up only when it
|
|
124
|
+
* cannot fit below. Setting `placement` opts both paths into the same resolved
|
|
125
|
+
* corner, so an in-flow Select can finally open upward.
|
|
126
|
+
*/
|
|
127
|
+
placement?: SelectPlacement;
|
|
108
128
|
/** Snippet for rendering a compact trigger summary in multiple mode instead of one Pill per selected value. Receives `{ value, items }` so the consumer can compute e.g. "All" / "3 selected". When not provided the default Pill-per-value behaviour is used. */
|
|
109
129
|
triggerSummary?: import('svelte').Snippet<[{
|
|
110
130
|
value: string[];
|