Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add date input #1152

Merged
merged 16 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/css/src/components/date-input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- @license CC0-1.0 -->

# 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.
78 changes: 78 additions & 0 deletions packages/css/src/components/date-input/date-input.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
41 changes: 41 additions & 0 deletions packages/react/src/DateInput/DateInput.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<DateInput />)

const component = container.querySelector(':only-child')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
const { container } = render(<DateInput />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-date-input')
})

it('renders an additional class name', () => {
const { container } = render(<DateInput className="extra" />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-date-input extra')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLInputElement>()

const { container } = render(<DateInput ref={ref} />)

const component = container.querySelector(':only-child')

expect(ref.current).toBe(component)
})
})
18 changes: 18 additions & 0 deletions packages/react/src/DateInput/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>

export const DateInput = forwardRef(
({ className, ...restProps }: DateInputProps, ref: ForwardedRef<HTMLInputElement>) => (
<input {...restProps} ref={ref} className={clsx('ams-date-input', className)} type="date" />
),
)

DateInput.displayName = 'DateInput'
5 changes: 5 additions & 0 deletions packages/react/src/DateInput/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# React Date Input component

[Date Input documentation](../../../css/src/components/date-input/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/DateInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DateInput } from './DateInput'
export type { DateInputProps } from './DateInput'
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
export * from './DateInput'
export * from './Avatar'
export * from './FormFieldCharacterCounter'
export * from './DescriptionList'
Expand Down
45 changes: 45 additions & 0 deletions proprietary/tokens/src/components/ams/date-input.tokens.json
Original file line number Diff line number Diff line change
@@ -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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36' fill='%23004699'><path d='M28 6V2h-4v4H12V2H8v4H2v28h32V6zm2 24H6V14h24z'/><path d='M10 17h4v4h-4zm6 0h4v4h-4zm6 0h4v4h-4zm-12 6h4v4h-4zm6 0h4v4h-4z'/></svg>\")"
}
},
"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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36' fill='%23BEBEBE'><path d='M28 6V2h-4v4H12V2H8v4H2v28h32V6zm2 24H6V14h24z'/><path d='M10 17h4v4h-4zm6 0h4v4h-4zm6 0h4v4h-4zm-12 6h4v4h-4zm6 0h4v4h-4z'/></svg>\")"
}
}
},
"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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36' fill='%23102E62'><path d='M28 6V2h-4v4H12V2H8v4H2v28h32V6zm2 24H6V14h24z'/><path d='M10 17h4v4h-4zm6 0h4v4h-4zm6 0h4v4h-4zm-12 6h4v4h-4zm6 0h4v4h-4z'/></svg>\")"
}
}
},
"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}" }
}
}
}
}
}
19 changes: 19 additions & 0 deletions storybook/src/components/DateInput/DateInput.docs.mdx
Original file line number Diff line number Diff line change
@@ -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";

<Meta of={DateInputStories} />

<Markdown>{README}</Markdown>

<Primary />

<Controls />

## Invalid

<Canvas of={DateInputStories.Invalid} />

## Disabled

<Canvas of={DateInputStories.Disabled} />
35 changes: 35 additions & 0 deletions storybook/src/components/DateInput/DateInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<StoryProps>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const Invalid: Story = {
args: {
required: true,
},
}

export const Disabled: Story = {
args: {
disabled: true,
},
}
Loading