Skip to content

Commit

Permalink
feat(Field.Date): add date range functionality (#4006)
Browse files Browse the repository at this point in the history
Co-authored-by: Tobias Høegh <tobias@tujo.no>
Co-authored-by: Anders <anderslangseth@gmail.com>
  • Loading branch information
3 people authored Sep 30, 2024
1 parent 595cfd5 commit 12081c3
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,11 @@ export const Inline = () => {
</ComponentBox>
)
}

export const Range = () => {
return (
<ComponentBox>
<Value.Date value="2023-01-16|2023-04-01" />
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import * as Examples from './Examples'

<Examples.LabelAndValue />

### Date range

<Examples.Range />

### Inline

<Examples.Inline />
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ export const ValidationRequired = () => {
</ComponentBox>
)
}

export const Range = () => {
return (
<ComponentBox>
<Field.Date value="2023-01-16|2023-04-01" range />
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import * as Examples from './Examples'

<Examples.LabelAndValue />

### Date range

<Examples.Range />

### With help

<Examples.WithHelp />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ showTabs: true
import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable'
import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
import { fieldProperties } from '@dnb/eufemia/src/extensions/forms/Field/FieldDocs'
import { DateProperties } from '@dnb/eufemia/src/extensions/forms/Field/Date/DateDocs'

## Properties

### Field-specific properties

| Property | Type | Description |
| -------- | -------- | ------------------------------------------------------------------------------- |
| `help` | `object` | _(optional)_ Provide a help button. Object consisting of `title` and `content`. |
<PropertiesTable props={DateProperties} />

### General properties

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type DatePickerDirection = 'auto' | 'top' | 'bottom';
type DatePickerAlignPicker = 'auto' | 'left' | 'right';

// Make it possible to join React.Event interfaces with DatePickerEvent type.
type DatePickerEvent<T extends T> = T & {
export type DatePickerEvent<T extends T> = T & {
date?: string;
start_date?: string;
end_date?: string;
Expand Down
31 changes: 28 additions & 3 deletions packages/dnb-eufemia/src/extensions/forms/Field/Date/Date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ import FieldBlock from '../../FieldBlock'
import { parseISO, isValid } from 'date-fns'
import useErrorMessage from '../../hooks/useErrorMessage'
import useTranslation from '../../hooks/useTranslation'
import { DatePickerEvent } from '../../../../components/DatePicker'

export type Props = FieldHelpProps &
FieldProps<string, undefined | string> & {
// Validation
pattern?: string
/**
* Defines if the Date field should support a value of two dates (starting and ending date).
* The value needs to be a string containing two dates, separated by a pipe character (`|`) i.e. (`01-09-2024|30-09-2024`) when this is set to `true`.
* Defaults to `false`.
*/
range?: boolean
}

function DateComponent(props: Props) {
Expand Down Expand Up @@ -51,8 +58,12 @@ function DateComponent(props: Props) {
...props,
errorMessages,
schema,
fromInput: ({ date }: { date: string }) => {
return date
fromInput: ({
date,
start_date,
end_date,
}: DatePickerEvent<React.ChangeEvent<HTMLInputElement>>) => {
return range ? `${start_date}|${end_date}` : date
},
validateRequired,
}
Expand All @@ -73,8 +84,19 @@ function DateComponent(props: Props) {
handleFocus,
handleBlur,
handleChange,
range,
} = useFieldProps(preparedProps)

const rangeValue = useMemo(() => {
if (!range) {
return
}

const [startDate, endDate] = value.split('|')

return { startDate, endDate }
}, [range, value])

return (
<FieldBlock
className={classnames('dnb-forms-field-string', className)}
Expand All @@ -89,17 +111,20 @@ function DateComponent(props: Props) {
>
<DatePicker
id={id}
date={value}
date={!range ? value : undefined}
disabled={disabled}
show_input={true}
show_cancel_button={true}
show_reset_button={true}
start_date={range ? rangeValue.startDate : undefined}
end_date={range ? rangeValue.endDate : undefined}
status={hasError ? 'error' : undefined}
suffix={
help ? (
<HelpButton title={help.title}>{help.content}</HelpButton>
) : undefined
}
range={range}
on_change={handleChange}
on_reset={handleChange}
onFocus={handleFocus}
Expand Down
17 changes: 17 additions & 0 deletions packages/dnb-eufemia/src/extensions/forms/Field/Date/DateDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PropertiesTableProps } from '../../../../shared/types'

export const DateProperties: PropertiesTableProps = {
help: {
doc: 'Provide a help button. Object consisting of `title` and `content`.',
type: 'object',
status: 'optional',
},
range: {
doc:
'Defines if the Date field should support a value of two dates (starting and ending date). ' +
'The `value` needs to be a string containing two dates, separated by a pipe character (`|`) (`01-09-2024|30-09-2024`) when this is set to `true`. ' +
'Defaults to `false`.',
type: 'boolean',
status: 'optional',
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,70 @@ describe('Field.Date', () => {
expect(screen.queryByRole('alert')).toBeInTheDocument()
})

it('should support date range', () => {
const { rerender } = render(
<Field.Date range value="2024-09-01|2024-09-30" />
)

const fields = Array.from(
document.querySelectorAll('.dnb-date-picker__input')
)

expect(fields).toHaveLength(6)

const [startDay, startMonth, startYear, endDay, endMonth, endYear] =
fields

// Start date
expect(startDay).toHaveValue('01')
expect(startMonth).toHaveValue('09')
expect(startYear).toHaveValue('2024')

// End date
expect(endDay).toHaveValue('30')
expect(endMonth).toHaveValue('09')
expect(endYear).toHaveValue('2024')

// Should handle undfined or null end date
rerender(<Field.Date range value="2024-09-01|undefined" />)

// Start date
expect(startDay).toHaveValue('01')
expect(startMonth).toHaveValue('09')
expect(startYear).toHaveValue('2024')

// End date
expect(endDay).toHaveValue('dd')
expect(endMonth).toHaveValue('mm')
expect(endYear).toHaveValue('åååå')

// Should handle undfined or null start and end dates
rerender(<Field.Date range value="null|undefined" />)

// Start date
expect(startDay).toHaveValue('dd')
expect(startMonth).toHaveValue('mm')
expect(startYear).toHaveValue('åååå')

// End date
expect(endDay).toHaveValue('dd')
expect(endMonth).toHaveValue('mm')
expect(endYear).toHaveValue('åååå')

// Should handle undfined or null start date
rerender(<Field.Date range value="null|2024-04-01" />)

// Start date
expect(startDay).toHaveValue('dd')
expect(startMonth).toHaveValue('mm')
expect(startYear).toHaveValue('åååå')

// End date
expect(endDay).toHaveValue('01')
expect(endMonth).toHaveValue('04')
expect(endYear).toHaveValue('2024')
})

describe('error handling', () => {
describe('with validateInitially', () => {
it('should show error message initially', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import { Field } from '../../..'
import { Field, Value } from '../../..'
import FormHandler from '../../../Form/Handler/Handler'

export default {
title: 'Eufemia/Extensions/Forms/Date',
Expand All @@ -25,3 +26,12 @@ export function Date() {
/>
)
}

export function Range() {
return (
<FormHandler data={{ myRange: '2023-01-16|2023-01-20' }}>
<Field.Date path="/myRange" range />
<Value.Date path="/myRange" variant="numeric" />
</FormHandler>
)
}
66 changes: 47 additions & 19 deletions packages/dnb-eufemia/src/extensions/forms/Value/Date/Date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,40 @@ function DateComponent(props: Props) {
return undefined
}

const getOptions = (variant) => {
if (variant === 'numeric') {
return {
day: '2-digit',
month: '2-digit',
year: 'numeric',
} as const
// Either of the range dates can be null
const isRange =
/^(\d{4}-\d{2}-\d{2}|null|undefined)\|(\d{4}-\d{2}-\d{2}|null|undefined)$/.test(
value
)
const options = getOptions(variant)

if (isRange) {
const [startValue, endValue] = value.split('|')

const startDate = new Date(startValue)
const endDate = new Date(endValue)

// Stop if either date is invalid
if (isNaN(startDate.valueOf()) || isNaN(endDate.valueOf())) {
return undefined
}
return {
day: 'numeric',
month: variant,
year: 'numeric',
} as const

return typeof Intl !== 'undefined'
? new Intl.DateTimeFormat(locale, options).formatRange(
startDate,
endDate
)
: `${startDate.toLocaleString(
locale,
options
)}|${endDate.toLocaleString(locale, options)}`
}

const date = new Date(value)
const options = getOptions(variant)

const formattedDate =
typeof Intl !== 'undefined'
? new Intl.DateTimeFormat(locale, options).format(date)
: date.toLocaleString(locale, options)

return formattedDate
return typeof Intl !== 'undefined'
? new Intl.DateTimeFormat(locale, options).format(date)
: date.toLocaleString(locale, options)
},
[locale, variant]
)
Expand All @@ -56,5 +66,23 @@ function DateComponent(props: Props) {
return <StringValue {...stringProps} />
}

function getOptions(
variant: Props['variant']
): Intl.DateTimeFormatOptions {
if (variant === 'numeric') {
return {
day: '2-digit',
month: '2-digit',
year: 'numeric',
} as const
}

return {
day: 'numeric',
month: variant,
year: 'numeric',
} as const
}

DateComponent._supportsSpacingProps = true
export default DateComponent
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@ describe('Value.Date', () => {
globalThis.Intl = intlBackup
})

it('should support date range values', () => {
const { rerender } = render(
<Value.Date value="2024-09-01|2024-09-30" variant="numeric" />
)

const valueBlock = document.querySelector(
'.dnb-forms-value-block__content'
)

expect(valueBlock).toHaveTextContent('01.09.2024–30.09.2024')

rerender(
<Value.Date value="2024-09-01|2024-09-30" variant="short" />
)

expect(valueBlock).toHaveTextContent('1.–30. sep. 2024')

rerender(<Value.Date value="2024-09-01|2024-09-30" variant="long" />)

expect(valueBlock).toHaveTextContent('1.–30. september 2024')
})

describe('formats with different locale', () => {
it('given as prop', () => {
render(<Value.Date value="2023-01-16" locale="en-GB" />)
Expand Down

0 comments on commit 12081c3

Please sign in to comment.