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,
+ },
+}