Skip to content

Commit

Permalink
chore(Field.Date): wrap in FieldBlock (#3189)
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker authored Jan 9, 2024
1 parent f3ce934 commit 4fdbf56
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const Label = () => {

export const LabelAndValue = () => {
return (
<ComponentBox>
<ComponentBox data-visual-test="date-label">
<Field.Date
value="2023-01-16"
label="Label text"
Expand Down Expand Up @@ -75,7 +75,7 @@ export const Disabled = () => {

export const Error = () => {
return (
<ComponentBox scope={{ FormError }}>
<ComponentBox scope={{ FormError }} data-visual-test="date-error">
<Field.Date
value="2023-01-16"
label="Label text"
Expand Down
100 changes: 79 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,114 @@
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
}

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,
hasError,
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}
{...pickSpacingProps(props)}
/>
>
<DatePicker
id={id}
date={value}
disabled={disabled}
show_input={true}
show_cancel_button={true}
show_reset_button={true}
status={error || hasError ? 'error' : undefined}
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
@@ -0,0 +1,27 @@
import {
makeScreenshot,
setupPageScreenshot,
} from '../../../../../core/jest/jestSetupScreenshots'

const url = '/uilib/extensions/forms/feature-fields/Date/demos'

describe.each(['ui'])('Date for %s', (themeName) => {
setupPageScreenshot({
themeName,
url,
})

it('have to match with a label', async () => {
const screenshot = await makeScreenshot({
selector: '[data-visual-test="date-label"]',
})
expect(screenshot).toMatchImageSnapshot()
})

it('have to match with an error', async () => {
const screenshot = await makeScreenshot({
selector: '[data-visual-test="date-error"]',
})
expect(screenshot).toMatchImageSnapshot()
})
})
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()
})
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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}
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
Loading

0 comments on commit 4fdbf56

Please sign in to comment.