Skip to content

Commit

Permalink
feat: NumberField (Spinbutton) (#903)
Browse files Browse the repository at this point in the history
* feat: basic feature

* feat: clamp, parsing modelValue

* feat: add locale, a11y, form control

* fix: currencySign was undefined

* chore: allow isNan

* fix: steps, min value, empty initial value

* test: add test

* test: add test case

* feat: expose NumberField

* docs: populate numberfield

* feat: use Label primitive, and make sure a11y

* fix: formatOptions and locale not reactive

* docs: number field example

* docs: add installation section
  • Loading branch information
zernonia committed May 15, 2024
1 parent 362930e commit f77b405
Show file tree
Hide file tree
Showing 34 changed files with 1,650 additions and 26 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
'no-console': 'warn',
'max-statements-per-line': ['error', { max: 2 }],
'vue/one-component-per-file': 'off',
'unicorn/prefer-number-properties': 'off',
},

overrides: [
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export default defineConfig({
{ text: `Listbox ${BadgeHTML('Alpha')}`, link: '/components/listbox' },
{ text: 'Menubar', link: '/components/menubar' },
{ text: 'Navigation Menu', link: '/components/navigation-menu' },
{ text: `Number Field ${BadgeHTML('Alpha')}`, link: '/components/number-field' },
{ text: 'Pagination', link: '/components/pagination' },
{ text: 'Pin Input', link: '/components/pin-input' },
{ text: 'Popover', link: '/components/popover' },
Expand Down
24 changes: 24 additions & 0 deletions docs/components/demo/NumberField/css/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldLabel, NumberFieldRoot } from 'radix-vue'
import { Icon } from '@iconify/vue'
</script>

<template>
<NumberFieldRoot
class="text-sm text-white"
:min="0"
:default-value="18"
>
<NumberFieldLabel>Age</NumberFieldLabel>

<div class="mt-1 flex items-center border bg-blackA7 border-blackA9 rounded-md">
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</div>
</NumberFieldRoot>
</template>
35 changes: 35 additions & 0 deletions docs/components/demo/NumberField/css/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@import '@radix-ui/colors/black-alpha.css';
@import '@radix-ui/colors/grass.css';

/* reset */
button {
all: unset;
}

.CheckboxRoot {
background-color: white;
width: 25px;
height: 25px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px var(--black-a7);
}
.CheckboxRoot:hover {
background-color: var(--grass-3);
}
.CheckboxRoot:focus {
box-shadow: 0 0 0 2px black;
}

.CheckboxIndicator {
color: var(--grass-11);
}

.Label {
color: white;
padding-left: 15px;
font-size: 15px;
line-height: 1;
}
24 changes: 24 additions & 0 deletions docs/components/demo/NumberField/tailwind/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldLabel, NumberFieldRoot } from 'radix-vue'
import { Icon } from '@iconify/vue'
</script>

<template>
<NumberFieldRoot
class="text-sm text-white"
:min="0"
:default-value="18"
>
<NumberFieldLabel>Age</NumberFieldLabel>

<div class="mt-1 flex items-center border bg-blackA7 border-blackA9 rounded-md">
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</div>
</NumberFieldRoot>
</template>
14 changes: 14 additions & 0 deletions docs/components/demo/NumberField/tailwind/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { blackA } = require('@radix-ui/colors')

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./**/*.vue'],
theme: {
extend: {
colors: {
...blackA,
},
},
},
plugins: [],
}
244 changes: 244 additions & 0 deletions docs/content/components/number-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
---

title: Number Field
description: A number field allows a user to enter a number, and increment or decrement the value using stepper buttons.
name: number field
aria: https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton
---

# Number Field

<Description>
A number field allows a user to enter a number, and increment or decrement the value using stepper buttons.
</Description>

<ComponentPreview name="NumberField" />


## Features

<Highlights
:features="[
'Full keyboard navigation.',
'Can be controlled or uncontrolled.',
'Support button hold and wheel event.',
'Support numbering systems in different locale.',
'Customizable formatting.'
]"
/>

## Installation


Install the number package.

<InstallationTabs value="@internationalized/number" />

Install the component from your command line.

<InstallationTabs value="radix-vue" />

## Anatomy

Import all parts and piece them together.

```vue
<script setup>
import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldLabel, NumberFieldRoot } from 'radix-vue'
</script>
<template>
<NumberFieldRoot>
<NumberFieldLabel />
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldRoot>
</template>
```

## API Reference

### Root

Contains all the parts of a number field. An `input` will also render when used within a `form` to ensure events propagate correctly.


<!-- @include: @/meta/NumberFieldRoot.md -->

<DataAttributesTable
:data="[
{
attribute: '[data-disabled]',
values: 'Present when disabled',
},
]"
/>

### Label

Label for the input.

<!-- @include: @/meta/NumberFieldLabel.md -->

### Input

Input

The input component that render the textValue based on value and format options.

<!-- @include: @/meta/NumberFieldInput.md -->


<DataAttributesTable
:data="[
{
attribute: '[data-disabled]',
values: 'Present when disabled',
},
]"
/>


### Increment

The button that increase the value.

<!-- @include: @/meta/NumberFieldIncrement.md -->


<DataAttributesTable
:data="[
{
attribute: '[data-pressed]',
values: 'Present when pressed',
},
{
attribute: '[data-disabled]',
values: 'Present when disabled',
},
]"
/>


### Decrement

The button that decrease the value.

<!-- @include: @/meta/NumberFieldDecrement.md -->


<DataAttributesTable
:data="[
{
attribute: '[data-pressed]',
values: 'Present when pressed',
},
{
attribute: '[data-disabled]',
values: 'Present when disabled',
},
]"
/>


## Example


### Decimal

All options supported by [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) are supported, including configuration of minimum and maximum fraction digits, sign display, grouping separators, etc.


```vue line=3-7
<template>
<NumberFieldRoot
:default-value="5"
:format-options="{
signDisplay: 'exceptZero',
minimumFractionDigits: 1,
}"
>
</NumberFieldRoot>
</template>
```

### Percentage

You can set `formatOptions.style` to `percent` to treat the value as percentage. You need to set the `step` to `0.01` manually to allow appriopriate step size in this mode.


```vue line=3-7
<template>
<NumberFieldRoot
:default-value="0.05"
:step="0.01"
:format-options="{
style: 'percent',
}"
>
</NumberFieldRoot>
</template>
```

### Currency

You can set `formatOptions.style` to `currency` to treat the value as currency value. The `currency` option must also be passed to set the currency code (e.g. USD).

If you need to allow the user to change the currency, you should include a separate dropdown next to the number field. The number field itself will not determine the currency from the user input.

```vue line=4-9
<template>
<NumberFieldRoot
:default-value="5"
:format-options="{
style: 'currency',
currency: 'EUR',
currencyDisplay: 'code',
currencySign: 'accounting',
}"
>
</NumberFieldRoot>
</template>
```



## Accessibility

Adheres to the [Spinbutton WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton).

### Keyboard Interactions

<KeyboardTable
:data="[
{
keys: ['Arrow Up'],
description: 'Increase the value',
},
{
keys: ['Arrow Down'],
description: 'Decrease the value',
},
{
keys: ['Page Up'],
description: 'Increase the value by scale of 10',
},
{
keys: ['Page Down'],
description: 'Decrease the value by scale of 10',
},
{
keys: ['Home'],
description: 'Set value to minimum (if <code>min</code> is provided)',
},
{
keys: ['End'],
description: 'Set value to maximum (if <code>max</code> is provided)',
},
]"
/>
23 changes: 23 additions & 0 deletions docs/content/meta/NumberFieldDecrement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- This file was automatic generated. Do not edit it manually -->

<PropsTable :data="[
{
'name': 'as',
'description': '<p>The element or component this component should render as. Can be overwrite by <code>asChild</code></p>\n',
'type': 'AsTag | Component',
'required': false,
'default': '\'button\''
},
{
'name': 'asChild',
'description': '<p>Change the default rendered element for the one passed as a child, merging their props and behavior.</p>\n<p>Read our <a href=\'https://www.radix-vue.com/guides/composition.html\'>Composition</a> guide for more details.</p>\n',
'type': 'boolean',
'required': false
},
{
'name': 'disabled',
'description': '',
'type': 'boolean',
'required': false
}
]" />

0 comments on commit f77b405

Please sign in to comment.