compote-ui 0.86.0 → 0.87.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/components/number-input/number-input.svelte +61 -19
- package/dist/components/select/select.svelte +2 -1
- package/dist/utils/number-input-wheel.d.ts +32 -0
- package/dist/utils/number-input-wheel.js +46 -0
- package/package.json +1 -1
- package/skills/component-usage/references/forms-and-inputs.md +6 -1
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
import { PhCaretDown, PhCaretUp } from '../../icons';
|
|
6
6
|
import { useLocaleContext } from '@ark-ui/svelte/locale';
|
|
7
7
|
import { toNumberInputValue } from '../../utils/number-input-value';
|
|
8
|
+
import {
|
|
9
|
+
getWheelDirection,
|
|
10
|
+
getWheelNextValue,
|
|
11
|
+
getWheelStep
|
|
12
|
+
} from '../../utils/number-input-wheel';
|
|
8
13
|
|
|
9
14
|
let {
|
|
10
15
|
value = $bindable(),
|
|
@@ -21,6 +26,39 @@
|
|
|
21
26
|
? 'flex items-center justify-between gap-1.5 w-full data-disabled:opacity-50 data-disabled:grayscale'
|
|
22
27
|
: 'flex flex-col gap-1.5 w-full max-w-48 data-disabled:opacity-50 data-disabled:grayscale'
|
|
23
28
|
);
|
|
29
|
+
|
|
30
|
+
type NumberInputApi = { setValue: (n: number) => void; valueAsNumber: number };
|
|
31
|
+
|
|
32
|
+
// Shift+wheel steps by `largeStep`, Alt+wheel by `smallStep` (same modifiers as the arrow
|
|
33
|
+
// keys). Capture phase + stopImmediatePropagation keeps Zag's plain-step wheel handler from also
|
|
34
|
+
// firing. Zag only listens while the input is focused, so we do the same.
|
|
35
|
+
function modifierWheel(api: () => NumberInputApi) {
|
|
36
|
+
return (node: HTMLElement) => {
|
|
37
|
+
function onWheel(event: WheelEvent) {
|
|
38
|
+
if (restProps.disabled || readonly) return;
|
|
39
|
+
const input = node.querySelector('input');
|
|
40
|
+
if (!input || document.activeElement !== input) return;
|
|
41
|
+
const stepSize = getWheelStep(event, restProps);
|
|
42
|
+
if (stepSize === null) return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
event.stopImmediatePropagation();
|
|
45
|
+
const direction = getWheelDirection(event);
|
|
46
|
+
if (direction === 0) return;
|
|
47
|
+
api().setValue(
|
|
48
|
+
getWheelNextValue({
|
|
49
|
+
current: api().valueAsNumber,
|
|
50
|
+
direction,
|
|
51
|
+
stepSize,
|
|
52
|
+
min: restProps.min,
|
|
53
|
+
max: restProps.max,
|
|
54
|
+
allowOverflow: restProps.allowOverflow
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
node.addEventListener('wheel', onWheel, { capture: true, passive: false });
|
|
59
|
+
return () => node.removeEventListener('wheel', onWheel, { capture: true });
|
|
60
|
+
};
|
|
61
|
+
}
|
|
24
62
|
</script>
|
|
25
63
|
|
|
26
64
|
<input type="hidden" {name} value={Number.isNaN(value) ? undefined : value?.toString()} />
|
|
@@ -47,23 +85,27 @@
|
|
|
47
85
|
<Field.RequiredIndicator />
|
|
48
86
|
</NumberInput.Label>
|
|
49
87
|
{/if}
|
|
50
|
-
<NumberInput.
|
|
51
|
-
|
|
52
|
-
class="
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
<NumberInput.Context>
|
|
89
|
+
{#snippet render(api)}
|
|
90
|
+
<NumberInput.Control class="relative isolate" {@attach modifierWheel(api)}>
|
|
91
|
+
<NumberInput.Input
|
|
92
|
+
class="h-9 w-full max-w-48 rounded-md border bg-surface-1 px-3 pr-8 text-right text-sm font-medium tabular-nums shadow-sm focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none data-invalid:border-danger data-invalid:focus-visible:ring-danger"
|
|
93
|
+
/>
|
|
94
|
+
<div
|
|
95
|
+
class="absolute top-px right-px bottom-px z-10 flex w-6 flex-col overflow-hidden rounded-r border-l"
|
|
96
|
+
>
|
|
97
|
+
<NumberInput.IncrementTrigger
|
|
98
|
+
class="inline-flex flex-1 cursor-pointer items-center justify-center border-none text-ink-dim hover:bg-surface-2 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary active:bg-surface-2 disabled:cursor-not-allowed disabled:text-ink-dim"
|
|
99
|
+
>
|
|
100
|
+
<PhCaretUp class="h-4 w-4 shrink-0" />
|
|
101
|
+
</NumberInput.IncrementTrigger>
|
|
102
|
+
<NumberInput.DecrementTrigger
|
|
103
|
+
class="inline-flex flex-1 cursor-pointer items-center justify-center border-t text-ink-dim hover:bg-surface-2 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary active:bg-surface-2 disabled:cursor-not-allowed disabled:text-ink-dim"
|
|
104
|
+
>
|
|
105
|
+
<PhCaretDown class="h-4 w-4 shrink-0" />
|
|
106
|
+
</NumberInput.DecrementTrigger>
|
|
107
|
+
</div>
|
|
108
|
+
</NumberInput.Control>
|
|
109
|
+
{/snippet}
|
|
110
|
+
</NumberInput.Context>
|
|
69
111
|
</NumberInput.Root>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
invalid,
|
|
18
18
|
readOnly,
|
|
19
19
|
disabled,
|
|
20
|
+
deselectable = true,
|
|
20
21
|
...restProps
|
|
21
22
|
}: SelectProps<T> = $props();
|
|
22
23
|
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
invalid={isInvalid}
|
|
35
36
|
readOnly={isReadOnly}
|
|
36
37
|
disabled={isDisabled}
|
|
37
|
-
deselectable
|
|
38
|
+
{deselectable}
|
|
38
39
|
value={value ? [value.toString()] : []}
|
|
39
40
|
onValueChange={(valueChangeDetails) => {
|
|
40
41
|
if (valueChangeDetails.items == null || valueChangeDetails.items.length === 0) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface WheelStepOptions {
|
|
2
|
+
step?: number;
|
|
3
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
4
|
+
largeStep?: number;
|
|
5
|
+
smallStep?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Zag's default step: 1, or 0.01 for percent formatting without an explicit `step`. */
|
|
8
|
+
export declare function getDefaultStep(step?: number, formatOptions?: Intl.NumberFormatOptions): number;
|
|
9
|
+
/**
|
|
10
|
+
* Step size for a wheel event, mirroring Zag's keyboard modifiers: Shift → `largeStep`
|
|
11
|
+
* (default `10 × step`), Alt → `smallStep` (default `step / 10`). Returns `null` for a plain
|
|
12
|
+
* wheel so Zag's own handler keeps handling it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getWheelStep(event: Pick<WheelEvent, 'shiftKey' | 'altKey'>, { step: stepProp, formatOptions, largeStep, smallStep }: WheelStepOptions): number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Wheel direction: +1 for scrolling up (increment), -1 for down, 0 for none. Browsers report
|
|
17
|
+
* Shift+wheel on `deltaX` instead of `deltaY` (Chrome/Edge), hence the fallback.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getWheelDirection(event: Pick<WheelEvent, 'deltaX' | 'deltaY'>): -1 | 0 | 1;
|
|
20
|
+
/**
|
|
21
|
+
* `current + direction × stepSize`, rounded to the precision of the operands (no
|
|
22
|
+
* `0.30000000000000004`) and clamped to `[min, max]` unless `allowOverflow`. An empty
|
|
23
|
+
* (`NaN`) current value starts from 0.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getWheelNextValue(options: {
|
|
26
|
+
current: number;
|
|
27
|
+
direction: -1 | 1;
|
|
28
|
+
stepSize: number;
|
|
29
|
+
min?: number;
|
|
30
|
+
max?: number;
|
|
31
|
+
allowOverflow?: boolean;
|
|
32
|
+
}): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Zag's default step: 1, or 0.01 for percent formatting without an explicit `step`. */
|
|
2
|
+
export function getDefaultStep(step, formatOptions) {
|
|
3
|
+
if (step !== undefined && !Number.isNaN(step))
|
|
4
|
+
return step;
|
|
5
|
+
return formatOptions?.style === 'percent' ? 0.01 : 1;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Step size for a wheel event, mirroring Zag's keyboard modifiers: Shift → `largeStep`
|
|
9
|
+
* (default `10 × step`), Alt → `smallStep` (default `step / 10`). Returns `null` for a plain
|
|
10
|
+
* wheel so Zag's own handler keeps handling it.
|
|
11
|
+
*/
|
|
12
|
+
export function getWheelStep(event, { step: stepProp, formatOptions, largeStep, smallStep }) {
|
|
13
|
+
const step = getDefaultStep(stepProp, formatOptions);
|
|
14
|
+
if (event.shiftKey)
|
|
15
|
+
return largeStep ?? step * 10;
|
|
16
|
+
if (event.altKey)
|
|
17
|
+
return smallStep ?? step / 10;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Wheel direction: +1 for scrolling up (increment), -1 for down, 0 for none. Browsers report
|
|
22
|
+
* Shift+wheel on `deltaX` instead of `deltaY` (Chrome/Edge), hence the fallback.
|
|
23
|
+
*/
|
|
24
|
+
export function getWheelDirection(event) {
|
|
25
|
+
const delta = event.deltaY || event.deltaX;
|
|
26
|
+
return (Math.sign(delta) * -1 || 0);
|
|
27
|
+
}
|
|
28
|
+
function decimals(n) {
|
|
29
|
+
if (!Number.isFinite(n))
|
|
30
|
+
return 0;
|
|
31
|
+
const [mantissa, exponent] = n.toString().split('e');
|
|
32
|
+
const fraction = mantissa.split('.')[1]?.length ?? 0;
|
|
33
|
+
return Math.max(0, fraction - Number(exponent ?? 0));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `current + direction × stepSize`, rounded to the precision of the operands (no
|
|
37
|
+
* `0.30000000000000004`) and clamped to `[min, max]` unless `allowOverflow`. An empty
|
|
38
|
+
* (`NaN`) current value starts from 0.
|
|
39
|
+
*/
|
|
40
|
+
export function getWheelNextValue(options) {
|
|
41
|
+
const { direction, stepSize, min = -Infinity, max = Infinity, allowOverflow } = options;
|
|
42
|
+
const current = Number.isNaN(options.current) ? 0 : options.current;
|
|
43
|
+
const precision = Math.max(decimals(current), decimals(stepSize));
|
|
44
|
+
const next = Number((current + direction * stepSize).toFixed(Math.min(precision, 20)));
|
|
45
|
+
return allowOverflow ? next : Math.min(max, Math.max(min, next));
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -170,7 +170,12 @@ If selection must be restricted to a fixed set (pick-only, no free text), use `C
|
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
Props: `value?: number | null` (bindable), `label?`, `readonly?`, `layout?: 'vertical' | 'horizontal'`,
|
|
173
|
-
plus Ark UI NumberInput props (`min`, `max`, `step`, `
|
|
173
|
+
plus Ark UI NumberInput props (`min`, `max`, `step`, `largeStep`, `smallStep`, `formatOptions`,
|
|
174
|
+
`locale`).
|
|
175
|
+
|
|
176
|
+
Step modifiers (focused input): arrow keys and mouse wheel use `step`; **Shift** uses `largeStep`
|
|
177
|
+
(default `10 × step`), **Alt** uses `smallStep` (default `step / 10`). Shift/Alt+wheel are handled by
|
|
178
|
+
Compote (values are clamped to `min`/`max` unless `allowOverflow`); Ark only provides them for keys.
|
|
174
179
|
|
|
175
180
|
`value` is controlled: setting it to `null` from outside clears the input. Use `defaultValue`
|
|
176
181
|
(a string) only for uncontrolled use, without passing `value`.
|