diff --git a/packages/components/package.json b/packages/components/package.json index 9bd67982e..7b7dbd7a1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -20,6 +20,7 @@ "./Link": "./dist/Link.js", "./Navigation": "./dist/Navigation.js", "./Note": "./dist/Note.js", + "./NumberField": "./dist/NumberField.js", "./RadioGroup": "./dist/RadioGroup.js", "./StatusIcon": "./dist/StatusIcon.js", "./Switch": "./dist/Switch.js", diff --git a/packages/components/src/components/Button/Button.module.scss b/packages/components/src/components/Button/Button.module.scss index 30af9e653..9b8bc40f8 100644 --- a/packages/components/src/components/Button/Button.module.scss +++ b/packages/components/src/components/Button/Button.module.scss @@ -6,6 +6,7 @@ display: flex; flex-direction: row; align-items: center; + justify-content: center; padding-block: var(--button--padding-squished-y); padding-inline: var(--button--padding-squished-x); font-size: var(--font-size--default); diff --git a/packages/components/src/components/NumberField/NumberField.module.scss b/packages/components/src/components/NumberField/NumberField.module.scss new file mode 100644 index 000000000..8dd2044c9 --- /dev/null +++ b/packages/components/src/components/NumberField/NumberField.module.scss @@ -0,0 +1,113 @@ +@use "@/styles/mixins/textInput.scss"; + +.group { + order: 2; + display: grid; + grid-template-columns: 1fr max-content; + grid-template-areas: + "input increment" + "input decrement"; +} + +.input { + @include textInput.textInput(); + grid-area: input; +} + +.decrementButton { + grid-area: decrement; +} + +.incrementButton { + grid-area: increment; +} + +.group.group { + .decrementButton, + .incrementButton { + color: var(--form-field-control--color); + border-width: var(--form-control--border-width); + border-color: var(--form-field-control--border-color); + border-style: solid; + } +} + +@media (pointer: fine) { + .input { + border-start-end-radius: 0; + border-end-end-radius: 0; + } + + .incrementButton { + [data-icon="plus"] { + display: none; + } + } + + .decrementButton { + [data-icon="minus"] { + display: none; + } + } + + .group.group { + .decrementButton, + .incrementButton { + border-inline-start: none; + border-start-start-radius: 0; + border-end-start-radius: 0; + padding-block: 0; + } + + .decrementButton { + border-start-end-radius: 0; + } + + .incrementButton { + border-block-end: none; + border-end-end-radius: 0; + } + } +} + +@media (pointer: coarse) { + .group { + grid-template-columns: auto 1fr auto; + grid-template-areas: "decrement input increment"; + } + + .input { + border-radius: 0; + } + + .decrementButton { + [data-icon="chevron-down"] { + display: none; + } + } + + .incrementButton { + [data-icon="chevron-up"] { + display: none; + } + } + + .group.group { + .decrementButton, + .incrementButton { + padding-inline: var(--form-control--padding-x); + } + + .decrementButton { + border-start-end-radius: 0; + border-end-end-radius: 0; + border-inline-end: none; + } + + .incrementButton { + border-start-start-radius: 0; + border-end-start-radius: 0; + border-inline-start: none; + } + } +} diff --git a/packages/components/src/components/NumberField/NumberField.tsx b/packages/components/src/components/NumberField/NumberField.tsx new file mode 100644 index 000000000..74087bdfb --- /dev/null +++ b/packages/components/src/components/NumberField/NumberField.tsx @@ -0,0 +1,67 @@ +import React, { FC, PropsWithChildren } from "react"; +import * as Aria from "react-aria-components"; +import formFieldStyles from "../FormField/FormField.module.scss"; +import styles from "./NumberField.module.scss"; +import clsx from "clsx"; +import { PropsContext, PropsContextProvider } from "@/lib/propsContext"; +import { FieldError } from "@/components/FieldError"; +import { Button } from "@/components/Button"; +import { Icon } from "@/components/Icon"; +import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; +import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; +import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus"; + +export interface NumberFieldProps + extends PropsWithChildren> {} + +export const NumberField: FC = (props) => { + const { children, className, ...rest } = props; + + const rootClassName = clsx(formFieldStyles.formField, className); + + const propsContext: PropsContext = { + Label: { + className: formFieldStyles.label, + optional: !props.isRequired, + }, + FieldDescription: { + className: formFieldStyles.fieldDescription, + }, + FieldError: { + className: formFieldStyles.customFieldError, + }, + }; + + return ( + + + + + + + + {children} + + + + ); +}; + +export default NumberField; diff --git a/packages/components/src/components/NumberField/index.ts b/packages/components/src/components/NumberField/index.ts new file mode 100644 index 000000000..6f431d39e --- /dev/null +++ b/packages/components/src/components/NumberField/index.ts @@ -0,0 +1,3 @@ +import { NumberField } from "./NumberField"; +export { type NumberFieldProps, NumberField } from "./NumberField"; +export default NumberField; diff --git a/packages/components/src/components/NumberField/stories/Default.stories.tsx b/packages/components/src/components/NumberField/stories/Default.stories.tsx new file mode 100644 index 000000000..1fc9b3b13 --- /dev/null +++ b/packages/components/src/components/NumberField/stories/Default.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { NumberField } from "../index"; +import React from "react"; +import { Label } from "@/components/Label"; +import { action } from "@storybook/addon-actions"; +import FieldDescription from "@/components/FieldDescription/FieldDescription"; +import { FieldError } from "@/components/FieldError"; + +const meta: Meta = { + title: "Forms/NumberField", + component: NumberField, + render: (props) => ( + + + + ), +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Disabled: Story = { args: { isDisabled: true } }; + +export const Required: Story = { + args: { isRequired: true }, +}; + +export const WithFieldDescription: Story = { + render: (props) => ( + + + Enter your age + + ), +}; + +export const WithDefaultValue: Story = { + render: (props) => ( + + + + ), +}; + +export const WithFieldError: Story = { + render: (props) => ( + + + Age is required + + ), +}; diff --git a/packages/components/src/components/NumberField/stories/EdgeCases.stories.tsx b/packages/components/src/components/NumberField/stories/EdgeCases.stories.tsx new file mode 100644 index 000000000..3a4977b87 --- /dev/null +++ b/packages/components/src/components/NumberField/stories/EdgeCases.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { NumberField } from "../index"; +import React from "react"; +import { Label } from "@/components/Label"; +import { action } from "@storybook/addon-actions"; + +const meta: Meta = { + title: "Forms/NumberField/EdgeCases", + component: NumberField, + render: (props) => ( + + + + ), +}; + +export default meta; + +type Story = StoryObj; + +export const WithDisabledDecrement: Story = { + args: { minValue: 5, defaultValue: 5 }, +}; + +export const WithDisabledIncrement: Story = { + args: { maxValue: 5, defaultValue: 5 }, +}; diff --git a/packages/components/src/styles/mixins/formControl.scss b/packages/components/src/styles/mixins/formControl.scss new file mode 100644 index 000000000..6a90c10a4 --- /dev/null +++ b/packages/components/src/styles/mixins/formControl.scss @@ -0,0 +1,18 @@ +@mixin formControl { + color: var(--form-field-control--color); + + border-width: var(--form-control--border-width); + border-style: var(--form-control--border-style); + border-radius: var(--form-control--border-radius); + border-color: var(--form-field-control--border-color); + + padding-block: var(--form-control--padding-y); + padding-inline: var(--form-control--padding-x); + + background-color: var(--form-field-control--background-color); + + &:focus-visible, + &:focus { + outline: none; + } +} diff --git a/packages/components/src/styles/mixins/textInput.scss b/packages/components/src/styles/mixins/textInput.scss index c58dae059..a5162f846 100644 --- a/packages/components/src/styles/mixins/textInput.scss +++ b/packages/components/src/styles/mixins/textInput.scss @@ -1,22 +1,6 @@ -@mixin textInput { - & { - order: 2; - - color: var(--form-field-control--color); - - border-width: var(--form-control--border-width); - border-style: var(--form-control--border-style); - border-radius: var(--form-control--border-radius); - border-color: var(--form-field-control--border-color); +@use "./formControl"; - padding-block: var(--form-control--padding-y); - padding-inline: var(--form-control--padding-x); - - background-color: var(--form-field-control--background-color); - } - - &:focus-visible, - &:focus { - outline: none; - } +@mixin textInput { + order: 2; + @include formControl.formControl(); } diff --git a/packages/components/vite.build.config.ts b/packages/components/vite.build.config.ts index fa34ad04b..ec173e8c0 100644 --- a/packages/components/vite.build.config.ts +++ b/packages/components/vite.build.config.ts @@ -31,6 +31,7 @@ export default defineConfig( Link: "./src/components/Link/index.ts", Navigation: "./src/components/Navigation/index.ts", Note: "./src/components/Note/index.ts", + NumberField: "./src/components/NumberField/index.ts", Radio: "./src/components/RadioGroup/components/Radio/index.ts", RadioGroup: "./src/components/RadioGroup/index.ts", StatusIcon: "./src/components/StatusIcon/index.ts",