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 cbf5362 commit feb8184
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 60 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)
}}
/>
)
}

0 comments on commit feb8184

Please sign in to comment.