From ccec68efafa02d5f9ac9bd4abea6c5e7cb72d556 Mon Sep 17 00:00:00 2001 From: Niels Roozemond Date: Fri, 19 Apr 2024 16:59:33 +0200 Subject: [PATCH] feat: Time input component (#1202) Co-authored-by: Aram <37216945+alimpens@users.noreply.github.com> --- packages/css/src/components/index.scss | 1 + .../css/src/components/time-input/README.md | 11 +++ .../src/components/time-input/time-input.scss | 79 +++++++++++++++++++ packages/react/src/TimeInput/README.md | 5 ++ .../react/src/TimeInput/TimeInput.test.tsx | 41 ++++++++++ packages/react/src/TimeInput/TimeInput.tsx | 18 +++++ packages/react/src/TimeInput/index.ts | 2 + packages/react/src/index.ts | 1 + .../src/components/ams/time-input.tokens.json | 45 +++++++++++ .../components/TimeInput/TimeInput.docs.mdx | 19 +++++ .../TimeInput/TimeInput.stories.tsx | 33 ++++++++ 11 files changed, 255 insertions(+) create mode 100644 packages/css/src/components/time-input/README.md create mode 100644 packages/css/src/components/time-input/time-input.scss create mode 100644 packages/react/src/TimeInput/README.md create mode 100644 packages/react/src/TimeInput/TimeInput.test.tsx create mode 100644 packages/react/src/TimeInput/TimeInput.tsx create mode 100644 packages/react/src/TimeInput/index.ts create mode 100644 proprietary/tokens/src/components/ams/time-input.tokens.json create mode 100644 storybook/src/components/TimeInput/TimeInput.docs.mdx create mode 100644 storybook/src/components/TimeInput/TimeInput.stories.tsx diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index bccc219e1e..0ca1ff3e8e 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,6 +4,7 @@ */ /* Append here */ +@import "./time-input/time-input"; @import "./date-input/date-input"; @import "./document/document"; @import "./avatar/avatar"; diff --git a/packages/css/src/components/time-input/README.md b/packages/css/src/components/time-input/README.md new file mode 100644 index 0000000000..bf50aff30c --- /dev/null +++ b/packages/css/src/components/time-input/README.md @@ -0,0 +1,11 @@ + + +# Time Input + +Helps users enter time. + +## Visual considerations + +This component uses a native time 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/time-input/time-input.scss b/packages/css/src/components/time-input/time-input.scss new file mode 100644 index 0000000000..fc721ca6bf --- /dev/null +++ b/packages/css/src/components/time-input/time-input.scss @@ -0,0 +1,79 @@ +/** + * @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; + width: auto; // Reset width of 10em set by Android devices +} + +.ams-time-input { + background-color: var(--ams-time-input-background-color); + box-shadow: var(--ams-time-input-box-shadow); + color: var(--ams-time-input-color); + font-family: var(--ams-time-input-font-family); + font-size: var(--ams-time-input-font-size); + font-weight: var(--ams-time-input-font-weight); + line-height: var(--ams-time-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-time-input-font-size) * var(--ams-time-input-line-height)) + 2 * var(--ams-time-input-padding-block) + ); + + // Set min width for iOS, so the width is closer to the filled in width. + min-width: calc(4ch + (2 * var(--ams-time-input-padding-inline))); + outline-offset: var(--ams-time-input-outline-offset); + padding-block: var(--ams-time-input-padding-block); + padding-inline: var(--ams-time-input-padding-inline); + touch-action: manipulation; + + @include text-rendering; + @include reset; + + &:hover { + box-shadow: var(--ams-time-input-hover-box-shadow); + } +} + +// This changes the default calendar icon on Chromium browsers only +.ams-time-input::-webkit-calendar-picker-indicator { + appearance: none; + background-image: var(--ams-time-input-calender-picker-indicator-background-image); + cursor: pointer; +} + +.ams-time-input:hover::-webkit-calendar-picker-indicator { + background-image: var(--ams-time-input-hover-calender-picker-indicator-background-image); +} + +.ams-time-input:disabled { + background-color: var(--ams-time-input-disabled-background-color); + box-shadow: var(--ams-time-input-disabled-box-shadow); + color: var(--ams-time-input-disabled-color); + cursor: not-allowed; +} + +.ams-time-input:disabled::-webkit-calendar-picker-indicator { + background-image: var(--ams-time-input-disabled-calender-picker-indicator-background-image); + visibility: visible; +} + +.ams-time-input:invalid, +.ams-time-input[aria-invalid="true"] { + box-shadow: var(--ams-time-input-invalid-box-shadow); + + &:hover { + // TODO: this should be the (currently non-existent) dark red hover color + box-shadow: var(--ams-time-input-invalid-hover-box-shadow); + } +} diff --git a/packages/react/src/TimeInput/README.md b/packages/react/src/TimeInput/README.md new file mode 100644 index 0000000000..4304acea01 --- /dev/null +++ b/packages/react/src/TimeInput/README.md @@ -0,0 +1,5 @@ + + +# React Time Input component + +[Time Input documentation](../../../css/src/components/time-input/README.md) diff --git a/packages/react/src/TimeInput/TimeInput.test.tsx b/packages/react/src/TimeInput/TimeInput.test.tsx new file mode 100644 index 0000000000..60843757d4 --- /dev/null +++ b/packages/react/src/TimeInput/TimeInput.test.tsx @@ -0,0 +1,41 @@ +import { render } from '@testing-library/react' +import { createRef } from 'react' +import { TimeInput } from './TimeInput' +import '@testing-library/jest-dom' + +describe('Time 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-time-input') + }) + + it('renders an additional class name', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-time-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/TimeInput/TimeInput.tsx b/packages/react/src/TimeInput/TimeInput.tsx new file mode 100644 index 0000000000..1eee477db8 --- /dev/null +++ b/packages/react/src/TimeInput/TimeInput.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 TimeInputProps = InputHTMLAttributes + +export const TimeInput = forwardRef( + ({ className, ...restProps }: TimeInputProps, ref: ForwardedRef) => ( + + ), +) + +TimeInput.displayName = 'TimeInput' diff --git a/packages/react/src/TimeInput/index.ts b/packages/react/src/TimeInput/index.ts new file mode 100644 index 0000000000..f49dd440b9 --- /dev/null +++ b/packages/react/src/TimeInput/index.ts @@ -0,0 +1,2 @@ +export { TimeInput } from './TimeInput' +export type { TimeInputProps } from './TimeInput' diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 33c068651e..fbdc4c7857 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ */ /* Append here */ +export * from './TimeInput' export * from './DateInput' export * from './Avatar' export * from './FormFieldCharacterCounter' diff --git a/proprietary/tokens/src/components/ams/time-input.tokens.json b/proprietary/tokens/src/components/ams/time-input.tokens.json new file mode 100644 index 0000000000..0c3c0a0433 --- /dev/null +++ b/proprietary/tokens/src/components/ams/time-input.tokens.json @@ -0,0 +1,45 @@ +{ + "ams": { + "time-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.md}" }, + "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/TimeInput/TimeInput.docs.mdx b/storybook/src/components/TimeInput/TimeInput.docs.mdx new file mode 100644 index 0000000000..7e2d03f56a --- /dev/null +++ b/storybook/src/components/TimeInput/TimeInput.docs.mdx @@ -0,0 +1,19 @@ +import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks"; +import * as TimeInputStories from "./TimeInput.stories.tsx"; +import README from "../../../../packages/css/src/components/time-input/README.md?raw"; + + + +{README} + + + + + +## Invalid + + + +## Disabled + + diff --git a/storybook/src/components/TimeInput/TimeInput.stories.tsx b/storybook/src/components/TimeInput/TimeInput.stories.tsx new file mode 100644 index 0000000000..190febc353 --- /dev/null +++ b/storybook/src/components/TimeInput/TimeInput.stories.tsx @@ -0,0 +1,33 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { TimeInput } from '@amsterdam/design-system-react/src' +import { Meta, StoryObj } from '@storybook/react' + +const meta = { + title: 'Components/Forms/Time Input', + component: TimeInput, + 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, + }, +}