From afbbef8bb1a3d13fa47638ce2856def0c028b13f Mon Sep 17 00:00:00 2001 From: Aram <37216945+alimpens@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:25:30 +0200 Subject: [PATCH] feat: Add date input (#1152) --- .../css/src/components/date-input/README.md | 11 +++ .../src/components/date-input/date-input.scss | 78 +++++++++++++++++++ packages/css/src/components/index.scss | 1 + .../react/src/DateInput/DateInput.test.tsx | 41 ++++++++++ packages/react/src/DateInput/DateInput.tsx | 18 +++++ packages/react/src/DateInput/README.md | 5 ++ packages/react/src/DateInput/index.ts | 2 + packages/react/src/index.ts | 1 + .../src/components/ams/date-input.tokens.json | 45 +++++++++++ .../components/DateInput/DateInput.docs.mdx | 19 +++++ .../DateInput/DateInput.stories.tsx | 35 +++++++++ 11 files changed, 256 insertions(+) create mode 100644 packages/css/src/components/date-input/README.md create mode 100644 packages/css/src/components/date-input/date-input.scss create mode 100644 packages/react/src/DateInput/DateInput.test.tsx create mode 100644 packages/react/src/DateInput/DateInput.tsx create mode 100644 packages/react/src/DateInput/README.md create mode 100644 packages/react/src/DateInput/index.ts create mode 100644 proprietary/tokens/src/components/ams/date-input.tokens.json create mode 100644 storybook/src/components/DateInput/DateInput.docs.mdx create mode 100644 storybook/src/components/DateInput/DateInput.stories.tsx diff --git a/packages/css/src/components/date-input/README.md b/packages/css/src/components/date-input/README.md new file mode 100644 index 0000000000..b4061163c7 --- /dev/null +++ b/packages/css/src/components/date-input/README.md @@ -0,0 +1,11 @@ + + +# Date Input + +Helps users enter a date. + +## Visual considerations + +This component uses a native date input, which is styled differently in different browsers and operating systems. +Recreating the native functionality is quite difficult and prone to accessibility errors, which is why we’ve chosen not to do that. +This does mean that this component does not look identical on all browsers and operating systems. diff --git a/packages/css/src/components/date-input/date-input.scss b/packages/css/src/components/date-input/date-input.scss new file mode 100644 index 0000000000..88522d76f0 --- /dev/null +++ b/packages/css/src/components/date-input/date-input.scss @@ -0,0 +1,78 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +@import "../../common/text-rendering"; + +@mixin reset { + // Reset native appearance, this causes issues on iOS and Android devices + -webkit-appearance: none; + appearance: none; + border: 0; + border-radius: 0; // Reset rounded borders on iOS devices + box-sizing: border-box; + margin-block: 0; +} + +.ams-date-input { + background-color: var(--ams-date-input-background-color); + box-shadow: var(--ams-date-input-box-shadow); + color: var(--ams-date-input-color); + font-family: var(--ams-date-input-font-family); + font-size: var(--ams-date-input-font-size); + font-weight: var(--ams-date-input-font-weight); + line-height: var(--ams-date-input-line-height); + + // Set min height for iOS, otherwise an empty box is a lot smaller than a filled one. + min-height: calc( + (var(--ams-date-input-font-size) * var(--ams-date-input-line-height)) + 2 * var(--ams-date-input-padding-block) + ); + + // Set min width for iOS, so the width is closer to the filled in width. + min-width: calc(8ch + (2 * var(--ams-date-input-padding-inline))); + outline-offset: var(--ams-date-input-outline-offset); + padding-block: var(--ams-date-input-padding-block); + padding-inline: var(--ams-date-input-padding-inline); + touch-action: manipulation; + + @include text-rendering; + @include reset; + + &:hover { + box-shadow: var(--ams-date-input-hover-box-shadow); + } +} + +// This changes the default calendar icon on Chromium browsers only +.ams-date-input::-webkit-calendar-picker-indicator { + appearance: none; + background-image: var(--ams-date-input-calender-picker-indicator-background-image); + cursor: pointer; +} + +.ams-date-input:hover::-webkit-calendar-picker-indicator { + background-image: var(--ams-date-input-hover-calender-picker-indicator-background-image); +} + +.ams-date-input:disabled { + background-color: var(--ams-date-input-disabled-background-color); + box-shadow: var(--ams-date-input-disabled-box-shadow); + color: var(--ams-date-input-disabled-color); + cursor: not-allowed; +} + +.ams-date-input:disabled::-webkit-calendar-picker-indicator { + background-image: var(--ams-date-input-disabled-calender-picker-indicator-background-image); + visibility: visible; +} + +.ams-date-input:invalid, +.ams-date-input[aria-invalid="true"] { + box-shadow: var(--ams-date-input-invalid-box-shadow); + + &:hover { + // TODO: this should be the (currently non-existent) dark red hover color + box-shadow: var(--ams-date-input-invalid-hover-box-shadow); + } +} diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index 800d1ae507..75a34eb3cd 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,6 +4,7 @@ */ /* Append here */ +@import "./date-input/date-input"; @import "./document/document"; @import "./avatar/avatar"; @import "./form-field-character-counter/form-field-character-counter"; diff --git a/packages/react/src/DateInput/DateInput.test.tsx b/packages/react/src/DateInput/DateInput.test.tsx new file mode 100644 index 0000000000..9c0f3a33da --- /dev/null +++ b/packages/react/src/DateInput/DateInput.test.tsx @@ -0,0 +1,41 @@ +import { render } from '@testing-library/react' +import { createRef } from 'react' +import { DateInput } from './DateInput' +import '@testing-library/jest-dom' + +describe('Date input', () => { + it('renders', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toBeInTheDocument() + expect(component).toBeVisible() + }) + + it('renders a design system BEM class name', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-date-input') + }) + + it('renders an additional class name', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-date-input extra') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(ref.current).toBe(component) + }) +}) diff --git a/packages/react/src/DateInput/DateInput.tsx b/packages/react/src/DateInput/DateInput.tsx new file mode 100644 index 0000000000..6e1c119a9e --- /dev/null +++ b/packages/react/src/DateInput/DateInput.tsx @@ -0,0 +1,18 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import clsx from 'clsx' +import { forwardRef } from 'react' +import type { ForwardedRef, InputHTMLAttributes } from 'react' + +export type DateInputProps = InputHTMLAttributes + +export const DateInput = forwardRef( + ({ className, ...restProps }: DateInputProps, ref: ForwardedRef) => ( + + ), +) + +DateInput.displayName = 'DateInput' diff --git a/packages/react/src/DateInput/README.md b/packages/react/src/DateInput/README.md new file mode 100644 index 0000000000..b61087b76e --- /dev/null +++ b/packages/react/src/DateInput/README.md @@ -0,0 +1,5 @@ + + +# React Date Input component + +[Date Input documentation](../../../css/src/components/date-input/README.md) diff --git a/packages/react/src/DateInput/index.ts b/packages/react/src/DateInput/index.ts new file mode 100644 index 0000000000..2303f03937 --- /dev/null +++ b/packages/react/src/DateInput/index.ts @@ -0,0 +1,2 @@ +export { DateInput } from './DateInput' +export type { DateInputProps } from './DateInput' diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index a71afe61bc..509e46d365 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ */ /* Append here */ +export * from './DateInput' export * from './Avatar' export * from './FormFieldCharacterCounter' export * from './DescriptionList' diff --git a/proprietary/tokens/src/components/ams/date-input.tokens.json b/proprietary/tokens/src/components/ams/date-input.tokens.json new file mode 100644 index 0000000000..d119b52a57 --- /dev/null +++ b/proprietary/tokens/src/components/ams/date-input.tokens.json @@ -0,0 +1,45 @@ +{ + "ams": { + "date-input": { + "background-color": { "value": "{ams.color.primary-white}" }, + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.sm} {ams.color.primary-black}" }, + "color": { "value": "{ams.color.primary-black}" }, + "font-family": { "value": "{ams.text.font-family}" }, + "font-size": { "value": "{ams.text.level.5.font-size}" }, + "font-weight": { "value": "{ams.text.font-weight.normal}" }, + "line-height": { "value": "{ams.text.level.5.line-height}" }, + "outline-offset": { "value": "{ams.focus.outline-offset}" }, + "padding-block": { "value": "{ams.space.inside.xs}" }, + "padding-inline": { "value": "{ams.space.inside.lg}" }, + "calender-picker-indicator": { + "background-image": { + "value": "url(\"data:image/svg+xml;utf8,\")" + } + }, + "disabled": { + "background-color": { "value": "{ams.color.primary-white}" }, + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.sm} {ams.color.neutral-grey2}" }, + "color": { "value": "{ams.color.neutral-grey2}" }, + "calender-picker-indicator": { + "background-image": { + "value": "url(\"data:image/svg+xml;utf8,\")" + } + } + }, + "hover": { + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-black}" }, + "calender-picker-indicator": { + "background-image": { + "value": "url(\"data:image/svg+xml;utf8,\")" + } + } + }, + "invalid": { + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.sm} {ams.color.primary-red}" }, + "hover": { + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-red}" } + } + } + } + } +} diff --git a/storybook/src/components/DateInput/DateInput.docs.mdx b/storybook/src/components/DateInput/DateInput.docs.mdx new file mode 100644 index 0000000000..6d734c27d5 --- /dev/null +++ b/storybook/src/components/DateInput/DateInput.docs.mdx @@ -0,0 +1,19 @@ +import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks"; +import * as DateInputStories from "./DateInput.stories.tsx"; +import README from "../../../../packages/css/src/components/date-input/README.md?raw"; + + + +{README} + + + + + +## Invalid + + + +## Disabled + + diff --git a/storybook/src/components/DateInput/DateInput.stories.tsx b/storybook/src/components/DateInput/DateInput.stories.tsx new file mode 100644 index 0000000000..43018e8c1d --- /dev/null +++ b/storybook/src/components/DateInput/DateInput.stories.tsx @@ -0,0 +1,35 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { DateInput, DateInputProps } from '@amsterdam/design-system-react' +import { Meta, StoryObj } from '@storybook/react' + +type StoryProps = DateInputProps & { invalid?: boolean } + +const meta = { + title: 'Components/Forms/Date Input', + component: DateInput, + args: { + disabled: false, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = {} + +export const Invalid: Story = { + args: { + required: true, + }, +} + +export const Disabled: Story = { + args: { + disabled: true, + }, +}