Skip to content

Commit

Permalink
chore(Field.Date): add FieldBlock
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Jan 8, 2024
1 parent 2062213 commit 55fe616
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 75 deletions.
104 changes: 83 additions & 21 deletions packages/dnb-eufemia/src/extensions/forms/Field/Date/Date.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,118 @@
import React, { useContext } from 'react'
import React, { useCallback, useContext, useMemo } from 'react'
import { DatePicker, HelpButton } from '../../../../components'
import { useDataValue } from '../../hooks'
import { FieldProps, FieldHelpProps } from '../../types'
import { pickSpacingProps } from '../../../../components/flex/utils'
import SharedContext from '../../../../shared/Context'
import { JSONSchema7 } from 'json-schema'
import classnames from 'classnames'
import FieldBlock from '../../FieldBlock'
import { parseISO, isValid } from 'date-fns'

export type Props = FieldHelpProps & FieldProps<string>
export type Props = FieldHelpProps &
FieldProps<string> & {
// Validation
pattern?: string
// Styling
width?: false | 'small' | 'medium' | 'large' | 'stretch'
}

function DateComponent(props: Props) {
const sharedContext = useContext(SharedContext)
const tr = sharedContext?.translation.Forms

const errorMessages = useMemo(
() => ({
required: tr.dateErrorRequired,
pattern: tr.inputErrorPattern,
...props.errorMessages,
}),
[tr, props.errorMessages]
)

const schema = useMemo<JSONSchema7>(
() =>
props.schema ?? {
type: 'string',
pattern: props.pattern,
},
[props.schema, props.pattern]
)

const validateRequired = useCallback(
(value: string, { required, error }) => {
if (required && (!value || !isValid(parseISO(value)))) {
return error
}

return undefined
},
[]
)

const preparedProps: Props = {
...props,
errorMessages,
schema,
fromInput: ({ date }: { date: string }) => {
return date
},
emptyValue: null,
validateRequired,
}

const {
id,
className,
label,
labelDescription,
labelSecondary,
value,
help,
info,
warning,
error,
width,
disabled,
handleFocus,
handleBlur,
handleChange,
} = useDataValue(preparedProps)

return (
<DatePicker
className={className}
<FieldBlock
className={classnames('dnb-forms-field-string', className)}
forId={id}
label={label ?? sharedContext?.translation.Forms.dateLabel}
label_direction="vertical"
date={value}
status={error?.message}
labelDescription={labelDescription}
labelSecondary={labelSecondary}
info={info}
warning={warning}
disabled={disabled}
show_input={true}
show_cancel_button={true}
show_reset_button={true}
suffix={
help ? (
<HelpButton title={help.title}>{help.contents}</HelpButton>
) : undefined
}
on_change={handleChange}
on_reset={handleChange}
on_show={handleFocus}
on_hide={handleBlur}
error={error}
width={width === 'stretch' ? width : undefined}
contentsWidth={width !== false ? width : undefined}
{...pickSpacingProps(props)}
/>
>
<DatePicker
id={id}
date={value}
disabled={disabled}
show_input={true}
show_cancel_button={true}
show_reset_button={true}
stretch
suffix={
help ? (
<HelpButton title={help.title}>{help.contents}</HelpButton>
) : undefined
}
on_change={handleChange}
on_reset={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
{...pickSpacingProps(props)}
/>
</FieldBlock>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import React from 'react'
import { act, render } from '@testing-library/react'
import Date, { Props } from '..'
import { render, waitFor, screen, fireEvent } from '@testing-library/react'
import Date from '..'
import userEvent from '@testing-library/user-event'
import { axeComponent } from '../../../../../core/jest/jestSetup'

const props: Props = {}

describe('Field.Date', () => {
it('should render with props', () => {
render(<Date {...props} />)
render(<Date />)
})

it('should show required warning', async () => {
render(<Date {...props} value="2023-12-07" required />)
render(<Date value="2023-12-07" required />)

const datepicker = document.querySelector('.dnb-date-picker')
const inputs: Array<HTMLInputElement> = Array.from(
const [, , year]: Array<HTMLInputElement> = Array.from(
datepicker.querySelectorAll('.dnb-date-picker__input')
)

Expand All @@ -26,31 +24,17 @@ describe('Field.Date', () => {
datepicker.querySelector('.dnb-form-status__text')
).not.toBeInTheDocument()

act(() => {
inputs[inputs.length - 1].focus()
inputs[inputs.length - 1].setSelectionRange(4, 4)
})
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

await userEvent.keyboard('{Backspace>8}')
fireEvent.focus(year)
await userEvent.type(year, '{Backspace>2}')
fireEvent.blur(year)

expect(datepicker.classList).toContain(
'dnb-date-picker__status--error'
)
expect(
datepicker.querySelector('.dnb-form-status__text')
).toBeInTheDocument()
expect(
datepicker.querySelector('.dnb-form-status__text')
).toHaveTextContent('The value is required')
expect(screen.queryByRole('alert')).toBeInTheDocument()

await userEvent.keyboard('20231207')

expect(datepicker.classList).not.toContain(
'dnb-date-picker__status--error'
)
expect(
datepicker.querySelector('.dnb-form-status__text')
).not.toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

await userEvent.click(
document.querySelector('.dnb-input__submit-button__button')
Expand All @@ -62,20 +46,47 @@ describe('Field.Date', () => {
.querySelectorAll('.dnb-button--tertiary ')[0]
)

expect(datepicker.classList).toContain(
'dnb-date-picker__status--error'
)
expect(
datepicker.querySelector('.dnb-form-status__text')
).toBeInTheDocument()
expect(
datepicker.querySelector('.dnb-form-status__text')
).toHaveTextContent('The value is required')
expect(screen.queryByRole('alert')).toBeInTheDocument()
})

it('should validate with ARIA rules', async () => {
const result = render(<Date {...props} value="2023-12-07" required />)
describe('error handling', () => {
describe('with validateInitially', () => {
it('should show error message initially', async () => {
render(<Date required validateInitially />)
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument()
})
})
})

describe('with validateUnchanged', () => {
it('should show error message when blurring without any changes', async () => {
jest.spyOn(console, 'log').mockImplementationOnce(jest.fn()) // because of the invalid date
render(
<Date
value="2023-12-0"
schema={{ type: 'string', minLength: 10 }}
validateUnchanged
/>
)
const input = document.querySelector('input')
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
input.focus()
fireEvent.blur(input)
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument()
})
})
})
})

describe('ARIA', () => {
it('should validate with ARIA rules', async () => {
const result = render(
<Date label="Label" required validateInitially />
)

expect(await axeComponent(result)).toHaveNoViolations()
expect(await axeComponent(result)).toHaveNoViolations()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { Field } from '../../..'

export default {
title: 'Eufemia/Extensions/Forms/Date',
}

export function Date() {
const [state, update] = React.useState('2023-01-16')
React.useEffect(() => {
update('2023-01-18')
}, [])

return (
<Field.Date
required
// validateInitially
value={state}
width="large"
onBlur={console.log}
onFocus={console.log}
onChange={(value) => {
console.log('onChange', value)
update(value)
}}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import countries, { CountryType } from '../../constants/countries'
import StringComponent, { Props as InputProps } from '../String'
import { useDataValue } from '../../hooks'
import FieldBlock from '../../FieldBlock'
import { FieldHelpProps, FieldProps, FormError } from '../../types'
import { FieldHelpProps, FieldProps } from '../../types'
import { pickSpacingProps } from '../../../../components/flex/utils'
import SharedContext from '../../../../shared/Context'
import {
Expand Down Expand Up @@ -82,11 +82,7 @@ function PhoneNumber(props: Props) {
)

const validateRequired = useCallback(
(value: string, { required, isChanged }) => {
const error = new FormError('The value is required', {
validationRule: 'required',
})

(value: string, { required, isChanged, error }) => {
if (required) {
const [countryCode, phoneNumber] = splitValue(value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,12 @@ export default function useDataValue<
toEvent = (value: Value) => value,
transformValue = (value: Value) => value,
fromExternal = (value: Value) => value,
validateRequired = (value: Value, { emptyValue, required }) => {
validateRequired = (value: Value, { emptyValue, required, error }) => {
const res =
required &&
(value === emptyValue ||
(typeof emptyValue === 'undefined' && value === ''))
? new FormError('The value is required', {
validationRule: 'required',
})
? error
: undefined
return res
},
Expand Down Expand Up @@ -289,6 +287,9 @@ export default function useDataValue<
emptyValue,
required,
isChanged: changedRef.current,
error: new FormError('The value is required', {
validationRule: 'required',
}),
}
)
if (requiredError instanceof Error) {
Expand Down
12 changes: 7 additions & 5 deletions packages/dnb-eufemia/src/extensions/forms/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function omitDataValueReadProps<Props extends DataValueReadProps>(

export interface DataValueWriteProps<
Value = unknown,
EmptyValue = undefined | string | number,
EmptyValue = undefined | string,
> {
emptyValue?: EmptyValue
onFocus?: (value: Value | EmptyValue) => void
Expand Down Expand Up @@ -107,7 +107,7 @@ export function omitDataValueWriteProps<Props extends DataValueWriteProps>(

export type DataValueReadWriteProps<
Value = unknown,
EmptyValue = undefined | string | number,
EmptyValue = undefined | string,
> = DataValueReadProps<Value> & DataValueWriteProps<Value, EmptyValue>

export function pickDataValueReadWriteProps<
Expand Down Expand Up @@ -147,14 +147,14 @@ export type DataValueReadComponentProps<Value = unknown> = ComponentProps &

export type DataValueReadWriteComponentProps<
Value = unknown,
EmptyValue = undefined | string | number,
EmptyValue = undefined | string,
> = ComponentProps &
DataValueReadProps<Value> &
DataValueWriteProps<Value, EmptyValue>

export interface FieldProps<
Value = unknown,
EmptyValue = undefined | string | number,
EmptyValue = undefined | string,
ErrorMessages extends { required?: string } = DefaultErrorMessages,
> extends DataValueReadWriteComponentProps<Value, EmptyValue> {
/** ID added to the actual field component, and linked to the label via for-attribute */
Expand Down Expand Up @@ -218,10 +218,12 @@ export interface FieldProps<
emptyValue,
required,
isChanged,
error,
}: {
emptyValue: undefined | string | number
emptyValue: EmptyValue
required: boolean
isChanged: boolean
error: FormError | undefined
}
) => FormError | undefined
}
Expand Down

0 comments on commit 55fe616

Please sign in to comment.