Skip to content

Commit

Permalink
feat(ui-date-time-input): add initialTimeForNewDate prop to datetimei…
Browse files Browse the repository at this point in the history
…nput

Closes: INSTUI-3966
  • Loading branch information
joyenjoyer committed Feb 8, 2024
1 parent 82c8147 commit 33711a3
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 5 deletions.
5 changes: 4 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion packages/ui-date-time-input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"devDependencies": {
"@instructure/ui-babel-preset": "8.52.0",
"@instructure/ui-test-locator": "8.52.0",
"@instructure/ui-test-utils": "8.52.0"
"@instructure/ui-test-utils": "8.52.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1"
},
"dependencies": {
"@babel/runtime": "^7.23.2",
Expand All @@ -43,6 +46,7 @@
"@instructure/ui-testable": "8.52.0",
"@instructure/ui-time-select": "8.52.0",
"@instructure/ui-utils": "8.52.0",
"@instructure/console": "8.52.0",
"prop-types": "^15.8.1"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 - present Instructure, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import { userEvent } from '@testing-library/user-event'
import DateTimeInput from '../index'

describe('<DateTimeInput />', () => {
it("should change value of TimeSelect to initialTimeForNewDate prop's value", async () => {
const locale = 'en-US'
const timezone = 'US/Eastern'

render(
<DateTimeInput
description="date time"
prevMonthLabel="Previous month"
nextMonthLabel="Next month"
dateRenderLabel="date"
timeRenderLabel="time"
invalidDateTimeMessage="whoops"
locale={locale}
timezone={timezone}
initialTimeForNewDate="05:05"
/>
)

const input = screen.getAllByRole('combobox')[0]

fireEvent.click(input)

const firstDay = screen.getByText('15')

await userEvent.click(firstDay)

const allInputs = screen.getAllByRole('combobox')
const targetInput = allInputs.find(
(input) => (input as HTMLInputElement).value === '5:05 AM'
)
expect(targetInput).toBeInTheDocument()
})

it("should throw warning if initialTimeForNewDate prop's value is not HH:MM", async () => {
const locale = 'en-US'
const timezone = 'US/Eastern'

const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {})

const initialTimeForNewDate = 'WRONG_FORMAT'

render(
<DateTimeInput
description="date time"
prevMonthLabel="Previous month"
nextMonthLabel="Next month"
dateRenderLabel="date"
timeRenderLabel="time"
invalidDateTimeMessage="whoops"
locale={locale}
timezone={timezone}
initialTimeForNewDate={initialTimeForNewDate}
/>
)

expect(consoleError.mock.calls[0][2]).toContain(
`Invalid prop \`initialTimeForNewDate\` \`${initialTimeForNewDate}\` supplied to \`DateTimeInput\`, expected a HH:MM formatted string.`
)

const input = screen.getAllByRole('combobox')[0]

fireEvent.click(input)

const firstDay = screen.getByText('15')

await userEvent.click(firstDay)

expect(consoleError.mock.calls[1][0]).toBe(
`Warning: [DateTimeInput] initialTimeForNewDate prop is not in the correct format. Please use HH:MM format.`
)
})

it('should throw warning if initialTimeForNewDate prop hour and minute values are not in interval', async () => {
const locale = 'en-US'
const timezone = 'US/Eastern'

const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {})

const initialTimeForNewDate = '99:99'

render(
<DateTimeInput
description="date time"
prevMonthLabel="Previous month"
nextMonthLabel="Next month"
dateRenderLabel="date"
timeRenderLabel="time"
invalidDateTimeMessage="whoops"
locale={locale}
timezone={timezone}
initialTimeForNewDate={initialTimeForNewDate}
/>
)

const input = screen.getAllByRole('combobox')[0]

fireEvent.click(input)

const firstDay = screen.getByText('15')

await userEvent.click(firstDay)

expect(consoleError.mock.calls[0][0]).toContain(
`Warning: [DateTimeInput] 0 <= hour < 24 and 0 <= minute < 60 for initialTimeForNewDate prop.`
)
})

/*
* TODO write this test with Cypress
* Steps:
* 0. Set initialTimeForNewDate and defaultValue props -> check if defaultValue is rendered in the date input and time input fields
* 1. Clear date input field
* 2. Click on an area outside of the component
* 3. Check if time input field got cleared as well
* 4. Click on the date input and select a date
* 5. Observe if the time input has the value of initialTimeForNewDate
*/

// it('should merge defaultValue and initialTimeForNewDate when the user clears date input', async () => {
// const locale = 'en-US'
// const timezone = 'US/Eastern'
//
// // const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {})
//
// const initialTimeForNewDate = '16:16'
// const defaultValue = '2018-01-18T13:30'
//
// const {container} = render(
// <DateTimeInput
// description="date time"
// prevMonthLabel="Previous month"
// nextMonthLabel="Next month"
// dateRenderLabel="date"
// timeRenderLabel="time"
// invalidDateTimeMessage="whoops"
// defaultValue={defaultValue}
// locale={locale}
// timezone={timezone}
// initialTimeForNewDate={initialTimeForNewDate}
// />
// )
//
// const dateInput = container.querySelector('input')
//
// const timeInput = container.querySelectorAll('input')[1]
//
// fireEvent.change(dateInput as Element, { target: { value: '' } })
//
//
// await waitFor(() => {
// fireEvent.keyDown(dateInput as Element, { key: 'Escape' })
//
// expect(timeInput).toHaveValue('')
// })
//
// // fireEvent.
// // fireEvent.change(timeInput as Element, { target: { value: '' } })
//
//
// const firstDay = screen.getByText('15')
//
// await userEvent.click(firstDay)
//
// const time = screen.getByText('4:16 PM')
//
// expect(time).toBeInTheDocument()
//
// // expect(consoleWarn.mock.calls[0][0]).toContain(
// // `Warning: [DateTimeInput] if defaultValue is set, initialTimeForNewDate will be ignored.`
// // )
// })

afterEach(() => {
jest.resetAllMocks()
})
})
18 changes: 18 additions & 0 deletions packages/ui-date-time-input/src/DateTimeInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from '@instructure/ui-icons'
import type { DateTimeInputProps, DateTimeInputState } from './props'
import { propTypes, allowedProps } from './props'
import { error } from '@instructure/console'

/**
---
Expand Down Expand Up @@ -134,6 +135,23 @@ class DateTimeInput extends Component<DateTimeInputProps, DateTimeInputState> {
.month(this.state.iso.month())
.year(this.state.iso.year())
}
if (this.props.initialTimeForNewDate && !this.state?.timeSelectValue) {
const hour = Number(this.props.initialTimeForNewDate.slice(0, 2))
const minute = Number(this.props.initialTimeForNewDate.slice(3, 5))
if (isNaN(hour) || isNaN(minute)) {
error(
false,
`[DateTimeInput] initialTimeForNewDate prop is not in the correct format. Please use HH:MM format.`
)
} else if (hour < 0 || hour > 23 || minute > 59 || minute < 0) {
error(
false,
`[DateTimeInput] 0 <= hour < 24 and 0 <= minute < 60 for initialTimeForNewDate prop.`
)
} else {
parsed.hour(hour).minute(minute)
}
}
const newTimeSelectValue = parsed.toISOString()
if (this.isDisabledDate(parsed)) {
let text =
Expand Down
30 changes: 28 additions & 2 deletions packages/ui-date-time-input/src/DateTimeInput/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type { FormMessage } from '@instructure/ui-form-field'
import type { InteractionType } from '@instructure/ui-react-utils'
import { I18nPropTypes } from '@instructure/ui-i18n'
import type { Moment } from '@instructure/ui-i18n'
import PropTypes from 'prop-types'
import PropTypes, { Validator } from 'prop-types'
import { controllable } from '@instructure/ui-prop-types'
import type { PropValidators, Renderable } from '@instructure/shared-types'

Expand Down Expand Up @@ -242,6 +242,11 @@ type DateTimeInputProps = {
* Default is `undefined` which equals to `false`
*/
allowNonStepInput?: boolean
/**
* The default time to be prefilled if a day is selected. The time input has to be empty for this to be applied.
* An error is thrown if the time format is not HH:MM.
*/
initialTimeForNewDate?: string
}

type DateTimeInputState = {
Expand Down Expand Up @@ -269,6 +274,26 @@ type DateTimeInputState = {
type PropKeys = keyof DateTimeInputProps
type AllowedPropKeys = Readonly<Array<PropKeys>>

const hourMinuteValidator: Validator<string> = function (
props,
propName,
componentName,
location
) {
const propValue = props[propName]
if (typeof propValue === 'undefined' || propValue === '') return null

const hourMinuteRegex = /^\d{2}:\d{2}$/

if (typeof propValue === 'string' && !propValue.match(hourMinuteRegex)) {
return new Error(
`Invalid ${location} \`${propName}\` \`${propValue}\` supplied to \`${componentName}\`, expected ` +
`a HH:MM formatted string.`
)
}
return null
}

const propTypes: PropValidators<PropKeys> = {
description: PropTypes.node.isRequired,
dateRenderLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.node])
Expand Down Expand Up @@ -313,7 +338,8 @@ const propTypes: PropValidators<PropKeys> = {
PropTypes.string,
PropTypes.func
]),
allowNonStepInput: PropTypes.bool
allowNonStepInput: PropTypes.bool,
initialTimeForNewDate: hourMinuteValidator
}

const allowedProps: AllowedPropKeys = [
Expand Down
3 changes: 2 additions & 1 deletion packages/ui-date-time-input/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
{ "path": "../ui-react-utils/tsconfig.build.json" },
{ "path": "../ui-testable/tsconfig.build.json" },
{ "path": "../ui-time-select/tsconfig.build.json" },
{ "path": "../ui-utils/tsconfig.build.json" }
{ "path": "../ui-utils/tsconfig.build.json" },
{ "path": "../console/tsconfig.build.json" }
]
}

0 comments on commit 33711a3

Please sign in to comment.