diff --git a/.changeset/date-field-component.md b/.changeset/date-field-component.md new file mode 100644 index 000000000..9ca43d9d8 --- /dev/null +++ b/.changeset/date-field-component.md @@ -0,0 +1,5 @@ +--- +'@godaddy/antares': minor +--- + +feat(antares): add DateField component and re-export I18nProvider diff --git a/packages/@godaddy/antares/components/_internal/field-frame/src/index.module.css b/packages/@godaddy/antares/components/_internal/field-frame/src/index.module.css index 7bb14fda6..50eeea3a8 100644 --- a/packages/@godaddy/antares/components/_internal/field-frame/src/index.module.css +++ b/packages/@godaddy/antares/components/_internal/field-frame/src/index.module.css @@ -26,8 +26,10 @@ .frame { border-radius: var(--field-frame-br); - min-height: 40.5px; box-sizing: border-box; + font-family: inherit; + font-size: var(--font-body-size-md, var(--ux-cxbe8g, 1rem)); + line-height: var(--ux-jw5s9j, 1.375); & > :first-child { border-start-start-radius: var(--field-frame-br); @@ -56,12 +58,14 @@ } &[data-invalid]:has(input[data-focused="true"]), - &[data-invalid]:has(textarea[data-focused="true"]) { + &[data-invalid]:has(textarea[data-focused="true"]), + &[data-invalid]:has([role="spinbutton"][data-focused="true"]) { outline: 2px solid var(--field-frame-critical-color); } &:has(input[data-focused="true"]), - &:has(textarea[data-focused="true"]) { + &:has(textarea[data-focused="true"]), + &:has([role="spinbutton"][data-focused="true"]) { outline: 2px solid Highlight; outline: 2px auto -webkit-focus-ring-color; outline-offset: -1px; @@ -74,22 +78,16 @@ } .input { + font: inherit; background: transparent; border: none; outline: none; flex: 1; padding-inline: var(--sp-md); + padding-block: var(--sp-md); min-width: 0; &[data-disabled] { cursor: not-allowed; } } - -input.input { - padding-block: var(--sp-md); -} - -textarea.input { - padding-block: var(--sp-sm); -} diff --git a/packages/@godaddy/antares/components/_internal/field-frame/src/index.tsx b/packages/@godaddy/antares/components/_internal/field-frame/src/index.tsx index 6fe86c1fe..e5c4348e5 100644 --- a/packages/@godaddy/antares/components/_internal/field-frame/src/index.tsx +++ b/packages/@godaddy/antares/components/_internal/field-frame/src/index.tsx @@ -117,7 +117,7 @@ export const FieldFrame = forwardRef(function F (function F )} + +## Features + +- **Segmented input**: Editable Year, Month, and Day segments with arrow-key adjustment +- **Label, description, error**: Optional label, helper text, and error message with proper accessibility +- **Controlled or uncontrolled**: Use `value` and `onChange`, or `defaultValue` +- **Range bounds**: `minValue` and `maxValue` reject out-of-range entry +- **Form integration**: Submits an ISO date string when given a `name` prop +- **Accessible by default**: Full keyboard support, ARIA semantics, and locale-aware segment handling + +## Installation + +```bash +npm install --save @godaddy/antares +``` + +## Working with dates + +Date components in Antares are typed for `CalendarDate` from `@internationalized/date` — a +date-only type with no time and no timezone. + +### Why `@internationalized/date`? + +JavaScript's built-in `Date` is a single timestamp that conflates calendar date, time of day, +and timezone. That's the wrong shape for "a calendar date" (e.g. a birthday or a booking date), +and round-tripping it through string parsers and `toISOString()` is the source of countless +off-by-one timezone bugs. [`@internationalized/date`](https://react-spectrum.adobe.com/internationalized/date/index.html) +separates these concepts cleanly. Antares date components only deal with `CalendarDate`. + +### Installation + +Install `@internationalized/date` alongside `@godaddy/antares`: + +```bash +npm install --save @internationalized/date +``` + +### Locale and i18n + +Locale comes from the host app's ``, which Antares exports from +`@godaddy/antares`. If no `` is found, the date will be formatted in the default locale. + +## Props + +The DateField component accepts the following props: + + + +## Examples + +### Default + +Minimal usage with a label. + + + + +### With default value + +Pass a `CalendarDate` to `defaultValue` to initialize the segments. + + + + +### Controlled + +Use `value` and `onChange` for controlled state. + + + + +### With description + +Provide helper text via `description`. + + + + +### With error + +Use `isInvalid` with `errorMessage` for validation feedback. + + + + +### Min and max + +Constrain typed entry with `minValue` and `maxValue`. + + + + +### Disabled, required, read-only + +`isDisabled` blocks all interaction. `isRequired` marks the field for form validation. `isReadOnly` +allows focus and copy but blocks edits. + + + + +### Form + +Pass `name` to integrate with native `
` submission. The submitted value is a date string with the format `YYYY-MM-DD`. + + + + +### With I18nProvider + +Wrap the `DateField` in an `` to override the locale used for segment ordering, formatting, and writing direction. Compare an English locale to an RTL Arabic locale below. + + + diff --git a/packages/@godaddy/antares/components/date-field/date-field.stories.tsx b/packages/@godaddy/antares/components/date-field/date-field.stories.tsx new file mode 100644 index 000000000..efe7c3648 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/date-field.stories.tsx @@ -0,0 +1,60 @@ +'use client'; +import { getComponentDocs, getMeta, getStory } from '@bento/storybook-addon-helpers'; +import { DateFieldBasicExample } from './examples/basic.tsx'; +import { DateFieldControlledExample } from './examples/controlled.tsx'; +import { DateFieldDisabledRequiredReadOnlyExample } from './examples/disabled-required-readonly.tsx'; +import { DateFieldFormExample } from './examples/form.tsx'; +import { DateFieldMinMaxExample } from './examples/min-max.tsx'; +import { + DateFieldPlaygroundExample, + type DateFieldPlaygroundExampleProps +} from './examples/date-field-playground.tsx'; +import { DateFieldWithDefaultValueExample } from './examples/with-default-value.tsx'; +import { DateFieldWithDescriptionExample } from './examples/with-description.tsx'; +import { DateFieldWithErrorExample } from './examples/with-error.tsx'; +import { DateFieldWithI18nExample } from './examples/with-i18n.tsx'; +import { DateField } from './src/index.tsx'; + +export default getMeta({ + title: 'components/DateField' +}); + +export const Props = getComponentDocs(DateField); + +export const Default = getStory(DateFieldBasicExample); + +export const WithDefaultValue = getStory(DateFieldWithDefaultValueExample); + +export const Controlled = getStory(DateFieldControlledExample); + +export const WithDescription = getStory(DateFieldWithDescriptionExample); + +export const WithError = getStory(DateFieldWithErrorExample); + +export const MinMax = getStory(DateFieldMinMaxExample); + +export const DisabledRequiredReadOnly = getStory(DateFieldDisabledRequiredReadOnlyExample); + +export const Form = getStory(DateFieldFormExample); + +export const WithI18n = getStory(DateFieldWithI18nExample); + +export const Playground = { + render: (args: DateFieldPlaygroundExampleProps) => , + args: { + label: 'Start date', + isDisabled: false, + isInvalid: false, + isReadOnly: false, + isRequired: false + }, + argTypes: { + label: { control: 'text', description: 'Label text shown above the frame' }, + description: { control: 'text', description: 'Helper text shown below the frame' }, + errorMessage: { control: 'text', description: 'Error message when invalid' }, + isDisabled: { control: 'boolean', description: 'Disable the input' }, + isInvalid: { control: 'boolean', description: 'Show invalid state' }, + isReadOnly: { control: 'boolean', description: 'Make the input read-only' }, + isRequired: { control: 'boolean', description: 'Mark as required' } + } +}; diff --git a/packages/@godaddy/antares/components/date-field/examples/basic.tsx b/packages/@godaddy/antares/components/date-field/examples/basic.tsx new file mode 100644 index 000000000..6bc8d94fb --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/basic.tsx @@ -0,0 +1,5 @@ +import { DateField, type DateFieldProps } from '@godaddy/antares'; + +export function DateFieldBasicExample(props: DateFieldProps) { + return ; +} diff --git a/packages/@godaddy/antares/components/date-field/examples/controlled.tsx b/packages/@godaddy/antares/components/date-field/examples/controlled.tsx new file mode 100644 index 000000000..c41ce7ab2 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/controlled.tsx @@ -0,0 +1,16 @@ +import { type CalendarDate, parseDate } from '@internationalized/date'; +import { useState } from 'react'; +import { DateField, Text } from '@godaddy/antares'; + +export function DateFieldControlledExample() { + const [value, setValue] = useState(parseDate('2024-03-15')); + + return ( + <> + + + Value: {value ? value.toString() : '(empty)'} + + + ); +} diff --git a/packages/@godaddy/antares/components/date-field/examples/date-field-playground.tsx b/packages/@godaddy/antares/components/date-field/examples/date-field-playground.tsx new file mode 100644 index 000000000..59ad8a361 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/date-field-playground.tsx @@ -0,0 +1,33 @@ +import { DateField } from '@godaddy/antares'; + +export interface DateFieldPlaygroundExampleProps { + description?: string; + errorMessage?: string; + isDisabled?: boolean; + isInvalid?: boolean; + isReadOnly?: boolean; + isRequired?: boolean; + label?: string; +} + +export function DateFieldPlaygroundExample({ + description, + errorMessage, + isDisabled, + isInvalid, + isReadOnly, + isRequired, + label +}: DateFieldPlaygroundExampleProps) { + return ( + + ); +} diff --git a/packages/@godaddy/antares/components/date-field/examples/disabled-required-readonly.tsx b/packages/@godaddy/antares/components/date-field/examples/disabled-required-readonly.tsx new file mode 100644 index 000000000..a7ce5d2c8 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/disabled-required-readonly.tsx @@ -0,0 +1,12 @@ +import { parseDate } from '@internationalized/date'; +import { Flex, DateField } from '@godaddy/antares'; + +export function DateFieldDisabledRequiredReadOnlyExample() { + return ( + + + + + + ); +} diff --git a/packages/@godaddy/antares/components/date-field/examples/form.tsx b/packages/@godaddy/antares/components/date-field/examples/form.tsx new file mode 100644 index 000000000..f2c722143 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/form.tsx @@ -0,0 +1,26 @@ +import { type FormEvent, useState } from 'react'; +import { Button, DateField, Flex, Text } from '@godaddy/antares'; + +export function DateFieldFormExample() { + const [submitted, setSubmitted] = useState(null); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + setSubmitted(String(formData.get('startDate') ?? '')); + } + + return ( + + + + + {submitted !== null && ( + + Submitted: {submitted || '(empty)'} + + )} + + + ); +} diff --git a/packages/@godaddy/antares/components/date-field/examples/min-max.tsx b/packages/@godaddy/antares/components/date-field/examples/min-max.tsx new file mode 100644 index 000000000..bbbea9464 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/min-max.tsx @@ -0,0 +1,14 @@ +import { parseDate } from '@internationalized/date'; +import { DateField } from '@godaddy/antares'; + +export function DateFieldMinMaxExample() { + return ( + + ); +} diff --git a/packages/@godaddy/antares/components/date-field/examples/with-default-value.tsx b/packages/@godaddy/antares/components/date-field/examples/with-default-value.tsx new file mode 100644 index 000000000..2e4716586 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/with-default-value.tsx @@ -0,0 +1,6 @@ +import { parseDate } from '@internationalized/date'; +import { DateField } from '@godaddy/antares'; + +export function DateFieldWithDefaultValueExample() { + return ; +} diff --git a/packages/@godaddy/antares/components/date-field/examples/with-description.tsx b/packages/@godaddy/antares/components/date-field/examples/with-description.tsx new file mode 100644 index 000000000..996312776 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/with-description.tsx @@ -0,0 +1,5 @@ +import { DateField } from '@godaddy/antares'; + +export function DateFieldWithDescriptionExample() { + return ; +} diff --git a/packages/@godaddy/antares/components/date-field/examples/with-error.tsx b/packages/@godaddy/antares/components/date-field/examples/with-error.tsx new file mode 100644 index 000000000..d2c444d72 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/with-error.tsx @@ -0,0 +1,5 @@ +import { DateField } from '@godaddy/antares'; + +export function DateFieldWithErrorExample() { + return ; +} diff --git a/packages/@godaddy/antares/components/date-field/examples/with-i18n.tsx b/packages/@godaddy/antares/components/date-field/examples/with-i18n.tsx new file mode 100644 index 000000000..4a33a18cb --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/examples/with-i18n.tsx @@ -0,0 +1,15 @@ +import { parseDate } from '@internationalized/date'; +import { DateField, Flex, I18nProvider } from '@godaddy/antares'; + +export function DateFieldWithI18nExample() { + return ( + + + + + + + + + ); +} diff --git a/packages/@godaddy/antares/components/date-field/src/index.module.css b/packages/@godaddy/antares/components/date-field/src/index.module.css new file mode 100644 index 000000000..abec2e241 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/src/index.module.css @@ -0,0 +1,28 @@ +.dateInput { + min-width: 0; +} + +.segment { + padding-inline: 1px; + font-variant-numeric: tabular-nums; + text-align: end; + outline: none; + border-radius: 2px; + + &[data-type="literal"] { + padding-inline: 0; + } + + &[data-placeholder] { + color: color-mix(in oklch, currentColor, transparent 40%); + } + + &[data-focused] { + background: color-mix(in oklch, Highlight, transparent 80%); + color: inherit; + } + + &[data-invalid] { + color: var(--color-feedback-critical-strong, #ae1302); + } +} diff --git a/packages/@godaddy/antares/components/date-field/src/index.tsx b/packages/@godaddy/antares/components/date-field/src/index.tsx new file mode 100644 index 000000000..af96e58c6 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/src/index.tsx @@ -0,0 +1,104 @@ +import type { CalendarDate } from '@internationalized/date'; +import { + DateField as RACDateField, + DateInput as RACDateInput, + DateSegment as RACDateSegment, + type DateFieldProps as RACDateFieldProps +} from 'react-aria-components'; +import { FieldFrame, type FieldFrameProps } from '#components/_internal/field-frame'; +import { Flex } from '#components/layout/flex'; +import styles from './index.module.css'; + +/** + * Extended props for the DateField component. + * Typed for `CalendarDate` (date-only, no time, no timezone). + * Extends RAC DateFieldProps. + */ +export interface DateFieldProps + extends Omit, 'children' | 'errorMessage'>, + Pick { + /** Selected date value (controlled). */ + value?: CalendarDate | null; + + /** Default date value (uncontrolled). */ + defaultValue?: CalendarDate | null; + + /** Absolute lower bound. Disables out-of-range typed entry. */ + minValue?: CalendarDate; + + /** Absolute upper bound. Disables out-of-range typed entry. */ + maxValue?: CalendarDate; + + /** Placeholder date used to hint the segments before a value is entered. */ + placeholderValue?: CalendarDate; + + /** Helper text shown below the frame. */ + description?: string; + + /** Error message shown when invalid. Use with `isInvalid`. */ + errorMessage?: string; + + /** Label text shown above the frame. */ + label?: string; + + /** Whether the input is disabled. */ + isDisabled?: boolean; + + /** Whether the value is invalid. Use with `errorMessage`. */ + isInvalid?: boolean; + + /** Whether user input is required before form submission. */ + isRequired?: boolean; + + /** Whether the input is read-only. */ + isReadOnly?: boolean; + + /** Name of the underlying form element. */ + name?: string; + + /** Handler called when the value changes. */ + onChange?: RACDateFieldProps['onChange']; +} + +/** + * Segmented date input with editable Year/Month/Day segments, typed for `CalendarDate` + * (date-only, no time, no timezone). Wraps RAC `DateField` in antares `FieldFrame`. + * + * @param props - {@link DateFieldProps} + * + * @example + * ```tsx + * import { parseDate } from '@internationalized/date'; + * + * + * ``` + */ +export function DateField(props: DateFieldProps) { + const { description, errorMessage, label, ...racProps } = props; + const { isDisabled, isRequired, isReadOnly } = racProps; + + return ( + + + + {(segment) => } + + + + ); +} diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/basic-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/basic-chromium-linux.png new file mode 100644 index 000000000..4cab48bea Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/basic-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/controlled-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/controlled-chromium-linux.png new file mode 100644 index 000000000..7311da1ce Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/controlled-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/default-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/default-chromium-linux.png new file mode 100644 index 000000000..af0b3aba4 Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/default-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/disabled-required-readonly-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/disabled-required-readonly-chromium-linux.png new file mode 100644 index 000000000..5348f2938 Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/disabled-required-readonly-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/form-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/form-chromium-linux.png new file mode 100644 index 000000000..5e1e61c7d Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/form-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/min-max-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/min-max-chromium-linux.png new file mode 100644 index 000000000..639d1ae2e Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/min-max-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-description-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-description-chromium-linux.png new file mode 100644 index 000000000..e89f98a73 Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-description-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-error-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-error-chromium-linux.png new file mode 100644 index 000000000..9599be79d Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-error-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-i18n-chromium-linux.png b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-i18n-chromium-linux.png new file mode 100644 index 000000000..4efe55930 Binary files /dev/null and b/packages/@godaddy/antares/components/date-field/test/__screenshots__/date-field.visual.test.tsx/with-i18n-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/date-field/test/__snapshots__/date-field.node.test.tsx.snap b/packages/@godaddy/antares/components/date-field/test/__snapshots__/date-field.node.test.tsx.snap new file mode 100644 index 000000000..bb3c5d72a --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/test/__snapshots__/date-field.node.test.tsx.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`@godaddy/antares > #DateField > #examples > renders basic example 1`] = `"
Start date
mmddyyyy
"`; + +exports[`@godaddy/antares > #DateField > #examples > renders controlled example 1`] = `"
Start date
3152024
Value: 2024-03-15"`; + +exports[`@godaddy/antares > #DateField > #examples > renders min-max example 1`] = `"
Booking date
6152024
Must fall within 2024.
"`; + +exports[`@godaddy/antares > #DateField > #examples > renders with-default-value example 1`] = `"
Start date
3152024
"`; diff --git a/packages/@godaddy/antares/components/date-field/test/date-field.browser.test.tsx b/packages/@godaddy/antares/components/date-field/test/date-field.browser.test.tsx new file mode 100644 index 000000000..78aac3c2c --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/test/date-field.browser.test.tsx @@ -0,0 +1,101 @@ +import assume from 'assume'; +import { describe, it } from 'vitest'; +import { render } from 'vitest-browser-react'; +import { userEvent } from 'vitest/browser'; +import { DateFieldBasicExample } from '../examples/basic.tsx'; +import { DateFieldControlledExample } from '../examples/controlled.tsx'; +import { DateFieldDisabledRequiredReadOnlyExample } from '../examples/disabled-required-readonly.tsx'; +import { DateFieldFormExample } from '../examples/form.tsx'; +import { DateFieldWithDefaultValueExample } from '../examples/with-default-value.tsx'; +import { DateFieldWithErrorExample } from '../examples/with-error.tsx'; +import { withTestLocale } from './locale.tsx'; + +describe('@godaddy/antares', function antares() { + describe('#DateField', function dateField() { + describe('#basic', function basic() { + it('renders label and editable segments', async function renders() { + const { locator, container } = await render(withTestLocale()); + + assume(locator.getByText('Start date').element()).exists(); + assume(container.querySelectorAll('[data-type="month"]').length).is.at.least(1); + assume(container.querySelectorAll('[data-type="day"]').length).is.at.least(1); + assume(container.querySelectorAll('[data-type="year"]').length).is.at.least(1); + }); + }); + + describe('#withDefaultValue', function withDefaultValue() { + it('populates segments from a CalendarDate default', async function defaults() { + const { container } = await render(withTestLocale()); + const text = container.textContent ?? ''; + + assume(text).contains('2024'); + assume(text).contains('15'); + }); + }); + + describe('#controlled', function controlled() { + it('shows the controlled value as ISO string', async function shows() { + const { locator } = await render(withTestLocale()); + + assume(locator.getByText(/2024-03-15/).element()).exists(); + }); + }); + + describe('#withError', function withError() { + it('renders error message and data-invalid', async function invalid() { + const { locator, container } = await render(withTestLocale()); + + assume(locator.getByText('Please enter a valid date.').element()).exists(); + assume(container.querySelector('[data-invalid="true"]')).exists(); + }); + }); + + describe('#disabledRequiredReadonly', function disabledRequiredReadonly() { + it('renders disabled, required, and read-only states', async function states() { + const { container } = await render(withTestLocale()); + + assume(container.querySelector('[data-disabled="true"]')).exists(); + assume(container.querySelector('[data-readonly="true"]')).exists(); + assume(container.querySelector('[data-required="true"]')).exists(); + }); + }); + + describe('#keyboard', function keyboard() { + it('arrow up increments the focused segment', async function arrowUp() { + const { container } = await render(withTestLocale()); + const monthSegment = container.querySelector('[data-type="month"]'); + + assume(monthSegment).exists(); + monthSegment?.focus(); + await userEvent.keyboard('{ArrowUp}'); + + // Default month is 03 (March); after ArrowUp should read 4 (April). + assume(monthSegment?.textContent).contains('4'); + }); + }); + + describe('#form', function form() { + it('submits an ISO date string under the name prop', async function submits() { + const { container, locator } = await render(withTestLocale()); + const yearSegment = container.querySelector('[data-type="year"]'); + const monthSegment = container.querySelector('[data-type="month"]'); + const daySegment = container.querySelector('[data-type="day"]'); + + assume(yearSegment).exists(); + assume(monthSegment).exists(); + assume(daySegment).exists(); + + monthSegment?.focus(); + await userEvent.keyboard('03'); + daySegment?.focus(); + await userEvent.keyboard('15'); + yearSegment?.focus(); + await userEvent.keyboard('2024'); + + await locator.getByRole('button', { name: 'Submit' }).click(); + + assume(locator.getByText(/2024-03-15/).element()).exists(); + }); + }); + }); +}); diff --git a/packages/@godaddy/antares/components/date-field/test/date-field.node.test.tsx b/packages/@godaddy/antares/components/date-field/test/date-field.node.test.tsx new file mode 100644 index 000000000..b4308ae29 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/test/date-field.node.test.tsx @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { renderToString } from 'react-dom/server'; +import { DateFieldBasicExample } from '../examples/basic.tsx'; +import { DateFieldControlledExample } from '../examples/controlled.tsx'; +import { DateFieldDisabledRequiredReadOnlyExample } from '../examples/disabled-required-readonly.tsx'; +import { DateFieldMinMaxExample } from '../examples/min-max.tsx'; +import { DateFieldWithDefaultValueExample } from '../examples/with-default-value.tsx'; +import { DateFieldWithDescriptionExample } from '../examples/with-description.tsx'; +import { DateFieldWithErrorExample } from '../examples/with-error.tsx'; +import { withTestLocale } from './locale.tsx'; + +describe('@godaddy/antares', function antares() { + describe('#DateField', function dateField() { + describe('#examples', function examples() { + it('renders basic example', function basic() { + const result = renderToString(withTestLocale()); + expect(result).toMatchSnapshot(); + }); + + it('renders with-default-value example', function withDefault() { + const result = renderToString(withTestLocale()); + expect(result).toContain('2024'); + expect(result).toMatchSnapshot(); + }); + + it('renders controlled example', function controlled() { + const result = renderToString(withTestLocale()); + expect(result).toMatchSnapshot(); + }); + + it('renders with-description example', function withDescription() { + const result = renderToString(withTestLocale()); + expect(result).toContain('subscription begins'); + }); + + it('renders with-error example', function withError() { + const result = renderToString(withTestLocale()); + expect(result).toContain('data-invalid'); + }); + + it('renders min-max example', function minMax() { + const result = renderToString(withTestLocale()); + expect(result).toMatchSnapshot(); + }); + + it('renders disabled-required-readonly example', function dro() { + const result = renderToString(withTestLocale()); + expect(result).toContain('data-disabled'); + expect(result).toContain('data-readonly'); + }); + }); + }); +}); diff --git a/packages/@godaddy/antares/components/date-field/test/date-field.visual.test.tsx b/packages/@godaddy/antares/components/date-field/test/date-field.visual.test.tsx new file mode 100644 index 000000000..25d554875 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/test/date-field.visual.test.tsx @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import { render } from 'vitest-browser-react'; +import { DateFieldBasicExample } from '../examples/basic.tsx'; +import { DateFieldControlledExample } from '../examples/controlled.tsx'; +import { DateFieldDisabledRequiredReadOnlyExample } from '../examples/disabled-required-readonly.tsx'; +import { DateFieldFormExample } from '../examples/form.tsx'; +import { DateFieldMinMaxExample } from '../examples/min-max.tsx'; +import { DateFieldWithDefaultValueExample } from '../examples/with-default-value.tsx'; +import { DateFieldWithDescriptionExample } from '../examples/with-description.tsx'; +import { DateFieldWithErrorExample } from '../examples/with-error.tsx'; +import { DateFieldWithI18nExample } from '../examples/with-i18n.tsx'; +import { withTestLocale } from './locale.tsx'; + +describe('@godaddy/antares', function antares() { + describe('#DateField', function dateFieldTests() { + it('basic example', async function basicRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('basic'); + }); + + it('controlled example', async function controlledRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('controlled'); + }); + + it('default example', async function defaultRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('default'); + }); + + it('with-description example', async function withDescriptionRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('with-description'); + }); + + it('with-error example', async function withErrorRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('with-error'); + }); + + it('min-max example', async function minMaxRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('min-max'); + }); + + it('disabled-required-readonly example', async function disabledRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('disabled-required-readonly'); + }); + + it('form example', async function formRender() { + const { container } = await render(withTestLocale()); + await expect(container).toMatchScreenshot('form'); + }); + + it('with-i18n example', async function withI18nRender() { + const { container } = await render(); + await expect(container).toMatchScreenshot('with-i18n'); + }); + }); +}); diff --git a/packages/@godaddy/antares/components/date-field/test/locale.tsx b/packages/@godaddy/antares/components/date-field/test/locale.tsx new file mode 100644 index 000000000..884de2850 --- /dev/null +++ b/packages/@godaddy/antares/components/date-field/test/locale.tsx @@ -0,0 +1,14 @@ +import type { ReactElement } from 'react'; +import { I18nProvider } from '@godaddy/antares'; + +/** + * Locale used to lock segment ordering, placeholder text, and numeral system + * for tests. Without this, RAC resolves the host runner's default locale, which + * varies across CI machines and produces flaky assertions/snapshots/screenshots. + */ +export const TEST_LOCALE = 'en-US'; + +/** Wraps a node in `` for deterministic tests. */ +export function withTestLocale(node: ReactElement): ReactElement { + return {node}; +} diff --git a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/basic-chromium-linux.png b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/basic-chromium-linux.png index ccb9b16e9..f95a873d2 100644 Binary files a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/basic-chromium-linux.png and b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/basic-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/controlled-chromium-linux.png b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/controlled-chromium-linux.png index 6129ca14d..0d2ce33ee 100644 Binary files a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/controlled-chromium-linux.png and b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/controlled-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/disabled-chromium-linux.png b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/disabled-chromium-linux.png index 02739abed..8c7f80efc 100644 Binary files a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/disabled-chromium-linux.png and b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/disabled-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/format-options-chromium-linux.png b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/format-options-chromium-linux.png index 104fd01ed..af8f8efdb 100644 Binary files a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/format-options-chromium-linux.png and b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/format-options-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/hide-stepper-chromium-linux.png b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/hide-stepper-chromium-linux.png index bd914f402..21ce73621 100644 Binary files a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/hide-stepper-chromium-linux.png and b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/hide-stepper-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/invalid-chromium-linux.png b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/invalid-chromium-linux.png index ab5ea1809..92807f3f7 100644 Binary files a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/invalid-chromium-linux.png and b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/invalid-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/value-scale-chromium-linux.png b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/value-scale-chromium-linux.png index ddc5e5fc6..87ce0ff22 100644 Binary files a/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/value-scale-chromium-linux.png and b/packages/@godaddy/antares/components/number-field/test/__screenshots__/number-field.visual.test.tsx/value-scale-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/adornments-chromium-linux.png b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/adornments-chromium-linux.png index 3d39ec8dd..fae6f3ae0 100644 Binary files a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/adornments-chromium-linux.png and b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/adornments-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/basic-chromium-linux.png b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/basic-chromium-linux.png index 0b5307159..7f08bc3c3 100644 Binary files a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/basic-chromium-linux.png and b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/basic-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/controlled-chromium-linux.png b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/controlled-chromium-linux.png index d38882e59..bd598c54d 100644 Binary files a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/controlled-chromium-linux.png and b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/controlled-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/disabled-chromium-linux.png b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/disabled-chromium-linux.png index ce95078bb..d74695575 100644 Binary files a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/disabled-chromium-linux.png and b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/disabled-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/invalid-chromium-linux.png b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/invalid-chromium-linux.png index 4151c148c..2a7f120b9 100644 Binary files a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/invalid-chromium-linux.png and b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/invalid-chromium-linux.png differ diff --git a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/multiline-chromium-linux.png b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/multiline-chromium-linux.png index 78395ef4d..b4109bb61 100644 Binary files a/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/multiline-chromium-linux.png and b/packages/@godaddy/antares/components/text-field/test/__screenshots__/text-field.visual.test.tsx/multiline-chromium-linux.png differ diff --git a/packages/@godaddy/antares/exports/DateField.ts b/packages/@godaddy/antares/exports/DateField.ts new file mode 100644 index 000000000..6028f6dd7 --- /dev/null +++ b/packages/@godaddy/antares/exports/DateField.ts @@ -0,0 +1 @@ +export { DateField, type DateFieldProps } from '#components/date-field'; diff --git a/packages/@godaddy/antares/exports/I18nProvider.ts b/packages/@godaddy/antares/exports/I18nProvider.ts new file mode 100644 index 000000000..087f38bce --- /dev/null +++ b/packages/@godaddy/antares/exports/I18nProvider.ts @@ -0,0 +1 @@ +export { I18nProvider, type I18nProviderProps } from 'react-aria-components'; diff --git a/packages/@godaddy/antares/index.ts b/packages/@godaddy/antares/index.ts index e5f084c7d..125bfd1aa 100644 --- a/packages/@godaddy/antares/index.ts +++ b/packages/@godaddy/antares/index.ts @@ -61,6 +61,10 @@ export { TextField, type TextFieldProps } from '#components/text-field'; export { NumberField, type NumberFieldProps } from '#components/number-field'; +export { DateField, type DateFieldProps } from '#components/date-field'; + +export { I18nProvider, type I18nProviderProps } from 'react-aria-components'; + export { Carousel, type CarouselProps, type CarouselRef } from '#components/carousel'; export { Pagination, type PaginationProps } from '#components/pagination'; diff --git a/packages/@godaddy/antares/package.json b/packages/@godaddy/antares/package.json index 37e05e1da..23a240e4a 100644 --- a/packages/@godaddy/antares/package.json +++ b/packages/@godaddy/antares/package.json @@ -43,6 +43,7 @@ "@bento/icon": "^0.2.0", "@bento/svg-parser": "^0.1.1", "@bento/use-data-attributes": "^0.1.1", + "@internationalized/date": "^3.12.2", "@react-aria/focus": "^3.22.0", "@react-aria/utils": "^3.34.0", "@react-stately/utils": "^3.12.0",