diff --git a/cypress/component/TimeSelect.cy.tsx b/cypress/component/TimeSelect.cy.tsx index dc2b28a1ff..2ed54c22be 100644 --- a/cypress/component/TimeSelect.cy.tsx +++ b/cypress/component/TimeSelect.cy.tsx @@ -26,8 +26,8 @@ import moment from 'moment-timezone' import 'cypress-real-events' import '../support/component' -import { TimeSelect } from '../../packages/ui' -import { DateTime } from '../../packages/ui-i18n' +import { TimeSelect } from '@instructure/ui' +import { DateTime } from '@instructure/ui-i18n' describe('', () => { it('should render an input and list', async () => { @@ -70,6 +70,34 @@ describe('', () => { }) }) + it('should fire onChange when input field is cleared and blurred and allowClearingSelection is true', async () => { + const onChange = cy.spy() + cy.mount( + + ) + cy.get('input[id^="Select_"]').as('input') + + cy.get('@input').click() + + cy.get('li[class$="-optionItem"]').eq(0).click() + + cy.get('@input').click() + cy.get('@input').clear() + cy.get('@input').blur() + + cy.wrap(onChange) + .should('have.been.called') + .then((spy) => { + expect(spy.lastCall.args[1]).to.have.property('value', '') + expect(spy.lastCall.args[1]).to.have.property('inputText', '') + }) + }) + it('should behave uncontrolled', async () => { const onChange = cy.spy() cy.mount() diff --git a/docs/contributor-docs/v11-upgrade-guide.md b/docs/contributor-docs/v11-upgrade-guide.md index e2659b5552..fa856849db 100644 --- a/docs/contributor-docs/v11-upgrade-guide.md +++ b/docs/contributor-docs/v11-upgrade-guide.md @@ -31,3 +31,4 @@ isWIP: true ## Changes - `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. +- 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. diff --git a/packages/ui-time-select/src/TimeSelect/index.tsx b/packages/ui-time-select/src/TimeSelect/index.tsx index fdea8d2e7a..59350dba1d 100644 --- a/packages/ui-time-select/src/TimeSelect/index.tsx +++ b/packages/ui-time-select/src/TimeSelect/index.tsx @@ -74,7 +74,8 @@ class TimeSelect extends Component { placement: 'bottom stretch', constrain: 'window', renderEmptyOption: '---', - allowNonStepInput: false + allowNonStepInput: false, + allowClearingSelection: false } static contextType = ApplyLocaleContext @@ -220,7 +221,8 @@ class TimeSelect extends Component { : initialOptions, isShowingOptions: false, highlightedOptionId: initialSelection ? initialSelection.id : undefined, - selectedOptionId: initialSelection ? initialSelection.id : undefined + selectedOptionId: initialSelection ? initialSelection.id : undefined, + isInputCleared: false } } @@ -338,7 +340,8 @@ class TimeSelect extends Component { selectedOptionId: this.isControlled ? this.state.selectedOptionId : undefined, - fireChangeOnBlur: undefined + fireChangeOnBlur: undefined, + isInputCleared: this.props.allowClearingSelection && value === '' }) } this.setState({ @@ -386,7 +389,7 @@ class TimeSelect extends Component { // when pressing ESC. NOT called when an item is selected via Enter/click, // (but in this case it will be called later when the input is blurred.) handleBlurOrEsc: SelectProps['onRequestHideOptions'] = (event) => { - const { selectedOptionId, inputValue } = this.state + const { selectedOptionId, inputValue, isInputCleared } = this.state let defaultValue = '' if (this.props.defaultValue) { const date = DateTime.parse( @@ -400,14 +403,23 @@ class TimeSelect extends Component { } const selectedOption = this.getOption('id', selectedOptionId) let newInputValue = defaultValue - if (selectedOption) { - // If there is a selected option use its value in the input field. - newInputValue = selectedOption.label - } // if input was completely cleared, ensure it stays clear // e.g. defaultValue defined, but no selection yet made - else if (inputValue === '') { + if (inputValue === '' && this.props.allowClearingSelection) { newInputValue = '' + } else if (selectedOption) { + // If there is a selected option use its value in the input field. + newInputValue = selectedOption.label + } else if (this.props.value) { + // If controlled and input is cleared and blurred after the first render, it should revert to value + const date = DateTime.parse( + this.props.value, + this.locale(), + this.timezone() + ) + newInputValue = this.props.format + ? date.format(this.props.format) + : date.toISOString() } this.setState(() => ({ isShowingOptions: false, @@ -421,6 +433,16 @@ class TimeSelect extends Component { value: this.state.fireChangeOnBlur.value.toISOString(), inputText: this.state.fireChangeOnBlur.label }) + } else if ( + isInputCleared && + (event as any).key !== 'Escape' && + this.props.allowClearingSelection + ) { + this.setState(() => ({ isInputCleared: false })) + this.props.onChange?.(event, { + value: '', + inputText: '' + }) } // TODO only fire this if handleSelectOption was not called before. this.props.onHideOptions?.(event) diff --git a/packages/ui-time-select/src/TimeSelect/props.ts b/packages/ui-time-select/src/TimeSelect/props.ts index 7c69668f3c..b0fd16c0b0 100644 --- a/packages/ui-time-select/src/TimeSelect/props.ts +++ b/packages/ui-time-select/src/TimeSelect/props.ts @@ -244,6 +244,11 @@ type TimeSelectOwnProps = { */ valueAsISOString?: string ) => void + /** + * Whether to allow for the user to clear the selected option in the input field. + * If `false`, the input field will return the last selected option after the input is cleared and loses focus. + */ + allowClearingSelection: boolean } const propTypes: PropValidators = { @@ -278,7 +283,8 @@ const propTypes: PropValidators = { locale: PropTypes.string, timezone: PropTypes.string, allowNonStepInput: PropTypes.bool, - onInputChange: PropTypes.func + onInputChange: PropTypes.func, + allowClearingSelection: PropTypes.bool } const allowedProps: AllowedPropKeys = [ @@ -313,7 +319,8 @@ const allowedProps: AllowedPropKeys = [ 'locale', 'timezone', 'allowNonStepInput', - 'onInputChange' + 'onInputChange', + 'allowClearingSelection' ] type TimeSelectOptions = { @@ -356,6 +363,10 @@ type TimeSelectState = { * fire onChange event when the popup closes? */ fireChangeOnBlur?: TimeSelectOptions + /** + * Whether to selected option is cleared + */ + isInputCleared: boolean } export type { TimeSelectProps, TimeSelectState, TimeSelectOptions }