Skip to content

Commit

Permalink
feat: TimePicker component (#1082)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonlenz authored Apr 9, 2021
1 parent 268d4e6 commit c7bfdee
Show file tree
Hide file tree
Showing 8 changed files with 649 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/components/forms/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const ComboBox = ({
noResults,
selectProps,
inputProps,
...customProps
}: ComboBoxProps): React.ReactElement => {
const isDisabled = !!disabled

Expand Down Expand Up @@ -270,7 +271,8 @@ export const ComboBox = ({
data-testid="combo-box"
className={containerClasses}
id={id}
ref={containerRef}>
ref={containerRef}
{...customProps}>
<select
className="usa-select usa-sr-only usa-combo-box__select"
name={name}
Expand Down
73 changes: 73 additions & 0 deletions src/components/forms/TimePicker/TimePicker.stories.tsx
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>
)
78 changes: 78 additions & 0 deletions src/components/forms/TimePicker/TimePicker.test.tsx
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()
})
})
86 changes: 86 additions & 0 deletions src/components/forms/TimePicker/TimePicker.tsx
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>
)
}
14 changes: 14 additions & 0 deletions src/components/forms/TimePicker/constants.ts
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})',
}
Loading

0 comments on commit c7bfdee

Please sign in to comment.