Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Navigate after date picker selection (native JS behavior)
Browse files Browse the repository at this point in the history
  • Loading branch information
MadLittleMods committed Dec 10, 2021
1 parent 594c7c7 commit 23da673
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 18 deletions.
14 changes: 12 additions & 2 deletions src/components/views/elements/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,7 +97,16 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElem
value: string;
}

type PropShapes = IInputProps | ISelectProps | ITextareaProps;
export interface ICustomInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
// 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<HTMLInputElement>;
}

type PropShapes = IInputProps | ISelectProps | ITextareaProps | ICustomInputProps;

interface IState {
valid: boolean;
Expand Down Expand Up @@ -256,7 +266,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
}

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.
Expand Down
113 changes: 97 additions & 16 deletions src/components/views/messages/DateSeparator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <input type="date"> 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<Omit<React.InputHTMLAttributes<HTMLInputElement>, '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 <input ref={this.registerCallbacks} {...this.props} onChange={() => {}} onInput={() => {}} />;
}
}

function getDaysArray(): string[] {
return [
_t('Sunday'),
Expand All @@ -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
}

Expand All @@ -65,10 +101,28 @@ export default class DateSeparator extends React.Component<IProps, IState> {
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);

Expand Down Expand Up @@ -136,25 +190,45 @@ export default class DateSeparator extends React.Component<IProps, IState> {
}
};

private onDateValueChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): 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<HTMLSelectElement | HTMLInputElement>): 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<HTMLSelectElement | HTMLInputElement>): 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 => {
Expand Down Expand Up @@ -217,14 +291,21 @@ export default class DateSeparator extends React.Component<IProps, IState> {
>
<form className="mx_DateSeparator_datePickerForm" onSubmit={this.onJumpToDateSubmit}>
<Field
element={CustomInput}
type="date"
onChange={this.onDateValueChange}
onInput={this.onDateValueInput}
onKeyDown={this.onDateInputKeyDown}
value={this.state.dateValue}
className="mx_DateSeparator_datePicker"
label={_t("Pick a date to jump to")}
autoFocus={true}
/>
<AccessibleButton kind="primary" className="mx_DateSeparator_datePickerSubmitButton" onClick={this.onJumpToDateSubmit}>
<AccessibleButton
kind="primary"
className="mx_DateSeparator_datePickerSubmitButton"
onClick={this.onJumpToDateSubmit}
>
{ _t("Go") }
</AccessibleButton>
</form>
Expand Down

0 comments on commit 23da673

Please sign in to comment.