From a953fc6b25c817b179a3683cf5effaacef654e50 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 27 Jun 2023 15:13:39 +0200 Subject: [PATCH 1/7] :sparkles: [#433] -- Implement an input group component The generic input group component can display related inputs inline, with (presumably) accessible markup using the fieldset element. It is required for the input-group variant/widget of the date field. --- src/components/forms/InputGroup/InputGroup.js | 57 ++++++++++++ .../forms/InputGroup/InputGroup.mdx | 56 +++++++++++ .../forms/InputGroup/InputGroup.scss | 37 ++++++++ .../forms/InputGroup/InputGroup.stories.js | 92 +++++++++++++++++++ src/components/forms/InputGroup/index.js | 1 + src/components/forms/Label.js | 51 ++++++---- src/components/forms/index.js | 1 + src/scss/nl-design-system-community.scss | 1 + 8 files changed, 277 insertions(+), 19 deletions(-) create mode 100644 src/components/forms/InputGroup/InputGroup.js create mode 100644 src/components/forms/InputGroup/InputGroup.mdx create mode 100644 src/components/forms/InputGroup/InputGroup.scss create mode 100644 src/components/forms/InputGroup/InputGroup.stories.js create mode 100644 src/components/forms/InputGroup/index.js diff --git a/src/components/forms/InputGroup/InputGroup.js b/src/components/forms/InputGroup/InputGroup.js new file mode 100644 index 000000000..8295079ea --- /dev/null +++ b/src/components/forms/InputGroup/InputGroup.js @@ -0,0 +1,57 @@ +import {Fieldset, FieldsetLegend, Paragraph} from '@utrecht/component-library-react'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import {LabelContent} from 'components/forms/Label'; + +import './InputGroup.scss'; + +export const InputGroup = ({ + children, + label, + isRequired = true, + disabled = false, + invalid = false, +}) => ( +
+ + + {label} + + + {children} +
+); + +InputGroup.propTypes = { + children: PropTypes.node, + label: PropTypes.node, + isRequired: PropTypes.bool, + disabled: PropTypes.bool, + invalid: PropTypes.bool, +}; + +export const InputGroupItem = ({children, component: Component = 'span'}) => ( + {children} +); + +InputGroupItem.propTypes = { + /** + * Provided children are displayed with flexbox (row), and their spacing is applied + * with CSS and design tokens. + */ + children: PropTypes.node, + /** + * Specify the wrapper component to render for each individual item. + * + * You can pass a string for the HTML node to render (span by default), or a React + * component type. + * + * * It must be able to take the `className` prop, like normal DOM elements do. + * * It must be limited to inline content, as their parent is a `

` element. + * + */ + component: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), +}; + +export default InputGroup; diff --git a/src/components/forms/InputGroup/InputGroup.mdx b/src/components/forms/InputGroup/InputGroup.mdx new file mode 100644 index 000000000..a8a86e865 --- /dev/null +++ b/src/components/forms/InputGroup/InputGroup.mdx @@ -0,0 +1,56 @@ +import ofTokens from '@open-formulieren/design-tokens/dist/tokens'; +import {ArgTypes, Canvas, Meta} from '@storybook/blocks'; +import TokensTable from 'design-token-editor/lib/esm/TokensTable'; + +import InputGroup, {InputGroupItem} from './InputGroup'; +import * as InputGroupStories from './InputGroup.stories'; + + + +# Input group + +An input group is a set of closely related inputs that should be considered a single value, for +example: + +- parts of a date +- date + time +- parts of a duration + + + +## Usage + +The input group is a low level component, intended to replace a regular `Textbox` or `Select` +component. + +It should be composed with the `Wrapper`, `FormField`, `FormLabel`, `HelpText` and +`ValidationErrors` components. + +## Props + + + +**InputGroupItem** + + + +## Design tokens + +We use the `Fieldset` component from `@utrecht/component-library-react`, and as such those +[design tokens](https://nl-design-system.github.io/utrecht/storybook/?path=/docs/css-component-form-fieldset-design-tokens--docs) +apply. + +Additionally Open Forms defines some design tokens to handle the invalid state. We recommend setting +the following values to prevent double 'error borders'. + +```css +--of-input-group-invalid-border-inline-start-color: transparent; +--of-input-group-invalid-border-inline-start-width: 0; +--of-input-group-invalid-padding-inline-start: 0; +``` + + + +## References + +- [NL Design System](https://nl-design-system.github.io/utrecht/storybook/?path=/docs/css-component-form-fieldset-readme--docs) diff --git a/src/components/forms/InputGroup/InputGroup.scss b/src/components/forms/InputGroup/InputGroup.scss new file mode 100644 index 000000000..03ca38a1a --- /dev/null +++ b/src/components/forms/InputGroup/InputGroup.scss @@ -0,0 +1,37 @@ +@use 'microscope-sass/lib/bem'; + +.openforms-input-group { + display: flex; + justify-content: var(--of-input-group-justify-content); + align-items: var(--of-input-group-align-items); + gap: var(--of-input-group-gap); + + @include bem.element('item') { + display: flex; + flex-direction: column; + gap: var(--of-input-group-item-gap); + } + + @include bem.element('label') { + font-size: var(--of-input-group-label-font-size); + } +} + +// XXX: move to upstream @utrecht/components when the input group is moved there. +.utrecht-form-field { + @include bem.modifier('openforms') { + .utrecht-form-fieldset { + @include bem.modifier('invalid') { + --utrecht-form-fieldset-invalid-border-inline-start-color: var( + --of-input-group-invalid-border-inline-start-color + ); + --utrecht-form-fieldset-invalid-border-inline-start-width: var( + --of-input-group-invalid-border-inline-start-width + ); + --utrecht-form-fieldset-invalid-padding-inline-start: var( + --of-input-group-invalid-padding-inline-start + ); + } + } + } +} diff --git a/src/components/forms/InputGroup/InputGroup.stories.js b/src/components/forms/InputGroup/InputGroup.stories.js new file mode 100644 index 000000000..b26980558 --- /dev/null +++ b/src/components/forms/InputGroup/InputGroup.stories.js @@ -0,0 +1,92 @@ +import {FormField, FormLabel, Textbox} from '@utrecht/component-library-react'; +import {Field} from 'formik'; + +import {SelectField, TextField, Wrapper} from 'components/forms'; +import {FormikDecorator} from 'story-utils/decorators'; + +import {InputGroup, InputGroupItem} from './InputGroup'; + +export default { + title: 'Pure React Components / Forms / Low level / Input group', + component: InputGroup, + decorators: [FormikDecorator], + argTypes: { + children: {control: false}, + }, +}; + +export const Basic = { + render: args => ( + + + + Sub 1 + + + + + + Sub 2 + + + + + ), + args: { + label: 'Group related inputs', + isRequired: false, + disabled: false, + invalid: false, + }, +}; + +export const InLargerForm = { + render: args => ( + <> + + + + + + + + Sub 1 + + + + + + Sub 2 + + + + + + + + + + ), + args: { + label: 'Group related inputs', + isRequired: false, + disabled: false, + invalid: false, + }, + parameters: { + formik: { + initialValues: { + before: '', + inputGroup: ['Jane', 'Doe'], + after: 'opt1', + }, + }, + }, +}; diff --git a/src/components/forms/InputGroup/index.js b/src/components/forms/InputGroup/index.js new file mode 100644 index 000000000..27808922a --- /dev/null +++ b/src/components/forms/InputGroup/index.js @@ -0,0 +1 @@ +export {InputGroup, InputGroupItem} from './InputGroup'; diff --git a/src/components/forms/Label.js b/src/components/forms/Label.js index 88c7b4888..3577804a2 100644 --- a/src/components/forms/Label.js +++ b/src/components/forms/Label.js @@ -6,30 +6,43 @@ import {FormattedMessage} from 'react-intl'; import {ConfigContext} from 'Context'; -const Label = ({id, isRequired = false, disabled = false, children}) => { +export const LabelContent = ({id, disabled = false, isRequired = false, children}) => { const {requiredFieldsWithAsterisk} = useContext(ConfigContext); return ( - - - children, - }} - /> - - + + children, + }} + /> + ); }; +LabelContent.propTypes = { + id: PropTypes.string, + isRequired: PropTypes.bool, + disabled: PropTypes.bool, + children: PropTypes.node, +}; + +const Label = ({id, isRequired = false, disabled = false, children}) => ( + + + {children} + + +); + Label.propTypes = { id: PropTypes.string, isRequired: PropTypes.bool, diff --git a/src/components/forms/index.js b/src/components/forms/index.js index 69ff9e76e..2791ffbcb 100644 --- a/src/components/forms/index.js +++ b/src/components/forms/index.js @@ -9,5 +9,6 @@ export {default as TextField} from './TextField'; export * from './FloatingWidget'; export {default as Label} from './Label'; export {default as HelpText} from './HelpText'; +export {InputGroup, InputGroupItem} from './InputGroup'; export {default as ValidationErrors} from './ValidationErrors'; export {default as Wrapper} from './Wrapper'; diff --git a/src/scss/nl-design-system-community.scss b/src/scss/nl-design-system-community.scss index c3e4b9eff..f8c40bac3 100644 --- a/src/scss/nl-design-system-community.scss +++ b/src/scss/nl-design-system-community.scss @@ -1,3 +1,4 @@ @import '@utrecht/components/dist/document/css/index.css'; @import '@utrecht/components/dist/img/css/index.css'; @import '@utrecht/components/dist/paragraph/css/index.css'; +@import '@utrecht/components/dist/form-fieldset/css/index.css'; From 397872f5d0d5794f2da466e047f981fd50551802 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 27 Jun 2023 15:28:49 +0200 Subject: [PATCH 2/7] :arrow_up: Bump to design tokens 0.35.0 --- design-tokens | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/design-tokens b/design-tokens index 92e2227dc..e7c26cb5b 160000 --- a/design-tokens +++ b/design-tokens @@ -1 +1 @@ -Subproject commit 92e2227dcb9c8ac5cbeb402bd89cf105e9a8f178 +Subproject commit e7c26cb5ba58fae61c5c345322d893faca83314e diff --git a/package.json b/package.json index f7db4fb70..096b61105 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@floating-ui/react": "^0.24.2", "@formio/protected-eval": "^1.2.1", "@fortawesome/fontawesome-free": "^6.1.1", - "@open-formulieren/design-tokens": "^0.34.0", + "@open-formulieren/design-tokens": "^0.35.0", "@sentry/react": "^6.13.2", "@sentry/tracing": "^6.13.2", "@trivago/prettier-plugin-sort-imports": "^4.0.0", From f84a523014e6e44514e9b81978f06f506fad5aee Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 27 Jun 2023 15:32:14 +0200 Subject: [PATCH 3/7] :truck: [#433] -- Re-organize date field stories/implementation Split the stories and implementation by widget, tackling the datepicker first and preparing for the input group variant. --- .../forms/DateField/DateField-datepicker.mdx | 49 ++++++ ...ies.js => DateField-datepicker.stories.js} | 8 +- src/components/forms/DateField/DateField.js | 164 ++++-------------- src/components/forms/DateField/DateField.mdx | 56 ++---- src/components/forms/DateField/DatePicker.js | 116 +++++++++++++ src/components/forms/DateField/utils.js | 29 ++++ 6 files changed, 250 insertions(+), 172 deletions(-) create mode 100644 src/components/forms/DateField/DateField-datepicker.mdx rename src/components/forms/DateField/{DateField.stories.js => DateField-datepicker.stories.js} (96%) create mode 100644 src/components/forms/DateField/DatePicker.js create mode 100644 src/components/forms/DateField/utils.js diff --git a/src/components/forms/DateField/DateField-datepicker.mdx b/src/components/forms/DateField/DateField-datepicker.mdx new file mode 100644 index 000000000..39669bac2 --- /dev/null +++ b/src/components/forms/DateField/DateField-datepicker.mdx @@ -0,0 +1,49 @@ +import {ArgTypes, Canvas, Meta} from '@storybook/blocks'; + +import DateField from './DateField'; +import * as DatepickerStories from './DateField-datepicker.stories'; +import DatePicker from './DatePicker'; + + + +# DateField - datepicker + +## Date picker widget + +The date picker widget is suitable for nearby dates (in the future or past). + +A calendar component is appropriate because: + +- you can display occupied dates and/or date ranges +- you can indicate the current date +- it is not expected that the user needs to click the year/month button too many times + +We aim to make it accessible to assistive technology and keyboard-only navigation. + +### Known issues/enhancements: + +- Use arrow keys to navigate inside calendar +- Selecting a date in the next/previous month has some odd behaviour at the moment + +## Widget prop + +Use the prop `widget="datepicker"` to use this widget. + + + +## Props + + + +**DatePicker props** + +Internally, the `DateField` uses the `DatePicker` widget. + + + +## No asterisks + +The backend can be configured to treat fields as required by default and instead mark optional +fields explicitly. + + diff --git a/src/components/forms/DateField/DateField.stories.js b/src/components/forms/DateField/DateField-datepicker.stories.js similarity index 96% rename from src/components/forms/DateField/DateField.stories.js rename to src/components/forms/DateField/DateField-datepicker.stories.js index 9e1ab63e5..e83df856a 100644 --- a/src/components/forms/DateField/DateField.stories.js +++ b/src/components/forms/DateField/DateField-datepicker.stories.js @@ -7,9 +7,15 @@ import {ConfigDecorator, FormikDecorator} from 'story-utils/decorators'; import DateField from './DateField'; export default { - title: 'Pure React Components / Forms / DateField', + title: 'Pure React Components / Forms / DateField / Datepicker', component: DateField, decorators: [FormikDecorator], + args: { + widget: 'datepicker', + }, + argTypes: { + showFormattedDate: {table: {disable: true}}, + }, parameters: { formik: { initialValues: { diff --git a/src/components/forms/DateField/DateField.js b/src/components/forms/DateField/DateField.js index a5c8a029e..cbf29ca6a 100644 --- a/src/components/forms/DateField/DateField.js +++ b/src/components/forms/DateField/DateField.js @@ -1,126 +1,12 @@ -import {FormField, Paragraph, Textbox} from '@utrecht/component-library-react'; -import parse from 'date-fns/parse'; +import {FormField} from '@utrecht/component-library-react'; import {Field, useFormikContext} from 'formik'; import PropTypes from 'prop-types'; import React from 'react'; -import {useIntl} from 'react-intl'; -import FAIcon from 'components/FAIcon'; -import { - FloatingWidget, - HelpText, - Label, - ValidationErrors, - Wrapper, - useFloatingWidget, -} from 'components/forms'; -import {getBEMClassName} from 'utils'; +import {HelpText, ValidationErrors, Wrapper} from 'components/forms'; -import DatePickerCalendar from './DatePickerCalendar'; - -const parseDate = value => { - if (!value) return undefined; - const parsed = parse(value, 'yyyy-MM-dd', new Date()); - // Invalid Date is apparently a thing :ThisIsFine: - if (isNaN(parsed)) return undefined; - return parsed; -}; - -const DatePicker = ({name, onChange, id, calendarProps, ...extra}) => { - const intl = useIntl(); - const {getFieldProps} = useFormikContext(); - const { - refs, - floatingStyles, - context, - getFloatingProps, - getReferenceProps, - isOpen, - setIsOpen, - arrowRef, - } = useFloatingWidget(); - - const calendarIconClassName = getBEMClassName('datepicker-textbox__calendar-toggle'); - const {value} = getFieldProps(name); - const currentDate = parseDate(value); - - const {onFocus, ...referenceProps} = getReferenceProps(); - return ( - <> - - { - onFocus(event); - extra?.onFocus?.(event); - }} - {...referenceProps} - /> - !isOpen && setIsOpen(true)} - /> - - - { - // TODO: shouldn't this return a Date instance? -> question asked in nl-ds Slack - const truncated = selectedDate.substring(0, 10); - onChange({target: {name, value: truncated}}); - setIsOpen(false, {keepDismissed: true}); - }} - currentDate={currentDate} - {...calendarProps} - /> - - - ); -}; - -DatePicker.propTypes = { - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - id: PropTypes.string, - calendarProps: PropTypes.shape({ - minDate: PropTypes.instanceOf(Date), - maxDate: PropTypes.instanceOf(Date), - events: PropTypes.arrayOf( - PropTypes.shape({ - date: PropTypes.string, - emphasis: PropTypes.bool, - selected: PropTypes.bool, - disabled: PropTypes.bool, - }) - ), - }), -}; +import DateInputGroup from './DateInputGroup'; +import DatePicker from './DatePicker'; /** * Implements a form field to select dates. @@ -130,16 +16,7 @@ DatePicker.propTypes = { * calendar where a date can be selected using a pointer device. * * TODO: on mobile devices, use the native date picker? - * TODO: add prop/option to use a split date field for day/month/year? See - * https://design-system.service.gov.uk/patterns/dates/ * TODO: when typing in the value, use the pattern/mask approach like form.io? - * - * NL DS tickets: - * - https://github.com/nl-design-system/backlog/issues/189 - * - https://github.com/nl-design-system/backlog/issues/188 - * - https://github.com/nl-design-system/backlog/issues/35#issuecomment-1547704753 - * - * Other references: https://medium.com/samsung-internet-dev/making-input-type-date-complicated-a544fd27c45a */ const DateField = ({ name, @@ -148,9 +25,11 @@ const DateField = ({ description = '', id = '', disabled = false, + widget = 'inputGroup', minDate, maxDate, disabledDates = [], + showFormattedDate = false, }) => { const {getFieldMeta} = useFormikContext(); const {error} = getFieldMeta(name); @@ -166,19 +45,36 @@ const DateField = ({ disabled: true, })); + let Widget = DateInputGroup; + let fieldProps = {}; + switch (widget) { + case 'datepicker': { + Widget = DatePicker; + fieldProps = { + calendarProps: {minDate, maxDate, events: calendarEvents}, + }; + break; + } + default: + case 'inputGroup': { + Widget = DateInputGroup; + fieldProps = {showFormattedDate}; + break; + } + } + return ( - {description} @@ -187,6 +83,8 @@ const DateField = ({ ); }; +export const WIDGETS = ['datepicker', 'inputGroup']; + DateField.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.node, @@ -197,6 +95,8 @@ DateField.propTypes = { minDate: PropTypes.instanceOf(Date), maxDate: PropTypes.instanceOf(Date), disabledDates: PropTypes.arrayOf(PropTypes.string), + widget: PropTypes.oneOf(WIDGETS), + showFormattedDate: PropTypes.bool, }; export default DateField; diff --git a/src/components/forms/DateField/DateField.mdx b/src/components/forms/DateField/DateField.mdx index 771dc0f15..466043d6e 100644 --- a/src/components/forms/DateField/DateField.mdx +++ b/src/components/forms/DateField/DateField.mdx @@ -1,14 +1,14 @@ -import {ArgTypes, Canvas, Meta, Story} from '@storybook/blocks'; +import {Meta} from '@storybook/blocks'; -import * as DateFieldStories from './DateField.stories'; + - +# Date Field -## Date field +The date field supports multiple widgets. -We identify different types of date inputs, each coming with their most-appropriate UI element. +## Types of dates -**Nearby dates** +### Nearby dates Think of appointment dates, booking dates and similar. Here, the expected date is in the nearby future or past. A calendar component is appropriate because: @@ -17,7 +17,9 @@ future or past. A calendar component is appropriate because: - you can indicate the current date - it is not expected that the user needs to click the year/month button too many times -**Known dates (currently not supported)** +See the `Datepicker` stories for this. + +### Known dates Known dates are exact dates that the user typically knows of the top of their head, such as birth dates, credit card expiry dates... @@ -25,39 +27,15 @@ dates, credit card expiry dates... Calendars are not suitable due to the usually bad default/initial value. Preferably they are able to enter the numeric value for day, month and year, leveraging autocomplete as well. -On mobile devices, you'd typically want to use the native date widget too. - -### Datepicker/calendar - -The datepicker/calendar is suitable for nearby dates. We aim to make it accessible to assistive -technology and keyboard-only navigation. - -Known issues/enhancements: - -- Use arrow keys to navigate inside calendar -- Selecting a date in the next/previous month has some odd behaviour at the moment - - +On mobile devices, you'd typically want to use the native date widget too. TODO: is this statement +correct? - - - - -## Props - - +See the `Input group` stories for this. ## References -- [GOV.UK](https://design-system.service.gov.uk/patterns/dates/) -- [Blogpost](https://medium.com/samsung-internet-dev/making-input-type-date-complicated-a544fd27c45a) -- [nl-design-system/backlog#189](https://github.com/nl-design-system/backlog/issues/189) -- [nl-design-system/backlog#188](https://github.com/nl-design-system/backlog/issues/188) -- [nl-design-system/backlog#35](https://github.com/nl-design-system/backlog/issues/35#issuecomment-1547704753) - -## No asterisks - -The backend can be configured to treat fields as required by default and instead mark optional -fields explicitly. - - +- https://design-system.service.gov.uk/patterns/dates/ +- https://medium.com/samsung-internet-dev/making-input-type-date-complicated-a544fd27c45a +- https://github.com/nl-design-system/backlog/issues/189 +- https://github.com/nl-design-system/backlog/issues/188 +- https://github.com/nl-design-system/backlog/issues/35#issuecomment-1547704753 diff --git a/src/components/forms/DateField/DatePicker.js b/src/components/forms/DateField/DatePicker.js new file mode 100644 index 000000000..cbeee3797 --- /dev/null +++ b/src/components/forms/DateField/DatePicker.js @@ -0,0 +1,116 @@ +import {Paragraph, Textbox} from '@utrecht/component-library-react'; +import {useFormikContext} from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {useIntl} from 'react-intl'; + +import FAIcon from 'components/FAIcon'; +import {FloatingWidget, Label, useFloatingWidget} from 'components/forms'; +import {getBEMClassName} from 'utils'; + +import DatePickerCalendar from './DatePickerCalendar'; +import {parseDate} from './utils'; + +const DatePicker = ({name, label, isRequired, onChange, id, disabled, calendarProps, ...extra}) => { + const intl = useIntl(); + const {getFieldProps} = useFormikContext(); + const { + refs, + floatingStyles, + context, + getFloatingProps, + getReferenceProps, + isOpen, + setIsOpen, + arrowRef, + } = useFloatingWidget(); + + const calendarIconClassName = getBEMClassName('datepicker-textbox__calendar-toggle'); + const {value} = getFieldProps(name); + const currentDate = parseDate(value); + + const {onFocus, ...referenceProps} = getReferenceProps(); + return ( + <> + + + { + onFocus(event); + extra?.onFocus?.(event); + }} + {...referenceProps} + /> + !isOpen && setIsOpen(true)} + /> + + + { + // TODO: shouldn't this return a Date instance? -> question asked in nl-ds Slack + const truncated = selectedDate.substring(0, 10); + onChange({target: {name, value: truncated}}); + setIsOpen(false, {keepDismissed: true}); + }} + currentDate={currentDate} + {...calendarProps} + /> + + + ); +}; + +DatePicker.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.node, + isRequired: PropTypes.bool, + onChange: PropTypes.func.isRequired, + id: PropTypes.string, + disabled: PropTypes.bool, + calendarProps: PropTypes.shape({ + minDate: PropTypes.instanceOf(Date), + maxDate: PropTypes.instanceOf(Date), + events: PropTypes.arrayOf( + PropTypes.shape({ + date: PropTypes.string, + emphasis: PropTypes.bool, + selected: PropTypes.bool, + disabled: PropTypes.bool, + }) + ), + }), +}; + +export default DatePicker; diff --git a/src/components/forms/DateField/utils.js b/src/components/forms/DateField/utils.js new file mode 100644 index 000000000..6e1e10033 --- /dev/null +++ b/src/components/forms/DateField/utils.js @@ -0,0 +1,29 @@ +import parse from 'date-fns/parse'; + +export const parseDate = value => { + if (!value) return undefined; + const parsed = parse(value, 'yyyy-MM-dd', new Date()); + // Invalid Date is apparently a thing :ThisIsFine: + if (isNaN(parsed)) return undefined; + return parsed; +}; + +export const getDateLocaleMeta = locale => { + const testDate = new Date(2023, 4, 31); + const options = {year: 'numeric', month: 'numeric', day: 'numeric'}; + const dateTimeFormat = new Intl.DateTimeFormat(locale, options); + const parts = dateTimeFormat.formatToParts(testDate); + + const separator = parts.find(part => part.type === 'literal')?.value || ''; + + const yearPart = parts.find(part => part.type === 'year'); + const monthPart = parts.find(part => part.type === 'month'); + const dayPart = parts.find(part => part.type === 'day'); + + return { + year: parts.indexOf(yearPart), + month: parts.indexOf(monthPart), + day: parts.indexOf(dayPart), + separator: separator, + }; +}; From 853bfa7f391ae34725965a7ce324d716e480a6cb Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 27 Jun 2023 15:33:55 +0200 Subject: [PATCH 4/7] :sparkles: [#433] -- implement a date field variant with input group Day, month and year can be entered individually, which is a more accessible alternative for known dates than a date picker. The order of the inputs also varies with the active locale, to align best with expectations for individual users. --- .../forms/DateField/DateField-inputgroup.mdx | 39 ++++ .../DateField/DateField-inputgroup.stories.js | 69 +++++++ .../forms/DateField/DateInputGroup.js | 179 ++++++++++++++++++ src/scss/components/_input.scss | 51 +++++ 4 files changed, 338 insertions(+) create mode 100644 src/components/forms/DateField/DateField-inputgroup.mdx create mode 100644 src/components/forms/DateField/DateField-inputgroup.stories.js create mode 100644 src/components/forms/DateField/DateInputGroup.js diff --git a/src/components/forms/DateField/DateField-inputgroup.mdx b/src/components/forms/DateField/DateField-inputgroup.mdx new file mode 100644 index 000000000..a390f7129 --- /dev/null +++ b/src/components/forms/DateField/DateField-inputgroup.mdx @@ -0,0 +1,39 @@ +import {ArgTypes, Canvas, Meta} from '@storybook/blocks'; + +import DateField from './DateField'; +import * as InputGroupStories from './DateField-inputgroup.stories'; +import DateInputGroup from './DateInputGroup'; + + + +# DateField - input group + +## Memorable dates + +Memorable dates are dates that users can easily remember, such as a date of birth or marriage. + +To avoid users having to scroll/click a lot in calendar widgets, it's recommended to use the input +group widget where they can enter numbers for day, month and year. + +## Widget prop + +Use the prop `widget="inputGroup"` to use this widget. + + + +## Props + + + +**DateInputGroup props** + +Internally, the `DateField` uses the `DateInputGroup` widget. + + + +## No asterisks + +The backend can be configured to treat fields as required by default and instead mark optional +fields explicitly. + + diff --git a/src/components/forms/DateField/DateField-inputgroup.stories.js b/src/components/forms/DateField/DateField-inputgroup.stories.js new file mode 100644 index 000000000..395ee35a3 --- /dev/null +++ b/src/components/forms/DateField/DateField-inputgroup.stories.js @@ -0,0 +1,69 @@ +import {ConfigDecorator, FormikDecorator} from 'story-utils/decorators'; + +import DateField from './DateField'; + +export default { + title: 'Pure React Components / Forms / DateField / Input group', + component: DateField, + decorators: [FormikDecorator], + args: { + widget: 'inputGroup', + showFormattedDate: false, + description: '', + id: '', + disabled: false, + }, + argTypes: { + minDate: {table: {disable: true}}, + maxDate: {table: {disable: true}}, + disabledDates: {table: {disable: true}}, + showFormattedDate: {control: 'boolean'}, + }, + parameters: { + formik: { + initialValues: { + test: '2023-06-16', + }, + }, + }, +}; + +export const InputGroup = { + name: 'Input group', + args: { + name: 'test', + label: 'A memorable date', + description: "What's your favourite date?", + isRequired: false, + }, +}; + +export const NoAsterisks = { + name: 'No asterisk for required', + decorators: [ConfigDecorator], + parameters: { + config: { + requiredFieldsWithAsterisk: false, + }, + }, + args: { + name: 'test', + label: 'Default required', + isRequired: true, + }, +}; + +export const WithValidationError = { + name: 'With validation error', + args: { + name: 'test', + label: 'When was the battle of Hastings?', + isRequired: true, + }, + parameters: { + formik: { + initialValues: {test: '1066-10-34'}, + initialErrors: {test: 'Invalid date entered'}, + }, + }, +}; diff --git a/src/components/forms/DateField/DateInputGroup.js b/src/components/forms/DateField/DateInputGroup.js new file mode 100644 index 000000000..1eb0710f5 --- /dev/null +++ b/src/components/forms/DateField/DateInputGroup.js @@ -0,0 +1,179 @@ +import {FormLabel, Paragraph, Textbox} from '@utrecht/component-library-react'; +import {parseISO} from 'date-fns'; +import {useFormikContext} from 'formik'; +import PropTypes from 'prop-types'; +import React, {forwardRef, useEffect, useId, useMemo, useState} from 'react'; +import {FormattedDate, FormattedMessage, useIntl} from 'react-intl'; + +import {InputGroup, InputGroupItem} from 'components/forms'; + +import {getDateLocaleMeta, parseDate} from './utils'; + +const DatePartInput = forwardRef(({name, value, onChange, ...props}, ref) => ( + +)); + +DatePartInput.propTypes = { + name: PropTypes.oneOf(['day', 'month', 'year']).isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +}; + +const DateInputs = ({day, month, year, disabled, onChange}) => { + const intl = useIntl(); + const meta = useMemo(() => getDateLocaleMeta(intl.locale), [intl.locale]); + const [dayId, monthId, yearId] = [useId(), useId(), useId()]; + const parts = { + day: ( + + + + + + + ), + month: ( + + + + + + + ), + year: ( + + + + + + + ), + }; + const orderedParts = Object.keys(parts) + .sort((a, b) => meta[a] - meta[b]) + .map(part => parts[part]); + return <>{orderedParts}; +}; + +DateInputs.propTypes = { + day: PropTypes.string.isRequired, + /** + * Month part of the date. Keep in mind that this is the JS date month, so January is '0'. + */ + month: PropTypes.string.isRequired, + year: PropTypes.string.isRequired, + disabled: PropTypes.bool, + onChange: PropTypes.func.isRequired, +}; + +// TODO: check if we can merge this with the ./utils/parseDate function +const dateFromParts = (yearStr, monthStr, dayStr) => { + const bits = [yearStr.padStart(4, '0'), monthStr.padStart(2, '0'), dayStr.padStart(2, '0')]; + const ISOFormatted = bits.join('-'); + const parsed = parseISO(ISOFormatted); + if (isNaN(parsed)) return undefined; // Invalid date (which is instanceof Date) + return ISOFormatted; +}; + +const convertMonth = (month, toAdd) => { + if (!month) return ''; + const monthNumber = parseInt(month); + return String(monthNumber + toAdd); +}; + +const DateInputGroup = ({ + name, + label, + isRequired, + onChange, + id, + disabled = false, + showFormattedDate = false, +}) => { + const {getFieldProps, getFieldMeta} = useFormikContext(); + const {value} = getFieldProps(name); + const {error} = getFieldMeta(name); + const currentDate = parseDate(value); + + // keep in mind the first month is 0 in JS... + const [dateParts, setDateParts] = useState({ + year: currentDate ? String(currentDate.getFullYear()) : '', + month: currentDate ? convertMonth(currentDate.getMonth(), 1) : '', + day: currentDate ? String(currentDate.getDate()) : '', + }); + + const enteredDate = dateFromParts(dateParts.year, dateParts.month, dateParts.day); + useEffect(() => { + if (!enteredDate || enteredDate === value) return; + onChange({target: {name, value: enteredDate}}); + }, [onChange, name, value, enteredDate]); + + return ( + <> + + setDateParts({...dateParts, [name]: value})} + /> + + {showFormattedDate && ( + + + + )} + + ); +}; + +DateInputGroup.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.node, + isRequired: PropTypes.bool, + onChange: PropTypes.func.isRequired, + id: PropTypes.string, + disabled: PropTypes.bool, + showFormattedDate: PropTypes.bool, +}; + +export default DateInputGroup; diff --git a/src/scss/components/_input.scss b/src/scss/components/_input.scss index 33c4169ff..df3032c53 100644 --- a/src/scss/components/_input.scss +++ b/src/scss/components/_input.scss @@ -5,9 +5,60 @@ $input-padding: $grid-margin-2; .utrecht-textbox { + // copied over from the upstream CSS rules and dumped in private vars to easier pass + // them to calc rules. + --_of-utrecht-textbox-inline-padding-end: var( + --utrecht-textbox-padding-inline-end, + var(--utrecht-form-control-padding-inline-end, initial) + ); + --_of-utrecht-textbox-inline-padding-start: var( + --utrecht-textbox-padding-inline-start, + var(--utrecht-form-control-padding-inline-start, initial) + ); + --_of-utrecht-textbox-border-width: var( + --utrecht-textbox-border-width, + var(--utrecht-form-control-border-width) + ); + --_of-utrecht-textbox-total-inline-padding: calc( + var(--_of-utrecht-textbox-inline-padding-end) + var(--_of-utrecht-textbox-inline-padding-start) + ); + @include bem.modifier('openforms') { font-weight: var(--of-input-font-weight, normal); } + + @include bem.modifier('openforms-date-day') { + --_of-utrecht-textbox-openforms-date-day-width: calc( + var(--_of-utrecht-textbox-total-inline-padding) + 3ch + 2 * + var(--_of-utrecht-textbox-border-width) + ); + width: var( + --of-utrecht-textbox-openforms-date-day-width, + var(--_of-utrecht-textbox-openforms-date-day-width) + ); + } + + @include bem.modifier('openforms-date-month') { + --_of-utrecht-textbox-openforms-date-month-width: calc( + var(--_of-utrecht-textbox-total-inline-padding) + 3ch + 2 * + var(--_of-utrecht-textbox-border-width) + ); + width: var( + --of-utrecht-textbox-openforms-date-month-width, + var(--_of-utrecht-textbox-openforms-date-month-width) + ); + } + + @include bem.modifier('openforms-date-year') { + --_of-utrecht-textbox-openforms-date-year-width: calc( + var(--_of-utrecht-textbox-total-inline-padding) + 5ch + 2 * + var(--_of-utrecht-textbox-border-width) + ); + width: var( + --of-utrecht-textbox-openforms-date-year-width, + var(--_of-utrecht-textbox-openforms-date-year-width) + ); + } } // overrides of the utrecht-textbox component specific to our theme From c496a334db4b4abc16aec03e969060a03716e40d Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 27 Jun 2023 15:45:20 +0200 Subject: [PATCH 5/7] :globe_with_meridians: [#433] -- update Dutch translations --- bin/find_untranslated_js.py | 2 ++ scripts/i18n-formatter.js | 28 +++++++++---------- src/i18n/compiled/en.json | 36 ++++++++++++++++++++++++ src/i18n/compiled/nl.json | 36 ++++++++++++++++++++++++ src/i18n/messages/en.json | 30 ++++++++++++++++++++ src/i18n/messages/nl.json | 56 +++++++++++++++++++++++++++++++------ 6 files changed, 166 insertions(+), 22 deletions(-) diff --git a/bin/find_untranslated_js.py b/bin/find_untranslated_js.py index d5f95c378..8676b232a 100755 --- a/bin/find_untranslated_js.py +++ b/bin/find_untranslated_js.py @@ -13,6 +13,8 @@ def main(): # skip translated messages if trans_object["defaultMessage"] != trans_object["originalDefault"]: continue + if trans_object.get("isTranslated", False): + continue print( f"ID '{unique_id}' appears untranslated, defaultMessage: {trans_object['originalDefault']}" diff --git a/scripts/i18n-formatter.js b/scripts/i18n-formatter.js index 9b9f590e8..134d875a1 100644 --- a/scripts/i18n-formatter.js +++ b/scripts/i18n-formatter.js @@ -2,22 +2,22 @@ const fs = require('fs'); const argv = require('yargs').argv; // load the existing catalog to prevent overwriting messages -const existingCatalog = JSON.parse( - fs.readFileSync(argv.outFile, 'utf-8') -); +const existingCatalog = JSON.parse(fs.readFileSync(argv.outFile, 'utf-8')); +const format = messages => { + Object.entries(messages).forEach(([id, msg]) => { + // always store the original (english) default message as a reference + msg.originalDefault = msg.defaultMessage; -const format = (messages) => { - Object.entries(messages).forEach(([id, msg]) => { - // always store the original (english) default message as a reference - msg.originalDefault = msg.defaultMessage; - - // if the message with the ID is already in the catalog, re-use it - const existingMsg = existingCatalog[id]; - if (!existingMsg) return; - msg.defaultMessage = existingMsg.defaultMessage; - }); - return messages; + // if the message with the ID is already in the catalog, re-use it + const existingMsg = existingCatalog[id]; + if (!existingMsg) return; + msg.defaultMessage = existingMsg.defaultMessage; + if (existingMsg.isTranslated) { + msg.isTranslated = true; + } + }); + return messages; }; exports.format = format; diff --git a/src/i18n/compiled/en.json b/src/i18n/compiled/en.json index 3efd2e91e..4816bf40f 100644 --- a/src/i18n/compiled/en.json +++ b/src/i18n/compiled/en.json @@ -221,6 +221,12 @@ "value": "This form is currently undergoing maintenance and can not be accessed at the moment." } ], + "DBTNgI": [ + { + "type": 0, + "value": "d" + } + ], "DCzTpQ": [ { "type": 0, @@ -389,6 +395,12 @@ "value": "Continue later" } ], + "MJXZYI": [ + { + "type": 0, + "value": "m" + } + ], "PCFPnE": [ { "type": 0, @@ -419,6 +431,12 @@ "value": "Yes" } ], + "RSPyzt": [ + { + "type": 0, + "value": "Month" + } + ], "Rf8Sot": [ { "type": 0, @@ -753,6 +771,12 @@ "value": ". Extend your session if you wish to continue." } ], + "nfRRDu": [ + { + "type": 0, + "value": "yyyy" + } + ], "nwQjsz": [ { "type": 0, @@ -885,6 +909,12 @@ "value": "Failed to determine the appointment" } ], + "vCObuG": [ + { + "type": 0, + "value": "Day" + } + ], "vIfXbq": [ { "type": 0, @@ -897,6 +927,12 @@ "value": "Amount" } ], + "yKo62N": [ + { + "type": 0, + "value": "Year" + } + ], "yW2uhj": [ { "type": 0, diff --git a/src/i18n/compiled/nl.json b/src/i18n/compiled/nl.json index 7fe148737..fecfc3b53 100644 --- a/src/i18n/compiled/nl.json +++ b/src/i18n/compiled/nl.json @@ -221,6 +221,12 @@ "value": "Dit formulier is momenteel in onderhoud en daardoor tijdelijk niet beschikbaar." } ], + "DBTNgI": [ + { + "type": 0, + "value": "d" + } + ], "DCzTpQ": [ { "type": 0, @@ -389,6 +395,12 @@ "value": "Later verdergaan" } ], + "MJXZYI": [ + { + "type": 0, + "value": "m" + } + ], "PCFPnE": [ { "type": 0, @@ -419,6 +431,12 @@ "value": "Ja" } ], + "RSPyzt": [ + { + "type": 0, + "value": "Maand" + } + ], "Rf8Sot": [ { "type": 0, @@ -753,6 +771,12 @@ "value": ". Verleng uw sessie indien u door wenst te gaan." } ], + "nfRRDu": [ + { + "type": 0, + "value": "jjjj" + } + ], "nwQjsz": [ { "type": 0, @@ -885,6 +909,12 @@ "value": "Het systeem kon de afspraak niet vinden" } ], + "vCObuG": [ + { + "type": 0, + "value": "Dag" + } + ], "vIfXbq": [ { "type": 0, @@ -897,6 +927,12 @@ "value": "Aantal" } ], + "yKo62N": [ + { + "type": 0, + "value": "Jaar" + } + ], "yW2uhj": [ { "type": 0, diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 09a2b469e..aa25be447 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -144,6 +144,11 @@ "description": "Maintenance mode message", "originalDefault": "This form is currently undergoing maintenance and can not be accessed at the moment." }, + "DBTNgI": { + "defaultMessage": "d", + "description": "Placeholder for day part of a date", + "originalDefault": "d" + }, "DCzTpQ": { "defaultMessage": "Remove", "description": "Appointments: remove product/service button text", @@ -259,6 +264,11 @@ "description": "Form save modal submit button", "originalDefault": "Continue later" }, + "MJXZYI": { + "defaultMessage": "m", + "description": "Placeholder for month part of a date", + "originalDefault": "m" + }, "PCFPnE": { "defaultMessage": "Payment is required for this product", "description": "Payment required info text", @@ -284,6 +294,11 @@ "description": "'True' display", "originalDefault": "Yes" }, + "RSPyzt": { + "defaultMessage": "Month", + "description": "Date input group: month label", + "originalDefault": "Month" + }, "Rf8Sot": { "defaultMessage": "Problem - {formName}", "description": "Form start outage title", @@ -504,6 +519,11 @@ "description": "Session expiry warning message (in modal)", "originalDefault": "Your session is about to expire {delta}. Extend your session if you wish to continue." }, + "nfRRDu": { + "defaultMessage": "yyyy", + "description": "Placeholder for year part of a date", + "originalDefault": "yyyy" + }, "nwQjsz": { "defaultMessage": "Your session will expire soon.", "description": "Session expiry warning title (in modal)", @@ -569,6 +589,11 @@ "description": "Appointment cancellation missing submission ID query string parameter", "originalDefault": "Failed to determine the appointment" }, + "vCObuG": { + "defaultMessage": "Day", + "description": "Date input group: day label", + "originalDefault": "Day" + }, "vIfXbq": { "defaultMessage": "Failed to determine the appointment time", "description": "Appointment cancellation missing time query string parameter", @@ -579,6 +604,11 @@ "description": "Appointments: product amount field label", "originalDefault": "Amount" }, + "yKo62N": { + "defaultMessage": "Year", + "description": "Date input group: year label", + "originalDefault": "Year" + }, "yW2uhj": { "defaultMessage": "Co-sign confirmation", "description": "On succesful completion title", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 0722426b2..316574181 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -72,7 +72,8 @@ "7Fk8NY": { "defaultMessage": "{name}: {amount}x", "description": "Product summary on appointments location and time step", - "originalDefault": "{name}: {amount}x" + "originalDefault": "{name}: {amount}x", + "isTranslated": true }, "7kd6yw": { "defaultMessage": "Dit formulier is momenteel in onderhoud. Als medewerker kunt u dit formulier wel invullen.", @@ -144,6 +145,12 @@ "description": "Maintenance mode message", "originalDefault": "This form is currently undergoing maintenance and can not be accessed at the moment." }, + "DBTNgI": { + "defaultMessage": "d", + "description": "Placeholder for day part of a date", + "originalDefault": "d", + "isTranslated": true + }, "DCzTpQ": { "defaultMessage": "Verwijderen", "description": "Appointments: remove product/service button text", @@ -162,7 +169,8 @@ "DxbUkA": { "defaultMessage": "Er is een fout opgetreden bij het inloggen met {service}. Probeer het later opnieuw.", "description": "Auth error message", - "originalDefault": "Er is een fout opgetreden bij het inloggen met {service}. Probeer het later opnieuw." + "originalDefault": "Er is een fout opgetreden bij het inloggen met {service}. Probeer het later opnieuw.", + "isTranslated": true }, "E4h5vK": { "defaultMessage": "Download uw inzending als PDF document", @@ -259,6 +267,12 @@ "description": "Form save modal submit button", "originalDefault": "Continue later" }, + "MJXZYI": { + "defaultMessage": "m", + "description": "Placeholder for month part of a date", + "originalDefault": "m", + "isTranslated": true + }, "PCFPnE": { "defaultMessage": "Voor dit product is betaling vereist", "description": "Payment required info text", @@ -284,6 +298,11 @@ "description": "'True' display", "originalDefault": "Yes" }, + "RSPyzt": { + "defaultMessage": "Maand", + "description": "Date input group: month label", + "originalDefault": "Month" + }, "Rf8Sot": { "defaultMessage": "Probleem - {formName}", "description": "Form start outage title", @@ -302,7 +321,8 @@ "UyEKdQ": { "defaultMessage": "Product {number}/{total}", "description": "Appointments: single product label/header", - "originalDefault": "Product {number}/{total}" + "originalDefault": "Product {number}/{total}", + "isTranslated": true }, "VaNtPf": { "defaultMessage": "Afspraak annuleren", @@ -337,7 +357,8 @@ "ZehFnX": { "defaultMessage": "Je hebt het inloggen met {service} geannuleerd.", "description": "DigiD/EHerkenning cancellation message. MUST BE THIS EXACT STRING!", - "originalDefault": "Je hebt het inloggen met {service} geannuleerd." + "originalDefault": "Je hebt het inloggen met {service} geannuleerd.", + "isTranslated": true }, "a8QRYP": { "defaultMessage": "Uw e-mailadres", @@ -427,7 +448,8 @@ "hQRQCS": { "defaultMessage": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie.", "description": "DigiD error message. MUST BE THIS EXACT STRING!", - "originalDefault": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie." + "originalDefault": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie.", + "isTranslated": true }, "hR1LxA": { "defaultMessage": "Huidige stap in formulier {formTitle}: {activeStepTitle}", @@ -477,17 +499,20 @@ "kICI8K": { "defaultMessage": "Product", "description": "Appointments navbar title for 'products' step", - "originalDefault": "Product" + "originalDefault": "Product", + "isTranslated": true }, "l/0YVO": { "defaultMessage": "Product", "description": "Appoinments: product select label", - "originalDefault": "Product" + "originalDefault": "Product", + "isTranslated": true }, "lY+Mza": { "defaultMessage": "Product", "description": "Appointments products step page title", - "originalDefault": "Product" + "originalDefault": "Product", + "isTranslated": true }, "lq29t6": { "defaultMessage": "Bent u zeker dat u deze formulierinzending wilt afbreken? U verliest alle voortgang als u bevestigt.", @@ -504,6 +529,11 @@ "description": "Session expiry warning message (in modal)", "originalDefault": "Your session is about to expire {delta}. Extend your session if you wish to continue." }, + "nfRRDu": { + "defaultMessage": "jjjj", + "description": "Placeholder for year part of a date", + "originalDefault": "yyyy" + }, "nwQjsz": { "defaultMessage": "Uw sessie vervalt binnenkort.", "description": "Session expiry warning title (in modal)", @@ -569,6 +599,11 @@ "description": "Appointment cancellation missing submission ID query string parameter", "originalDefault": "Failed to determine the appointment" }, + "vCObuG": { + "defaultMessage": "Dag", + "description": "Date input group: day label", + "originalDefault": "Day" + }, "vIfXbq": { "defaultMessage": "Het systeem kon de afspraak niet vinden", "description": "Appointment cancellation missing time query string parameter", @@ -579,6 +614,11 @@ "description": "Appointments: product amount field label", "originalDefault": "Amount" }, + "yKo62N": { + "defaultMessage": "Jaar", + "description": "Date input group: year label", + "originalDefault": "Year" + }, "yW2uhj": { "defaultMessage": "Bevestiging mede-ondertekening", "description": "On succesful completion title", From 0f0c70b679c695333a7f4222871b4bd50c187d43 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 27 Jun 2023 15:50:30 +0200 Subject: [PATCH 6/7] :green_heart: [#433] -- update appointment components --- src/components/appointments/DateSelect.js | 1 + src/components/forms/DateField/DatePicker.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/appointments/DateSelect.js b/src/components/appointments/DateSelect.js index 23356e0f2..6164ec78d 100644 --- a/src/components/appointments/DateSelect.js +++ b/src/components/appointments/DateSelect.js @@ -67,6 +67,7 @@ const DateSelect = () => { return ( { From 98dd212d6b08f1c487d5e68b24b9c18a4fd68764 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 27 Jun 2023 16:30:12 +0200 Subject: [PATCH 7/7] :white_check_mark: [#433] -- added (interaction) tests for input group date field --- .../DateField/DateField-inputgroup.stories.js | 50 +++++++++++++++++++ src/story-utils/decorators.js | 4 ++ 2 files changed, 54 insertions(+) diff --git a/src/components/forms/DateField/DateField-inputgroup.stories.js b/src/components/forms/DateField/DateField-inputgroup.stories.js index 395ee35a3..dec1f8b47 100644 --- a/src/components/forms/DateField/DateField-inputgroup.stories.js +++ b/src/components/forms/DateField/DateField-inputgroup.stories.js @@ -1,3 +1,7 @@ +import {expect, jest} from '@storybook/jest'; +import {userEvent, waitFor, within} from '@storybook/testing-library'; +import {Form, Formik} from 'formik'; + import {ConfigDecorator, FormikDecorator} from 'story-utils/decorators'; import DateField from './DateField'; @@ -38,6 +42,52 @@ export const InputGroup = { }, }; +export const ISO8601 = { + name: 'Value normalizes to ISO-8601', + render: ({onSubmit}) => ( + { + console.log(onSubmit, values); + onSubmit(values); + actions.setSubmitting(false); + }} + > +

+ + + + + ), + parameters: { + formik: {disable: true}, + }, + argTypes: { + onSubmit: {action: true}, + widget: {table: {disable: true}}, + showFormattedDate: {table: {disable: true}}, + description: {table: {disable: true}}, + id: {table: {disable: true}}, + disabled: {table: {disable: true}}, + name: {table: {disable: true}}, + label: {table: {disable: true}}, + isRequired: {table: {disable: true}}, + }, + play: async ({canvasElement, args, step}) => { + const canvas = within(canvasElement); + await step('Fill out inputs', async () => { + await userEvent.type(canvas.getByLabelText('Dag'), '9'); + await userEvent.type(canvas.getByLabelText('Maand'), '6'); + await userEvent.type(canvas.getByLabelText('Jaar'), '2023'); + }); + + await step('Submit form and inspect data', async () => { + await userEvent.click(canvas.getByRole('button')); + await waitFor(() => expect(args.onSubmit).toHaveBeenCalledWith({test: '2023-06-09'})); + }); + }, +}; + export const NoAsterisks = { name: 'No asterisk for required', decorators: [ConfigDecorator], diff --git a/src/story-utils/decorators.js b/src/story-utils/decorators.js index 2436cd1cd..046767579 100644 --- a/src/story-utils/decorators.js +++ b/src/story-utils/decorators.js @@ -53,6 +53,10 @@ export const DeprecatedRouterDecorator = (Story, {args: {routerArgs = {}}}) => { }; export const FormikDecorator = (Story, context) => { + const isDisabled = context.parameters?.formik?.disable ?? false; + if (isDisabled) { + return ; + } const initialValues = context.parameters?.formik?.initialValues || {}; const initialErrors = context.parameters?.formik?.initialErrors || {}; return (