Skip to content

Commit 1993282

Browse files
ToMESSKamatyasf
authored andcommitted
fix(ui-time-select): clear input field after setting an empty value
1 parent a3a75e0 commit 1993282

File tree

4 files changed

+75
-13
lines changed

4 files changed

+75
-13
lines changed

cypress/component/TimeSelect.cy.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import moment from 'moment-timezone'
2626
import 'cypress-real-events'
2727

2828
import '../support/component'
29-
import { TimeSelect } from '../../packages/ui'
30-
import { DateTime } from '../../packages/ui-i18n'
29+
import { TimeSelect } from '@instructure/ui'
30+
import { DateTime } from '@instructure/ui-i18n'
3131

3232
describe('<TimeSelect/>', () => {
3333
it('should render an input and list', async () => {
@@ -70,6 +70,34 @@ describe('<TimeSelect/>', () => {
7070
})
7171
})
7272

73+
it('should fire onChange when input field is cleared and blurred and allowClearingSelection is true', async () => {
74+
const onChange = cy.spy()
75+
cy.mount(
76+
<TimeSelect
77+
renderLabel="Choose a time"
78+
timezone="US/Eastern"
79+
onChange={onChange}
80+
allowClearingSelection={true}
81+
/>
82+
)
83+
cy.get('input[id^="Select_"]').as('input')
84+
85+
cy.get('@input').click()
86+
87+
cy.get('li[class$="-optionItem"]').eq(0).click()
88+
89+
cy.get('@input').click()
90+
cy.get('@input').clear()
91+
cy.get('@input').blur()
92+
93+
cy.wrap(onChange)
94+
.should('have.been.called')
95+
.then((spy) => {
96+
expect(spy.lastCall.args[1]).to.have.property('value', '')
97+
expect(spy.lastCall.args[1]).to.have.property('inputText', '')
98+
})
99+
})
100+
73101
it('should behave uncontrolled', async () => {
74102
const onChange = cy.spy()
75103
cy.mount(<TimeSelect renderLabel="Choose a time" onChange={onChange} />)

docs/contributor-docs/v11-upgrade-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ isWIP: true
3131
## Changes
3232

3333
- `ui-dom-utils`/`getComputedStyle` can now return `undefined`: In previous versions sometimes returned an empty object which could lead to runtime exceptions when one tried to call methods of `CSSStyleDeclaration` on it.
34+
- TO-DO: [TimeSelect](/#TimeSelect) as a controlled component can now return an '' instead of a valid date when its input field is cleared. In previous versions it always reverted to the last selected value when the input field was cleared and lost focus.

packages/ui-time-select/src/TimeSelect/index.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
7474
placement: 'bottom stretch',
7575
constrain: 'window',
7676
renderEmptyOption: '---',
77-
allowNonStepInput: false
77+
allowNonStepInput: false,
78+
allowClearingSelection: false
7879
}
7980
static contextType = ApplyLocaleContext
8081

@@ -220,7 +221,8 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
220221
: initialOptions,
221222
isShowingOptions: false,
222223
highlightedOptionId: initialSelection ? initialSelection.id : undefined,
223-
selectedOptionId: initialSelection ? initialSelection.id : undefined
224+
selectedOptionId: initialSelection ? initialSelection.id : undefined,
225+
isInputCleared: false
224226
}
225227
}
226228

@@ -338,7 +340,8 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
338340
selectedOptionId: this.isControlled
339341
? this.state.selectedOptionId
340342
: undefined,
341-
fireChangeOnBlur: undefined
343+
fireChangeOnBlur: undefined,
344+
isInputCleared: this.props.allowClearingSelection && value === ''
342345
})
343346
}
344347
this.setState({
@@ -386,7 +389,7 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
386389
// when pressing ESC. NOT called when an item is selected via Enter/click,
387390
// (but in this case it will be called later when the input is blurred.)
388391
handleBlurOrEsc: SelectProps['onRequestHideOptions'] = (event) => {
389-
const { selectedOptionId, inputValue } = this.state
392+
const { selectedOptionId, inputValue, isInputCleared } = this.state
390393
let defaultValue = ''
391394
if (this.props.defaultValue) {
392395
const date = DateTime.parse(
@@ -400,14 +403,23 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
400403
}
401404
const selectedOption = this.getOption('id', selectedOptionId)
402405
let newInputValue = defaultValue
403-
if (selectedOption) {
404-
// If there is a selected option use its value in the input field.
405-
newInputValue = selectedOption.label
406-
}
407406
// if input was completely cleared, ensure it stays clear
408407
// e.g. defaultValue defined, but no selection yet made
409-
else if (inputValue === '') {
408+
if (inputValue === '' && this.props.allowClearingSelection) {
410409
newInputValue = ''
410+
} else if (selectedOption) {
411+
// If there is a selected option use its value in the input field.
412+
newInputValue = selectedOption.label
413+
} else if (this.props.value) {
414+
// If controlled and input is cleared and blurred after the first render, it should revert to value
415+
const date = DateTime.parse(
416+
this.props.value,
417+
this.locale(),
418+
this.timezone()
419+
)
420+
newInputValue = this.props.format
421+
? date.format(this.props.format)
422+
: date.toISOString()
411423
}
412424
this.setState(() => ({
413425
isShowingOptions: false,
@@ -421,6 +433,16 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
421433
value: this.state.fireChangeOnBlur.value.toISOString(),
422434
inputText: this.state.fireChangeOnBlur.label
423435
})
436+
} else if (
437+
isInputCleared &&
438+
(event as any).key !== 'Escape' &&
439+
this.props.allowClearingSelection
440+
) {
441+
this.setState(() => ({ isInputCleared: false }))
442+
this.props.onChange?.(event, {
443+
value: '',
444+
inputText: ''
445+
})
424446
}
425447
// TODO only fire this if handleSelectOption was not called before.
426448
this.props.onHideOptions?.(event)

packages/ui-time-select/src/TimeSelect/props.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ type TimeSelectOwnProps = {
244244
*/
245245
valueAsISOString?: string
246246
) => void
247+
/**
248+
* Whether to allow for the user to clear the selected option in the input field.
249+
* If `false`, the input field will return the last selected option after the input is cleared and loses focus.
250+
*/
251+
allowClearingSelection: boolean
247252
}
248253

249254
const propTypes: PropValidators<PropKeys> = {
@@ -278,7 +283,8 @@ const propTypes: PropValidators<PropKeys> = {
278283
locale: PropTypes.string,
279284
timezone: PropTypes.string,
280285
allowNonStepInput: PropTypes.bool,
281-
onInputChange: PropTypes.func
286+
onInputChange: PropTypes.func,
287+
allowClearingSelection: PropTypes.bool
282288
}
283289

284290
const allowedProps: AllowedPropKeys = [
@@ -313,7 +319,8 @@ const allowedProps: AllowedPropKeys = [
313319
'locale',
314320
'timezone',
315321
'allowNonStepInput',
316-
'onInputChange'
322+
'onInputChange',
323+
'allowClearingSelection'
317324
]
318325

319326
type TimeSelectOptions = {
@@ -356,6 +363,10 @@ type TimeSelectState = {
356363
* fire onChange event when the popup closes?
357364
*/
358365
fireChangeOnBlur?: TimeSelectOptions
366+
/**
367+
* Whether to selected option is cleared
368+
*/
369+
isInputCleared: boolean
359370
}
360371

361372
export type { TimeSelectProps, TimeSelectState, TimeSelectOptions }

0 commit comments

Comments
 (0)