From 23da67328e93d324fc3642226ca046ff3659f8e6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 9 Dec 2021 21:42:23 -0600 Subject: [PATCH] Navigate after date picker selection (native JS behavior) --- src/components/views/elements/Field.tsx | 14 ++- .../views/messages/DateSeparator.tsx | 113 +++++++++++++++--- 2 files changed, 109 insertions(+), 18 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 512cc915039..a09791948b8 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -19,6 +19,7 @@ import classNames from 'classnames'; import * as sdk from '../../../index'; import { debounce } from "lodash"; import { IFieldState, IValidationResult } from "./Validation"; +import { ComponentClass } from "../../../@types/common"; // Invoke validation from user input (when typing, etc.) at most once every N ms. const VALIDATION_THROTTLE_MS = 200; @@ -96,7 +97,16 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes { + // The element to create. + element: ComponentClass; + // The input's value. This is a controlled component, so the value is required. + value: string; + // Optionally can be used for the CustomInput + onInput?: React.ChangeEventHandler; +} + +type PropShapes = IInputProps | ISelectProps | ITextareaProps | ICustomInputProps; interface IState { valid: boolean; @@ -256,7 +266,7 @@ export default class Field extends React.PureComponent { } const hasValidationFlag = forceValidity !== null && forceValidity !== undefined; - const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, { + const fieldClasses = classNames("mx_Field", `mx_Field_${typeof this.props.element === "string" ? this.props.element : "input"}`, className, { // If we have a prefix element, leave the label always at the top left and // don't animate it, as it looks a bit clunky and would add complexity to do // properly. diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 03da621156d..81cb1348995 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -37,6 +37,38 @@ import IconizedContextMenu, { IconizedContextMenuRadio, } from "../context_menus/IconizedContextMenu"; +interface CustomInputProps { + onChange?: (event: Event) => void; + onInput?: (event: Event) => void; +} +/** + * This component restores the native 'onChange' and 'onInput' behavior of + * JavaScript. via https://stackoverflow.com/a/62383569/796832 and + * https://github.com/facebook/react/issues/9657#issuecomment-643970199 + * + * See: + * - https://reactjs.org/docs/dom-elements.html#onchange + * - https://github.com/facebook/react/issues/3964 + * - https://github.com/facebook/react/issues/9657 + * - https://github.com/facebook/react/issues/14857 + * + * We use this for the date picker so we can distinguish + * from a final date picker selection vs navigating the months in the date + * picker which trigger an `input`(and `onChange` in React). + */ +class CustomInput extends React.Component, 'onChange' | 'onInput' | 'ref'> & CustomInputProps> { + private readonly registerCallbacks = (element: HTMLInputElement | null) => { + if (element) { + element.onchange = this.props.onChange ? this.props.onChange : null; + element.oninput = this.props.onInput ? this.props.onInput : null; + } + }; + + public render() { + return {}} onInput={() => {}} />; + } +} + function getDaysArray(): string[] { return [ _t('Sunday'), @@ -57,6 +89,10 @@ interface IProps { interface IState { dateValue: string, + // Whether or not to automatically navigate to the given date after someone + // selects a day in the date picker. We want to disable this after someone + // starts manually typing in the input instead of picking. + navigateOnDatePickerSelection: boolean, contextMenuPosition?: DOMRect } @@ -65,10 +101,28 @@ export default class DateSeparator extends React.Component { constructor(props, context) { super(props, context); this.state = { - dateValue: this.getDefaultDateValue() + dateValue: this.getDefaultDateValue(), + navigateOnDatePickerSelection: true }; } + private onContextMenuOpenClick = (e: React.MouseEvent): void => { + e.preventDefault(); + e.stopPropagation(); + const target = e.target as HTMLButtonElement; + this.setState({ contextMenuPosition: target.getBoundingClientRect() }); + }; + + private onContextMenuCloseClick = (): void => { + this.closeMenu(); + }; + + private closeMenu = (): void => { + this.setState({ + contextMenuPosition: null, + }); + }; + private getLabel(): string { const date = new Date(this.props.ts); @@ -136,25 +190,45 @@ export default class DateSeparator extends React.Component { } }; - private onDateValueChange = (e: React.ChangeEvent): void => { + // Since we're using CustomInput with native JavaScript behavior, this + // tracks the date value changes as they come in. + private onDateValueInput = (e: React.ChangeEvent): void => { + console.log('onDateValueInput') this.setState({ dateValue: e.target.value }); }; - - private onContextMenuOpenClick = (ev: React.MouseEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - const target = ev.target as HTMLButtonElement; - this.setState({ contextMenuPosition: target.getBoundingClientRect() }); - }; - private closeMenu = (): void => { - this.setState({ - contextMenuPosition: null, - }); + // Since we're using CustomInput with native JavaScript behavior, the change + // event listener will trigger when a date is picked from the date picker + // or when the text is fully filled out. In order to not trigger early + // as someone is typing out a date, we need to disable when we see keydowns. + private onDateValueChange = (e: React.ChangeEvent): void => { + console.log('onDateValueChange') + this.setState({ dateValue: e.target.value }); + + // Don't auto navigate if they were manually typing out a date + if(this.state.navigateOnDatePickerSelection) { + this.pickDate(this.state.dateValue); + this.closeMenu(); + } }; - private onContextMenuCloseClick = (): void => { - this.closeMenu(); + private onDateInputKeyDown = (e: React.KeyboardEvent): void => { + // Ignore the tab key which is probably just navigating focus around + // with the keyboard + if(e.key === "Tab") { + return; + } + + // Go and navigate if they submitted + if(e.key === "Enter") { + this.pickDate(this.state.dateValue); + this.closeMenu(); + return; + } + + // When we see someone manually typing out a date, disable the auto + // submit on change. + this.setState({ navigateOnDatePickerSelection: false }); }; private onLastWeekClicked = (): void => { @@ -217,14 +291,21 @@ export default class DateSeparator extends React.Component { >
- + { _t("Go") }