Skip to content

Commit 0b1f0e9

Browse files
authored
Number field (#58)
1 parent f6535bf commit 0b1f0e9

File tree

15 files changed

+482
-9
lines changed

15 files changed

+482
-9
lines changed

packages/primitives/src/components/number-field/number-field-root.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,14 @@ const inputMode = computed<HTMLAttributes['inputmode']>(() => {
9090
// Replace negative textValue formatted using currencySign: 'accounting'
9191
// with a textValue that can be announced using a minus sign.
9292
const textValueFormatter = useNumberFormatter(locale, formatOptions);
93-
const textValue = computed(() => (Number.isNaN(modelValue.value) ? '' : textValueFormatter.format(modelValue.value)));
93+
const textValue = computed(() => {
94+
if (Number.isNaN(modelValue.value)) return '';
95+
96+
const formatted = textValueFormatter.format(modelValue.value);
97+
if (formatted === 'NaN') return '';
98+
99+
return formatted;
100+
});
94101
95102
function validate(val: string) {
96103
return numberParser.isValidPartialNumber(val, min.value, max.value);

packages/primitives/src/components/number-field/shared.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { unrefElement, useEventListener } from '@vueuse/core';
44
import type { MaybeComputedElementRef } from '@vueuse/core';
55
import { createEventHook, isClient, reactiveComputed } from '@vueuse/shared';
66
import { NumberFormatter, NumberParser } from '@internationalized/number';
7+
import { isNullish } from '../../shared';
78

89
export function usePressedHold(options: { target?: MaybeComputedElementRef; disabled: Ref<boolean> }) {
910
const { disabled } = options;
@@ -68,7 +69,11 @@ export function useNumberParser(locale: Ref<string>, options: Ref<Intl.NumberFor
6869
return reactiveComputed(() => new NumberParser(locale.value, options.value));
6970
}
7071

71-
export function handleDecimalOperation(operator: '-' | '+', value1: number, value2: number): number {
72+
export function handleDecimalOperation(operator: '-' | '+', value1?: number, value2?: number): number {
73+
if (isNullish(value1) || isNullish(value2)) {
74+
return 0;
75+
}
76+
7277
let v1 = value1;
7378
let v2 = value2;
7479

packages/ui/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * from './label';
2323
export * from './menu';
2424
export * from './menubar';
2525
export * from './navigation-menu';
26+
export * from './number-field';
2627
export * from './pagination';
2728
export * from './pin-input';
2829
export * from './popover';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import SNumberFieldDecrement from './number-field-decrement.vue';
2+
import SNumberFieldIncrement from './number-field-increment.vue';
3+
import SNumberFieldInput from './number-field-input.vue';
4+
import SNumberFieldRoot from './number-field-root.vue';
5+
import SNumberField from './number-field.vue';
6+
7+
export { SNumberFieldDecrement, SNumberFieldIncrement, SNumberFieldInput, SNumberFieldRoot, SNumberField };
8+
9+
export * from './types';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { NumberFieldDecrement, Slot } from '@soybean-ui/primitives';
4+
import { cn, numberFieldVariants } from '@soybean-ui/variants';
5+
import { Minus } from 'lucide-vue-next';
6+
import type { NumberFieldDecrementProps } from './types';
7+
8+
defineOptions({
9+
name: 'SNumberFieldDecrement'
10+
});
11+
12+
const { class: cls, size, center, iconClass, disabled } = defineProps<NumberFieldDecrementProps>();
13+
14+
const mergedCls = computed(() => {
15+
const { decrement, decrementIcon } = numberFieldVariants({ size, center });
16+
return {
17+
cls: cn(decrement(), cls),
18+
iconCls: cn(decrementIcon(), iconClass)
19+
};
20+
});
21+
</script>
22+
23+
<template>
24+
<NumberFieldDecrement :class="mergedCls.cls" data-slot="decrement" :disabled="disabled">
25+
<Slot :class="mergedCls.iconCls">
26+
<slot>
27+
<Minus />
28+
</slot>
29+
</Slot>
30+
</NumberFieldDecrement>
31+
</template>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { NumberFieldIncrement, Slot } from '@soybean-ui/primitives';
4+
import { cn, numberFieldVariants } from '@soybean-ui/variants';
5+
import { Plus } from 'lucide-vue-next';
6+
import type { NumberFieldIncrementProps } from './types';
7+
8+
defineOptions({
9+
name: 'SNumberFieldIncrement'
10+
});
11+
12+
const { class: cls, size, center, iconClass, disabled } = defineProps<NumberFieldIncrementProps>();
13+
14+
const mergedCls = computed(() => {
15+
const { increment, incrementIcon } = numberFieldVariants({ size, center });
16+
17+
return {
18+
cls: cn(increment(), cls),
19+
iconCls: cn(incrementIcon(), iconClass)
20+
};
21+
});
22+
</script>
23+
24+
<template>
25+
<NumberFieldIncrement :class="mergedCls.cls" data-slot="increment" :disabled="disabled">
26+
<Slot :class="mergedCls.iconCls">
27+
<slot>
28+
<Plus />
29+
</slot>
30+
</Slot>
31+
</NumberFieldIncrement>
32+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { NumberFieldInput } from '@soybean-ui/primitives';
4+
import { cn, numberFieldVariants } from '@soybean-ui/variants';
5+
import type { NumberFieldInputProps } from './types';
6+
7+
defineOptions({
8+
name: 'SNumberFieldInput'
9+
});
10+
11+
const { class: cls, size, center } = defineProps<NumberFieldInputProps>();
12+
13+
const mergedCls = computed(() => {
14+
const { input } = numberFieldVariants({ size, center });
15+
16+
return cn(input(), cls);
17+
});
18+
</script>
19+
20+
<template>
21+
<NumberFieldInput :class="mergedCls" data-slot="input" />
22+
</template>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { NumberFieldRoot, useForwardPropsEmits } from '@soybean-ui/primitives';
4+
import { cn, numberFieldVariants } from '@soybean-ui/variants';
5+
import type { NumberFieldRootEmits, NumberFieldRootProps } from './types';
6+
7+
defineOptions({
8+
name: 'SNumberFieldRoot'
9+
});
10+
11+
const { class: cls, size, ...delegatedProps } = defineProps<NumberFieldRootProps>();
12+
13+
const emit = defineEmits<NumberFieldRootEmits>();
14+
15+
const forwarded = useForwardPropsEmits(delegatedProps, emit);
16+
17+
const mergedCls = computed(() => {
18+
const { root } = numberFieldVariants({ size });
19+
20+
return cn(root(), cls);
21+
});
22+
</script>
23+
24+
<template>
25+
<NumberFieldRoot v-bind="forwarded" :class="mergedCls">
26+
<slot />
27+
</NumberFieldRoot>
28+
</template>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import { useForwardPropsEmits } from '@soybean-ui/primitives';
3+
import NumberFieldDecrement from './number-field-decrement.vue';
4+
import NumberFieldIncrement from './number-field-increment.vue';
5+
import NumberFieldInput from './number-field-input.vue';
6+
import NumberFieldRoot from './number-field-root.vue';
7+
import type { NumberFieldEmits, NumberFieldProps } from './types';
8+
9+
defineOptions({
10+
name: 'SNumberField'
11+
});
12+
13+
const {
14+
class: cls,
15+
size,
16+
center,
17+
disabledDecrement,
18+
disabledIncrement,
19+
ui,
20+
...rootProps
21+
} = defineProps<NumberFieldProps>();
22+
23+
const emit = defineEmits<NumberFieldEmits>();
24+
25+
const forwarded = useForwardPropsEmits(rootProps, emit);
26+
</script>
27+
28+
<template>
29+
<NumberFieldRoot v-bind="forwarded" :class="cls || ui?.root" :size="size">
30+
<NumberFieldInput :class="ui?.input" :size="size" :center="center" />
31+
<NumberFieldDecrement
32+
:class="ui?.decrement"
33+
:size="size"
34+
:center="center"
35+
:icon-class="ui?.decrementIcon"
36+
:disabled="disabled || disabledDecrement"
37+
>
38+
<slot name="decrement-icon" />
39+
</NumberFieldDecrement>
40+
<NumberFieldIncrement
41+
:class="ui?.increment"
42+
:size="size"
43+
:center="center"
44+
:icon-class="ui?.incrementIcon"
45+
:disabled="disabled || disabledIncrement"
46+
>
47+
<slot name="increment-icon" />
48+
</NumberFieldIncrement>
49+
</NumberFieldRoot>
50+
</template>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type {
2+
ClassValue,
3+
NumberFieldRootEmits,
4+
NumberFieldDecrementProps as _NumberFieldDecrementProps,
5+
NumberFieldIncrementProps as _NumberFieldIncrementProps,
6+
NumberFieldInputProps as _NumberFieldInputProps,
7+
NumberFieldRootProps as _NumberFieldRootProps
8+
} from '@soybean-ui/primitives';
9+
import type { NumberFieldSlots, ThemeSize } from '@soybean-ui/variants';
10+
11+
export interface NumberFieldRootProps extends _NumberFieldRootProps {
12+
size?: ThemeSize;
13+
}
14+
15+
export interface NumberFieldInputProps extends _NumberFieldInputProps {
16+
size?: ThemeSize;
17+
center?: boolean;
18+
}
19+
20+
export interface NumberFieldDecrementProps extends _NumberFieldDecrementProps {
21+
size?: ThemeSize;
22+
center?: boolean;
23+
iconClass?: ClassValue;
24+
}
25+
26+
export interface NumberFieldIncrementProps extends _NumberFieldIncrementProps {
27+
size?: ThemeSize;
28+
center?: boolean;
29+
iconClass?: ClassValue;
30+
}
31+
32+
export type NumberFieldUi = Partial<Record<NumberFieldSlots, ClassValue>>;
33+
34+
export interface NumberFieldProps extends NumberFieldRootProps {
35+
center?: boolean;
36+
disabledDecrement?: boolean;
37+
disabledIncrement?: boolean;
38+
ui?: NumberFieldUi;
39+
}
40+
41+
export type NumberFieldEmits = NumberFieldRootEmits;
42+
43+
export type { NumberFieldRootEmits };

packages/variants/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './variants/label';
2121
export * from './variants/menu';
2222
export * from './variants/menubar';
2323
export * from './variants/navigation-menu';
24+
export * from './variants/number-field';
2425
export * from './variants/pagination';
2526
export * from './variants/pin-input';
2627
export * from './variants/popover';
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// @unocss-include
2+
import { tv } from 'tailwind-variants';
3+
4+
export const numberFieldVariants = tv({
5+
slots: {
6+
root: 'flex items-center w-full rounded-md border border-input bg-background focus-within:(border-input ring-2 ring-offset-2 ring-primary) disabled:(cursor-not-allowed opacity-50)',
7+
decrement: `flex h-full shrink-0 items-center justify-center bg-transparent outline-none disabled:(cursor-not-allowed opacity-20)`,
8+
decrementIcon: '',
9+
increment: `flex h-full shrink-0 items-center justify-center bg-transparent outline-none disabled:(cursor-not-allowed opacity-20)`,
10+
incrementIcon: '',
11+
input: [
12+
`h-full w-full grow bg-transparent`,
13+
`placeholder:text-muted-foreground outline-none disabled:(cursor-not-allowed opacity-50)`
14+
]
15+
},
16+
variants: {
17+
size: {
18+
xs: {
19+
decrement: 'p-1',
20+
decrementIcon: 'text-xs',
21+
increment: 'p-1',
22+
incrementIcon: 'text-xs',
23+
input: 'h-6 text-xs'
24+
},
25+
sm: {
26+
decrement: 'p-1.25',
27+
decrementIcon: 'text-sm',
28+
increment: 'p-1.25',
29+
incrementIcon: 'text-sm',
30+
input: 'h-7 text-sm'
31+
},
32+
md: {
33+
decrement: 'p-1.25',
34+
decrementIcon: 'text-sm',
35+
increment: 'p-1.25',
36+
incrementIcon: 'text-sm',
37+
input: 'h-8 text-sm'
38+
},
39+
lg: {
40+
decrement: 'p-1.5',
41+
decrementIcon: 'text-base',
42+
increment: 'p-1.5',
43+
incrementIcon: 'text-base',
44+
input: 'h-9 text-base'
45+
},
46+
xl: {
47+
decrement: 'p-1.5',
48+
decrementIcon: 'text-lg',
49+
increment: 'p-1.5',
50+
incrementIcon: 'text-lg',
51+
input: 'h-10 text-base'
52+
},
53+
xxl: {
54+
decrement: 'p-2',
55+
decrementIcon: 'text-xl',
56+
increment: 'p-2',
57+
incrementIcon: 'text-xl',
58+
input: 'h-12 text-lg'
59+
}
60+
},
61+
center: {
62+
true: {
63+
decrement: 'order-1',
64+
input: 'text-center order-2',
65+
increment: 'order-3'
66+
}
67+
}
68+
},
69+
compoundVariants: [
70+
{
71+
size: 'xs',
72+
center: false,
73+
class: {
74+
input: 'pl-1.5'
75+
}
76+
},
77+
{
78+
size: 'sm',
79+
center: false,
80+
class: {
81+
input: 'pl-2'
82+
}
83+
},
84+
{
85+
size: 'md',
86+
center: false,
87+
class: {
88+
input: 'pl-2.5'
89+
}
90+
},
91+
{
92+
size: 'lg',
93+
center: false,
94+
class: {
95+
input: 'pl-3'
96+
}
97+
},
98+
{
99+
size: 'xl',
100+
center: false,
101+
class: {
102+
input: 'pl-3.5'
103+
}
104+
},
105+
{
106+
size: 'xxl',
107+
center: false,
108+
class: {
109+
input: 'pl-4'
110+
}
111+
}
112+
],
113+
defaultVariants: {
114+
size: 'md',
115+
center: false
116+
}
117+
});
118+
119+
export type NumberFieldSlots = keyof typeof numberFieldVariants.slots;

0 commit comments

Comments
 (0)