-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
268d4e6
commit c7bfdee
Showing
8 changed files
with
649 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import React from 'react' | ||
|
||
import { Form } from '../Form/Form' | ||
import { TimePicker } from './TimePicker' | ||
|
||
export default { | ||
title: 'Components/Form controls/Time picker', | ||
component: TimePicker, | ||
argTypes: { | ||
onsubmit: { action: 'submitted' }, | ||
disabled: { control: { type: 'boolean' } }, | ||
}, | ||
parameters: { | ||
docs: { | ||
description: { | ||
component: ` | ||
### USWDS 2.0 TimePicker component | ||
https://designsystem.digital.gov/components/time-picker/ | ||
`, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
const noop = (): void => { | ||
return | ||
} | ||
|
||
export const completeTimePicker = (argTypes): React.ReactElement => ( | ||
<Form onSubmit={argTypes.onSubmit}> | ||
<TimePicker | ||
id="appointment-time" | ||
name="appointment-time" | ||
label="Appointment Time" | ||
hint="hh:mm" | ||
onChange={noop} | ||
/> | ||
</Form> | ||
) | ||
|
||
export const defaultTimePicker = (argTypes): React.ReactElement => ( | ||
<Form onSubmit={argTypes.onSubmit}> | ||
<TimePicker id="appointment-time" name="appointment-time" onChange={noop} /> | ||
</Form> | ||
) | ||
|
||
export const withMinAndMaxTimes = (argTypes): React.ReactElement => ( | ||
<Form onSubmit={argTypes.onSubmit}> | ||
<TimePicker | ||
id="appointment-time" | ||
name="appointment-time" | ||
label="Appointment Time" | ||
hint="hh:mm (9:00am - 5:00pm)" | ||
minTime="9:00" | ||
maxTime="17:00" | ||
onChange={noop} | ||
/> | ||
</Form> | ||
) | ||
|
||
export const withDefaultValue = (argTypes): React.ReactElement => ( | ||
<Form onSubmit={argTypes.onSubmit}> | ||
<TimePicker | ||
id="appointment-time" | ||
name="appointment-time" | ||
label="Appointment Time" | ||
hint="hh:mm" | ||
defaultValue="12:00" | ||
onChange={noop} | ||
/> | ||
</Form> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import React from 'react' | ||
import { render } from '@testing-library/react' | ||
|
||
import { TimePicker } from './TimePicker' | ||
import userEvent from '@testing-library/user-event' | ||
|
||
describe('TimePicker Component', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
const testProps = { | ||
id: 'appointment-time', | ||
name: 'appointment-time', | ||
onChange: jest.fn(), | ||
} | ||
|
||
it('renders without errors', () => { | ||
const { getByTestId } = render(<TimePicker {...testProps} />) | ||
|
||
const timePickerComboBox = getByTestId('combo-box') | ||
|
||
expect(timePickerComboBox).toBeInTheDocument() | ||
expect(timePickerComboBox).toHaveClass('usa-combo-box usa-time-picker') | ||
}) | ||
|
||
it('allows the user to select a time', () => { | ||
const { getByTestId } = render(<TimePicker {...testProps} />) | ||
|
||
const comboBoxTextInput = getByTestId('combo-box-input') | ||
const comboBoxDropdownList = getByTestId('combo-box-option-list') | ||
const elementToSelect = getByTestId('combo-box-option-00:00') | ||
const comboBoxClearButton = getByTestId('combo-box-clear-button') | ||
|
||
expect(comboBoxTextInput).toHaveAttribute('aria-expanded', 'false') | ||
expect(comboBoxDropdownList).not.toBeVisible() | ||
expect(comboBoxClearButton).not.toBeVisible() | ||
|
||
// Click on the TimePicker input | ||
userEvent.click(comboBoxTextInput) | ||
expect(comboBoxTextInput).toHaveAttribute('aria-expanded', 'true') | ||
expect(comboBoxDropdownList).toBeVisible() | ||
|
||
// Select a time | ||
jest.clearAllMocks() | ||
|
||
userEvent.hover(elementToSelect) | ||
expect(elementToSelect).toHaveClass('usa-combo-box__list-option--focused') | ||
|
||
userEvent.click(elementToSelect) | ||
expect(testProps.onChange).toHaveBeenCalledTimes(1) | ||
expect(elementToSelect).toHaveClass('usa-combo-box__list-option--selected') | ||
expect(comboBoxTextInput).toHaveAttribute('aria-expanded', 'false') | ||
expect(comboBoxDropdownList).not.toBeVisible() | ||
expect(comboBoxTextInput).toHaveValue(elementToSelect.textContent) | ||
expect(comboBoxClearButton).toBeVisible() | ||
}) | ||
|
||
it('allows the user to clear the input', () => { | ||
const { getByTestId } = render( | ||
<TimePicker {...testProps} defaultValue="00:00" /> | ||
) | ||
|
||
const comboBoxTextInput = getByTestId('combo-box-input') | ||
const comboBoxClearButton = getByTestId('combo-box-clear-button') | ||
|
||
expect(comboBoxClearButton).toBeVisible() | ||
expect(comboBoxTextInput).toHaveValue('12:00am') | ||
|
||
//Clear the input | ||
jest.clearAllMocks() | ||
|
||
userEvent.click(comboBoxClearButton) | ||
expect(testProps.onChange).toHaveBeenCalledTimes(1) | ||
expect(comboBoxClearButton).not.toBeVisible() | ||
expect(comboBoxTextInput).not.toHaveValue() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import React, { useMemo } from 'react' | ||
import classnames from 'classnames' | ||
import { FormGroup } from '../FormGroup/FormGroup' | ||
import { Label } from '../Label/Label' | ||
import { ComboBox } from '../ComboBox/ComboBox' | ||
import { getTimeOptions, parseTimeString } from './utils' | ||
import { | ||
DEFAULT_MAX_TIME, | ||
DEFAULT_MAX_TIME_MINUTES, | ||
DEFAULT_MIN_TIME, | ||
DEFAULT_MIN_TIME_MINUTES, | ||
DEFAULT_STEP, | ||
FILTER_DATASET, | ||
MIN_STEP, | ||
} from './constants' | ||
|
||
interface BaseTimePickerProps { | ||
id: string | ||
name: string | ||
onChange: (val?: string) => void | ||
label?: string | ||
defaultValue?: string | ||
disabled?: boolean | ||
minTime?: string | ||
maxTime?: string | ||
step?: number | ||
hint?: string | ||
className?: string | ||
} | ||
|
||
type TimePickerProps = BaseTimePickerProps & | ||
Omit<JSX.IntrinsicElements['input'], 'onChange'> | ||
|
||
export const TimePicker = ({ | ||
id, | ||
name, | ||
onChange, | ||
label, | ||
defaultValue, | ||
disabled, | ||
minTime = DEFAULT_MIN_TIME, | ||
maxTime = DEFAULT_MAX_TIME, | ||
step = DEFAULT_STEP, | ||
hint, | ||
className, | ||
}: TimePickerProps): React.ReactElement => { | ||
const classes = classnames('usa-time-picker', className) | ||
|
||
const parsedMinTime = parseTimeString(minTime) || DEFAULT_MIN_TIME_MINUTES | ||
const parsedMaxTime = parseTimeString(maxTime) || DEFAULT_MAX_TIME_MINUTES | ||
const validStep = step < MIN_STEP ? MIN_STEP : step | ||
const timeOptions = useMemo( | ||
() => getTimeOptions(parsedMinTime, parsedMaxTime, validStep), | ||
[minTime, maxTime, step] | ||
) | ||
|
||
const labelId = `${name}-label` | ||
const hintId = `${name}-hint` | ||
|
||
return ( | ||
<FormGroup> | ||
<Label className="usa-label" id={labelId} htmlFor={id}> | ||
{label} | ||
</Label> | ||
{hint && ( | ||
<div className="usa-hint" id={hintId}> | ||
{hint} | ||
</div> | ||
)} | ||
<ComboBox | ||
id={id} | ||
name={name} | ||
className={classes} | ||
onChange={onChange} | ||
defaultValue={defaultValue} | ||
options={timeOptions} | ||
disabled={disabled} | ||
data-filter={FILTER_DATASET.filter} | ||
data-ap-query-filter={FILTER_DATASET.apQueryFilter} | ||
data-hour-query-filter={FILTER_DATASET.hourQueryFilter} | ||
data-minute-query-filter={FILTER_DATASET.minuteQueryFilter} | ||
data-disable-filtering="true" | ||
/> | ||
</FormGroup> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
export const DEFAULT_MAX_TIME = '23:59' | ||
export const DEFAULT_MAX_TIME_MINUTES = 24 * 60 - 1 | ||
export const DEFAULT_MIN_TIME = '00:00' | ||
export const DEFAULT_MIN_TIME_MINUTES = 0 | ||
export const DEFAULT_STEP = 30 | ||
export const MIN_STEP = 1 | ||
|
||
export const FILTER_DATASET = { | ||
filter: | ||
'0?{{ hourQueryFilter }}:{{minuteQueryFilter}}.*{{ apQueryFilter }}m?', | ||
apQueryFilter: '([ap])', | ||
hourQueryFilter: '([1-9][0-2]?)', | ||
minuteQueryFilter: '[\\d]+:([0-9]{0,2})', | ||
} |
Oops, something went wrong.