From 7dfbe7a0680596cffab2f7b506027622384774de Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:15:16 +0000 Subject: [PATCH 01/47] Fix room list roving treeview New TooltipTarget & TextWithTooltip were not roving-accessible --- .../views/avatars/DecoratedRoomAvatar.tsx | 3 +++ src/components/views/elements/TextWithTooltip.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 15 +++++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 99f2b70efcc..6ba507c0cc2 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -31,6 +31,7 @@ import TextWithTooltip from "../elements/TextWithTooltip"; import DMRoomMap from "../../../utils/DMRoomMap"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; +import TooltipTarget from "../elements/TooltipTarget"; interface IProps { room: Room; @@ -39,6 +40,7 @@ interface IProps { forceCount?: boolean; oobData?: IOOBData; viewAvatarOnClick?: boolean; + tooltipProps?: Omit, "label" | "tooltipClassName" | "className">; } interface IState { @@ -189,6 +191,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent; } diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index d5a37e16e79..2b5926f3d77 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -24,7 +24,7 @@ interface IProps { class?: string; tooltipClass?: string; tooltip: React.ReactNode; - tooltipProps?: {}; + tooltipProps?: Omit, "label" | "tooltipClassName" | "className">; onClick?: (ev?: React.MouseEvent) => void; } diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index d6916f50348..25603c6e4c1 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -566,13 +566,6 @@ export default class RoomTile extends React.PureComponent { if (typeof name !== 'string') name = ''; name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - const roomAvatar = ; - let badge: React.ReactNode; if (!this.props.isMinimized && this.notificationState) { // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below @@ -663,7 +656,13 @@ export default class RoomTile extends React.PureComponent { aria-selected={this.state.selected} aria-describedby={ariaDescribedBy} > - { roomAvatar } + { nameContainer } { badge } { this.renderGeneralMenu() } From e6e788536fbf5f9b2000f68d3a23119a842e2dd0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:27:38 +0000 Subject: [PATCH 02/47] Fix programmatic focus management in roving tab index not triggering onFocus handler --- src/accessibility/RovingTabIndex.tsx | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index cdd937bba3b..68ebe43d6af 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -131,6 +131,7 @@ export const reducer = (state: IState, action: IAction) => { } case Type.SetFocus: { + if (state.activeRef === action.payload.ref) return state; // update active ref state.activeRef = action.payload.ref; return { ...state }; @@ -194,6 +195,7 @@ export const RovingTabIndexProvider: React.FC = ({ } let handled = false; + let focusRef: RefObject; // Don't interfere with input default keydown behaviour if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { // check if we actually have any items @@ -202,7 +204,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (handleHomeEnd) { handled = true; // move focus to first (visible) item - findSiblingElement(context.state.refs, 0)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, 0); } break; @@ -210,7 +212,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (handleHomeEnd) { handled = true; // move focus to last (visible) item - findSiblingElement(context.state.refs, context.state.refs.length - 1, true)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, context.state.refs.length - 1, true); } break; @@ -220,7 +222,7 @@ export const RovingTabIndexProvider: React.FC = ({ handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); - findSiblingElement(context.state.refs, idx - 1)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, idx + 1); } } break; @@ -231,7 +233,7 @@ export const RovingTabIndexProvider: React.FC = ({ handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); - findSiblingElement(context.state.refs, idx + 1, true)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, idx - 1, true); } } break; @@ -242,7 +244,17 @@ export const RovingTabIndexProvider: React.FC = ({ ev.preventDefault(); ev.stopPropagation(); } - }, [context.state, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight]); + + if (focusRef) { + focusRef.current?.focus(); + dispatch({ + type: Type.SetFocus, + payload: { + ref: focusRef, + }, + }); + } + }, [context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight]); return { children({ onKeyDownHandler }) } @@ -283,7 +295,7 @@ export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] type: Type.SetFocus, payload: { ref }, }); - }, [ref, context]); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const isActive = context.state.activeRef === ref; return [onFocus, isActive, ref]; From 08b1fdb4b733fc1074fd6e14ad60e04b2e225d10 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:28:06 +0000 Subject: [PATCH 03/47] Fix toolbar no longer handling left & right arrows --- src/accessibility/Toolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 6e99c7f1fa2..c0f2b567484 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -52,7 +52,7 @@ const Toolbar: React.FC = ({ children, ...props }) => { } }; - return + return { ({ onKeyDownHandler }) =>
{ children }
} From 7c226b94e9dfb2b75c466724aba13760261c3b6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:29:27 +0000 Subject: [PATCH 04/47] Fix roving tab index focus tracking on interactive element like context menu trigger --- src/components/views/messages/MessageActionBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index abaf78797e2..74217b131a0 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -67,8 +67,9 @@ const OptionsButton: React.FC = ({ const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + onFocus(); onFocusChange(menuDisplayed); - }, [onFocusChange, menuDisplayed]); + }, [onFocus, onFocusChange, menuDisplayed]); let contextMenu: ReactElement | null; if (menuDisplayed) { From 4ddc97de844b56535af732b714e33b8750700381 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:42:56 +0000 Subject: [PATCH 05/47] Fix thread list context menu roving --- .../context_menus/ThreadListContextMenu.tsx | 62 ++++++++++++------- src/components/views/rooms/EventTile.tsx | 4 +- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index f9aa7a4b9fc..012b2dbae4e 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useEffect, useState } from "react"; +import React, { RefObject, useCallback, useEffect } from "react"; import { MatrixEvent } from "matrix-js-sdk/src"; import { ButtonEvent } from "../elements/AccessibleButton"; @@ -22,11 +22,12 @@ import dis from '../../../dispatcher/dispatcher'; import { Action } from "../../../dispatcher/actions"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../../utils/strings"; -import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu"; +import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import { _t } from "../../../languageHandler"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; interface IProps { mxEvent: MatrixEvent; @@ -34,6 +35,13 @@ interface IProps { onMenuToggle?: (open: boolean) => void; } +interface IExtendedProps extends IProps { + // Props for making the button into a roving one + tabIndex?: number; + inputRef?: RefObject; + onFocus?(): void; +} + const contextMenuBelow = (elementRect: DOMRect) => { // align the context menu's icons with the icon which opened the context menu const left = elementRect.left + window.pageXOffset + elementRect.width; @@ -42,11 +50,27 @@ const contextMenuBelow = (elementRect: DOMRect) => { return { left, top, chevronFace }; }; -const ThreadListContextMenu: React.FC = ({ mxEvent, permalinkCreator, onMenuToggle }) => { - const [optionsPosition, setOptionsPosition] = useState(null); - const closeThreadOptions = useCallback(() => { - setOptionsPosition(null); - }, []); +export const RovingThreadListContextMenu: React.FC = (props) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + + return ; +}; + +const ThreadListContextMenu: React.FC = ({ + mxEvent, + permalinkCreator, + onMenuToggle, + onFocus, + inputRef, + ...props +}) => { + const [menuDisplayed, _ref, openMenu, closeThreadOptions] = useContextMenu(); + const button = inputRef ?? _ref; // prefer the ref we receive via props in case we are being controlled const viewInRoom = useCallback((evt: ButtonEvent): void => { evt.preventDefault(); @@ -68,37 +92,31 @@ const ThreadListContextMenu: React.FC = ({ mxEvent, permalinkCreator, on closeThreadOptions(); }, [mxEvent, closeThreadOptions, permalinkCreator]); - const toggleOptionsMenu = useCallback((ev: ButtonEvent): void => { - if (!!optionsPosition) { - closeThreadOptions(); - } else { - const position = ev.currentTarget.getBoundingClientRect(); - setOptionsPosition(position); - } - }, [closeThreadOptions, optionsPosition]); - useEffect(() => { if (onMenuToggle) { - onMenuToggle(!!optionsPosition); + onMenuToggle(menuDisplayed); } - }, [optionsPosition, onMenuToggle]); + onFocus?.(); + }, [menuDisplayed, onMenuToggle, onFocus]); const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget( MatrixClientPeg.get().getRoom(mxEvent.getRoomId()), ); return - { !!optionsPosition && ( { isMainSplitTimelineShown && diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 077ef3d1c8a..4c134f62264 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -66,7 +66,7 @@ import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import Toolbar from '../../../accessibility/Toolbar'; import { POLL_START_EVENT_TYPE } from '../../../polls/consts'; import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton'; -import ThreadListContextMenu from '../context_menus/ThreadListContextMenu'; +import { RovingThreadListContextMenu } from '../context_menus/ThreadListContextMenu'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -1382,7 +1382,7 @@ export default class EventTile extends React.Component { onClick={() => dispatchShowThreadEvent(this.props.mxEvent)} key="thread" /> - Date: Fri, 10 Dec 2021 15:44:43 +0000 Subject: [PATCH 06/47] add comment --- src/accessibility/RovingTabIndex.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 68ebe43d6af..7eefe97f0f9 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -247,6 +247,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (focusRef) { focusRef.current?.focus(); + // programmatic focus doesn't fire the onFocus handler so we must do the do ourselves dispatch({ type: Type.SetFocus, payload: { From 1784b444ef054a4fc769044547888ef25686ebde Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:52:20 +0000 Subject: [PATCH 07/47] fix comment --- src/accessibility/RovingTabIndex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 7eefe97f0f9..769b0b683f9 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -247,7 +247,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (focusRef) { focusRef.current?.focus(); - // programmatic focus doesn't fire the onFocus handler so we must do the do ourselves + // programmatic focus doesn't fire the onFocus handler, so we must do the do ourselves dispatch({ type: Type.SetFocus, payload: { From b061c94526c44b341c58e2c51d54615e7ae68428 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 10 Dec 2021 21:26:57 -0600 Subject: [PATCH 08/47] Copy Jump to Date PoC changes on top of RovingTabIndex fixes Copy changes from https://github.com/matrix-org/matrix-react-sdk/pull/7317 on top of RovingTabIndex fixes in https://github.com/matrix-org/matrix-react-sdk/pull/7336 --- .../context_menus/_IconizedContextMenu.scss | 12 +- res/css/views/elements/_Field.scss | 1 + res/css/views/messages/_DateSeparator.scss | 34 +++ src/components/structures/MessagePanel.tsx | 10 +- src/components/views/elements/Field.tsx | 14 +- .../views/messages/DateSeparator.tsx | 275 +++++++++++++++++- .../views/rooms/SearchResultTile.tsx | 2 +- src/i18n/strings/en_EN.json | 8 +- src/utils/exportUtils/HtmlExport.tsx | 2 +- 9 files changed, 338 insertions(+), 20 deletions(-) diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index 56e98fa50ec..9dfda3b013a 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -50,21 +50,21 @@ limitations under the License. } // round the top corners of the top button for the hover effect to be bounded - &:first-child .mx_AccessibleButton:first-child { + &:first-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child { border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu } // round the bottom corners of the bottom button for the hover effect to be bounded - &:last-child .mx_AccessibleButton:last-child { + &:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):last-child { border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu } // round all corners of the only button for the hover effect to be bounded - &:first-child:last-child .mx_AccessibleButton:first-child:last-child { + &:first-child:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child:last-child { border-radius: 8px; // radius matches .mx_ContextualMenu } - .mx_AccessibleButton { + .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) { // pad the inside of the button so that the hover background is padded too padding-top: 12px; padding-bottom: 12px; @@ -130,7 +130,7 @@ limitations under the License. } .mx_IconizedContextMenu_optionList_red { - .mx_AccessibleButton { + .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) { color: $alert !important; } @@ -148,7 +148,7 @@ limitations under the License. } .mx_IconizedContextMenu_active { - &.mx_AccessibleButton, .mx_AccessibleButton { + &.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind), .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) { color: $accent !important; } diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index a97e7ee949e..1083a324fe2 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -127,6 +127,7 @@ limitations under the License. transform 0.25s ease-out 0s, background-color 0.25s ease-out 0s; font-size: $font-10px; + line-height: normal; transform: translateY(-13px); padding: 0 2px; background-color: $background; diff --git a/res/css/views/messages/_DateSeparator.scss b/res/css/views/messages/_DateSeparator.scss index 66501b40cb3..bd9b77227db 100644 --- a/res/css/views/messages/_DateSeparator.scss +++ b/res/css/views/messages/_DateSeparator.scss @@ -33,3 +33,37 @@ limitations under the License. margin: 0 25px; flex: 0 0 auto; } + +.mx_DateSeparator_jumpToDateMenu { + display: flex; +} + +.mx_DateSeparator_chevron { + align-self: center; + width: 16px; + height: 16px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + background-color: $tertiary-content; +} + +.mx_DateSeparator_jumpToDateMenuOption > .mx_IconizedContextMenu_label { + flex: initial; + width: auto; +} + +.mx_DateSeparator_datePickerForm { + display: flex; +} + +.mx_DateSeparator_datePicker { + flex: initial; + margin: 0; + margin-left: 8px; +} + +.mx_DateSeparator_datePickerSubmitButton { + margin-left: 8px; +} diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 7db09c6720b..996c53149ee 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -715,8 +715,8 @@ export default class MessagePanel extends React.Component { // do we need a date separator since the last event? const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); - if (wantsDateSeparator && !isGrouped) { - const dateSeparator =
  • ; + if (wantsDateSeparator && !isGrouped && this.props.room) { + const dateSeparator =
  • ; ret.push(dateSeparator); } @@ -1109,7 +1109,7 @@ class CreationGrouper extends BaseGrouper { if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) { const ts = createEvent.getTs(); ret.push( -
  • , +
  • , ); } @@ -1222,7 +1222,7 @@ class RedactionGrouper extends BaseGrouper { if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { const ts = this.events[0].getTs(); ret.push( -
  • , +
  • , ); } @@ -1318,7 +1318,7 @@ class MemberGrouper extends BaseGrouper { if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { const ts = this.events[0].getTs(); ret.push( -
  • , +
  • , ); } diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 85f397834f2..b827002c1e8 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -20,6 +20,7 @@ import { debounce } from "lodash"; import * as sdk from '../../../index'; 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; @@ -97,7 +98,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; @@ -257,7 +267,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 b20319e800e..e9438da5e1e 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -20,6 +20,54 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import { formatFullDateNoTime } from '../../../DateUtils'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; +import dis from '../../../dispatcher/dispatcher'; +import { Action } from '../../../dispatcher/actions'; + +import Field from "../elements/Field"; +import Modal from '../../../Modal'; +import ErrorDialog from '../dialogs/ErrorDialog'; +import AccessibleButton from "../elements/AccessibleButton"; +import { contextMenuBelow } from '../rooms/RoomTile'; +import { ContextMenuTooltipButton } from "../../structures/ContextMenu"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList, + 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 [ @@ -34,13 +82,48 @@ function getDaysArray(): string[] { } interface IProps { + roomId: string, ts: number; forExport?: boolean; } +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 +} + @replaceableComponent("views.messages.DateSeparator") -export default class DateSeparator extends React.Component { - private getLabel() { +export default class DateSeparator extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + 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); // During the time the archive is being viewed, a specific day might not make sense, so we return the full date @@ -62,12 +145,196 @@ export default class DateSeparator extends React.Component { } } + private getDefaultDateValue(): string { + const date = new Date(this.props.ts); + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, "0") + const day = `${date.getDate()}`.padStart(2, "0") + + return `${year}-${month}-${day}` + } + + private pickDate = async (inputTimestamp): Promise => { + console.log('pickDate', inputTimestamp) + + const unixTimestamp = new Date(inputTimestamp).getTime(); + + const cli = MatrixClientPeg.get(); + try { + const roomId = this.props.roomId + const { event_id, origin_server_ts } = await cli.timestampToEvent( + roomId, + unixTimestamp, + Direction.Forward + ); + console.log(`/timestamp_to_event: found ${event_id} (${origin_server_ts}) for timestamp=${unixTimestamp}`) + + dis.dispatch({ + action: Action.ViewRoom, + event_id, + highlighted: true, + room_id: roomId, + }); + } catch (e) { + const code = e.errcode || e.statusCode; + // only show the dialog if failing for something other than a network error + // (e.g. no errcode or statusCode) as in that case the redactions end up in the + // detached queue and we show the room status bar to allow retry + if (typeof code !== "undefined") { + // display error message stating you couldn't delete this. + Modal.createTrackedDialog('Unable to find event at that date', '', ErrorDialog, { + title: _t('Error'), + description: _t('Unable to find event at that date. (%(code)s)', { code }), + }); + } + } + }; + + // 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 }); + }; + + // 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 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 => { + const date = new Date(); + // This just goes back 7 days. + // FIXME: Do we want this to go back to the last Sunday? https://upokary.com/how-to-get-last-monday-or-last-friday-or-any-last-day-in-javascript/ + date.setDate(date.getDate() - 7); + this.pickDate(date); + this.closeMenu(); + } + + private onLastMonthClicked = (): void => { + const date = new Date(); + // Month numbers are 0 - 11 and `setMonth` handles the negative rollover + date.setMonth(date.getMonth() - 1, 1); + this.pickDate(date); + this.closeMenu(); + } + + private onTheBeginningClicked = (): void => { + const date = new Date(0); + this.pickDate(date); + this.closeMenu(); + } + + private onJumpToDateSubmit = (): void => { + console.log('onJumpToDateSubmit') + this.pickDate(this.state.dateValue); + this.closeMenu(); + } + + private renderNotificationsMenu(): React.ReactElement { + let contextMenu: JSX.Element; + if (this.state.contextMenuPosition) { + contextMenu = + + + + + + + + {}} + tabIndex={-1} + > +
    + + + { _t("Go") } + + +
    +
    +
    ; + } + + return ( + + +
    + { contextMenu } + + ); + } + render() { // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers - return

    + return


    - + { this.renderNotificationsMenu() }

    ; } diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 376c3166a98..df8761bb78f 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -47,7 +47,7 @@ export default class SearchResultTile extends React.Component { const eventId = mxEv.getId(); const ts1 = mxEv.getTs(); - const ret = []; + const ret = []; const layout = SettingsStore.getValue("layout"); const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 72dd21e9b26..2487b485fd5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2027,6 +2027,13 @@ "Saturday": "Saturday", "Today": "Today", "Yesterday": "Yesterday", + "Unable to find event at that date. (%(code)s)": "Unable to find event at that date. (%(code)s)", + "Last week": "Last week", + "Last month": "Last month", + "The beginning of the room": "The beginning of the room", + "Jump to date": "Jump to date", + "Pick a date to jump to": "Pick a date to jump to", + "Go": "Go", "Downloading": "Downloading", "Decrypting": "Decrypting", "Download": "Download", @@ -2551,7 +2558,6 @@ "Start a conversation with someone using their name, email address or username (like ).": "Start a conversation with someone using their name, email address or username (like ).", "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", - "Go": "Go", "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.", "If you can't see who you're looking for, send them your invite link below.": "If you can't see who you're looking for, send them your invite link below.", "Or send invite link": "Or send invite link", diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index a39bced5d63..3ccf967d1f9 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -248,7 +248,7 @@ export default class HTMLExporter extends Exporter { protected getDateSeparator(event: MatrixEvent) { const ts = event.getTs(); - const dateSeparator =
  • ; + const dateSeparator =
  • ; return renderToStaticMarkup(dateSeparator); } From 9cc435a5369a23af41445bc308852f4a004b80fa Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sat, 11 Dec 2021 02:43:17 -0600 Subject: [PATCH 09/47] RovingTabIndex in ContextMenu --- src/accessibility/RovingTabIndex.tsx | 3 + .../context_menu/ContextMenuButton.tsx | 11 +- .../context_menu/ContextMenuTooltipButton.tsx | 8 +- src/accessibility/context_menu/MenuItem.tsx | 13 +- src/components/structures/ContextMenu.tsx | 145 +++++++++--------- .../views/messages/DateSeparator.tsx | 1 - .../views/messages/MessageActionBar.tsx | 13 +- src/components/views/rooms/RoomList.tsx | 2 - 8 files changed, 96 insertions(+), 100 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 769b0b683f9..b783c4a5bc3 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -194,6 +194,7 @@ export const RovingTabIndexProvider: React.FC = ({ } } + console.log('onKeyDownHandler', context.state.refs.length) let handled = false; let focusRef: RefObject; // Don't interfere with input default keydown behaviour @@ -223,6 +224,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); focusRef = findSiblingElement(context.state.refs, idx + 1); + console.log('up', focusRef, idx + 1) } } break; @@ -234,6 +236,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); focusRef = findSiblingElement(context.state.refs, idx - 1, true); + console.log('down', focusRef, idx - 1) } } break; diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index e211a4c9333..5c957d368d0 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -18,15 +18,16 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import { RovingAccessibleButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; // whether or not the context menu is currently open isExpanded: boolean; } -// Semantic component for representing the AccessibleButton which launches a +// Semantic component for representing the RovingAccessibleButton which +// launches a export const ContextMenuButton: React.FC = ({ label, isExpanded, @@ -36,7 +37,7 @@ export const ContextMenuButton: React.FC = ({ ...props }) => { return ( - = ({ aria-expanded={isExpanded} > { children } - + ); }; diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index 74a45a57eea..999749da018 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -18,9 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; +import { RovingAccessibleTooltipButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { // whether or not the context menu is currently open isExpanded: boolean; } @@ -34,7 +34,7 @@ export const ContextMenuTooltipButton: React.FC = ({ ...props }) => { return ( - = ({ forceHide={isExpanded} > { children } - + ); }; diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 9c0b2482740..19bcbcd6f5a 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -18,10 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; -import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; +import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; tooltip?: string; } @@ -31,15 +30,15 @@ export const MenuItem: React.FC = ({ children, label, tooltip, ...props const ariaLabel = props["aria-label"] || label; if (tooltip) { - return + return { children } - ; + ; } return ( - + { children } - + ); }; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 6c8a56e9428..714dcd26c7e 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -26,6 +26,7 @@ import { Writeable } from "../../@types/common"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; import { getInputableElement } from "./LoggedInView"; +import { RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -88,7 +89,7 @@ export interface IProps extends IPosition { // If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered // within an existing FocusLock e.g inside a modal. - focusLock?: boolean; + // focusLock?: boolean; // Function to be called on menu close onFinished(); @@ -237,53 +238,53 @@ export default class ContextMenu extends React.PureComponent { ev.stopPropagation(); }; - private onKeyDown = (ev: React.KeyboardEvent) => { - // don't let keyboard handling escape the context menu - ev.stopPropagation(); - - if (!this.props.managed) { - if (ev.key === Key.ESCAPE) { - this.props.onFinished(); - ev.preventDefault(); - } - return; - } - - // only handle escape when in an input field - if (ev.key !== Key.ESCAPE && getInputableElement(ev.target as HTMLElement)) return; - - let handled = true; - - switch (ev.key) { - // XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils - // to inherit proper handling of unmount edge cases - case Key.TAB: - case Key.ESCAPE: - case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a - case Key.ARROW_RIGHT: - this.props.onFinished(); - break; - case Key.ARROW_UP: - this.onMoveFocus(ev.target as Element, true); - break; - case Key.ARROW_DOWN: - this.onMoveFocus(ev.target as Element, false); - break; - case Key.HOME: - this.onMoveFocusHomeEnd(this.state.contextMenuElem, true); - break; - case Key.END: - this.onMoveFocusHomeEnd(this.state.contextMenuElem, false); - break; - default: - handled = false; - } - - if (handled) { - // consume all other keys in context menu - ev.preventDefault(); - } - }; + // private onKeyDown = (ev: React.KeyboardEvent) => { + // // don't let keyboard handling escape the context menu + // ev.stopPropagation(); + + // if (!this.props.managed) { + // if (ev.key === Key.ESCAPE) { + // this.props.onFinished(); + // ev.preventDefault(); + // } + // return; + // } + + // // only handle escape when in an input field + // if (ev.key !== Key.ESCAPE && getInputableElement(ev.target as HTMLElement)) return; + + // let handled = true; + + // switch (ev.key) { + // // XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils + // // to inherit proper handling of unmount edge cases + // case Key.TAB: + // case Key.ESCAPE: + // case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a + // case Key.ARROW_RIGHT: + // this.props.onFinished(); + // break; + // case Key.ARROW_UP: + // this.onMoveFocus(ev.target as Element, true); + // break; + // case Key.ARROW_DOWN: + // this.onMoveFocus(ev.target as Element, false); + // break; + // case Key.HOME: + // this.onMoveFocusHomeEnd(this.state.contextMenuElem, true); + // break; + // case Key.END: + // this.onMoveFocusHomeEnd(this.state.contextMenuElem, false); + // break; + // default: + // handled = false; + // } + + // if (handled) { + // // consume all other keys in context menu + // ev.preventDefault(); + // } + // }; protected renderMenu(hasBackground = this.props.hasBackground) { const position: Partial> = {}; @@ -401,30 +402,34 @@ export default class ContextMenu extends React.PureComponent { { props.children } ; - if (props.focusLock) { - body = - { body } - ; - } + // if (props.focusLock) { + // body = + // { body } + // ; + // } return ( -
    - { background } -
    - { body } -
    -
    + + { ({ onKeyDownHandler }) => ( +
    + { background } +
    + { body } +
    +
    + ) } +
    ); } diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index e9438da5e1e..81cb1348995 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -288,7 +288,6 @@ export default class DateSeparator extends React.Component { className="mx_DateSeparator_jumpToDateMenuOption" label={_t("Jump to date")} onClick={() => {}} - tabIndex={-1} >
    = ({ getRelationsForEvent, }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); - const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { - onFocus(); onFocusChange(menuDisplayed); - }, [onFocus, onFocusChange, menuDisplayed]); + }, [onFocusChange, menuDisplayed]); let contextMenu: ReactElement | null; if (menuDisplayed) { @@ -94,9 +92,6 @@ const OptionsButton: React.FC = ({ title={_t("Options")} onClick={openMenu} isExpanded={menuDisplayed} - inputRef={ref} - onFocus={onFocus} - tabIndex={isActive ? 0 : -1} /> { contextMenu } @@ -111,7 +106,6 @@ interface IReactButtonProps { const ReactButton: React.FC = ({ mxEvent, reactions, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); - const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { onFocusChange(menuDisplayed); }, [onFocusChange, menuDisplayed]); @@ -130,9 +124,6 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC title={_t("React")} onClick={openMenu} isExpanded={menuDisplayed} - inputRef={ref} - onFocus={onFocus} - tabIndex={isActive ? 0 : -1} /> { contextMenu } diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 8e29977786a..9122795931f 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -167,7 +167,6 @@ const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonPro return <> { return <> Date: Sat, 11 Dec 2021 05:04:30 -0600 Subject: [PATCH 10/47] Refactor JumpToDatePicker form out to functional component so we can use useRovingTabIndex --- .../views/messages/DateSeparator.tsx | 120 +--------------- .../views/messages/JumpToDatePicker.tsx | 133 ++++++++++++++++++ 2 files changed, 139 insertions(+), 114 deletions(-) create mode 100644 src/components/views/messages/JumpToDatePicker.tsx diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 81cb1348995..f6550372964 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -25,10 +25,8 @@ import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; import dis from '../../../dispatcher/dispatcher'; import { Action } from '../../../dispatcher/actions'; -import Field from "../elements/Field"; import Modal from '../../../Modal'; import ErrorDialog from '../dialogs/ErrorDialog'; -import AccessibleButton from "../elements/AccessibleButton"; import { contextMenuBelow } from '../rooms/RoomTile'; import { ContextMenuTooltipButton } from "../../structures/ContextMenu"; import IconizedContextMenu, { @@ -36,38 +34,8 @@ import IconizedContextMenu, { IconizedContextMenuOptionList, IconizedContextMenuRadio, } from "../context_menus/IconizedContextMenu"; +import JumpToDatePicker from './JumpToDatePicker'; -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 [ @@ -88,11 +56,6 @@ 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 } @@ -100,10 +63,7 @@ interface IState { export default class DateSeparator extends React.Component { constructor(props, context) { super(props, context); - this.state = { - dateValue: this.getDefaultDateValue(), - navigateOnDatePickerSelection: true - }; + this.state = {}; } private onContextMenuOpenClick = (e: React.MouseEvent): void => { @@ -145,15 +105,6 @@ export default class DateSeparator extends React.Component { } } - private getDefaultDateValue(): string { - const date = new Date(this.props.ts); - const year = date.getFullYear(); - const month = `${date.getMonth() + 1}`.padStart(2, "0") - const day = `${date.getDate()}`.padStart(2, "0") - - return `${year}-${month}-${day}` - } - private pickDate = async (inputTimestamp): Promise => { console.log('pickDate', inputTimestamp) @@ -190,46 +141,6 @@ export default class DateSeparator extends React.Component { } }; - // 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 }); - }; - - // 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 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 => { const date = new Date(); @@ -254,9 +165,9 @@ export default class DateSeparator extends React.Component { this.closeMenu(); } - private onJumpToDateSubmit = (): void => { - console.log('onJumpToDateSubmit') - this.pickDate(this.state.dateValue); + private onDatePicked = (dateString): void => { + console.log('onDatePicked', dateString) + this.pickDate(dateString); this.closeMenu(); } @@ -289,26 +200,7 @@ export default class DateSeparator extends React.Component { label={_t("Jump to date")} onClick={() => {}} > - - - - { _t("Go") } - - + ; diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx new file mode 100644 index 00000000000..7edd4d9f4db --- /dev/null +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -0,0 +1,133 @@ + +import React, { useState } from 'react'; +import { _t } from '../../../languageHandler'; + +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; + + +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={() => {}} />; + } +} + + +interface IProps { + ts: number; + onDatePicked?: (dateString: string) => void; +} + +const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { + const date = new Date(ts); + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, "0") + const day = `${date.getDate()}`.padStart(2, "0") + const dateDefaultValue = `${year}-${month}-${day}`; + + const [dateValue, setDateValue] = useState(dateDefaultValue); + // 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. + const [navigateOnDatePickerSelection, setNavigateOnDatePickerSelection] = useState(true); + + //const [onFocus, isActive, ref] = useRovingTabIndex(); + + // Since we're using CustomInput with native JavaScript behavior, this + // tracks the date value changes as they come in. + const onDateValueInput = (e: React.ChangeEvent): void => { + console.log('onDateValueInput') + setDateValue(e.target.value); + }; + + // 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. + const onDateValueChange = (e: React.ChangeEvent): void => { + console.log('onDateValueChange') + setDateValue(e.target.value); + + // Don't auto navigate if they were manually typing out a date + if(navigateOnDatePickerSelection) { + onDatePicked(dateValue); + } + }; + + const 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") { + onDatePicked(dateValue); + return; + } + + // When we see someone manually typing out a date, disable the auto + // submit on change. + setNavigateOnDatePickerSelection(false); + }; + + const onJumpToDateSubmit = (): void => { + console.log('onJumpToDateSubmit') + onDatePicked(dateValue); + } + + return ( +
    + + + { _t("Go") } + + + ) +}; + +export default JumpToDatePicker; From 9ad88686e5949e6d0e5bbd9fb11303424edc1cda Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sat, 11 Dec 2021 06:16:30 -0600 Subject: [PATCH 11/47] DatePicker only keyboard navigable when item focused --- .../context_menu/ContextMenuButton.tsx | 10 +- .../context_menu/ContextMenuTooltipButton.tsx | 8 +- .../views/messages/DateSeparator.tsx | 6 +- .../views/messages/JumpToDatePicker.tsx | 188 ++++++++++-------- .../views/spaces/SpaceTreeLevel.tsx | 2 +- 5 files changed, 114 insertions(+), 100 deletions(-) diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 5c957d368d0..7e1092361eb 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -18,15 +18,15 @@ limitations under the License. import React from "react"; -import { RovingAccessibleButton } from "../RovingTabIndex"; +import AccessibleButton from "../../components/views/elements/AccessibleButton"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; // whether or not the context menu is currently open isExpanded: boolean; } -// Semantic component for representing the RovingAccessibleButton which +// Semantic component for representing the AccessibleButton which // launches a export const ContextMenuButton: React.FC = ({ label, @@ -37,7 +37,7 @@ export const ContextMenuButton: React.FC = ({ ...props }) => { return ( - = ({ aria-expanded={isExpanded} > { children } - + ); }; diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index 999749da018..74a45a57eea 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -18,9 +18,9 @@ limitations under the License. import React from "react"; -import { RovingAccessibleTooltipButton } from "../RovingTabIndex"; +import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { // whether or not the context menu is currently open isExpanded: boolean; } @@ -34,7 +34,7 @@ export const ContextMenuTooltipButton: React.FC = ({ ...props }) => { return ( - = ({ forceHide={isExpanded} > { children } - + ); }; diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index f6550372964..94c7956f2c7 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -195,13 +195,13 @@ export default class DateSeparator extends React.Component { - {}} - > + > */} - + {/* */} ; } diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 7edd4d9f4db..cbefc765f3b 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; @@ -27,15 +27,21 @@ interface CustomInputProps { * 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; + private readonly registerCallbacks = (input: HTMLInputElement | null) => { + if (input) { + input.onchange = this.props.onChange ? this.props.onChange : null; + input.oninput = this.props.onInput ? this.props.onInput : null; } }; public render() { - return {}} onInput={() => {}} />; + return {}} + onInput={() => {}} + />; } } @@ -46,88 +52,96 @@ interface IProps { } const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { - const date = new Date(ts); - const year = date.getFullYear(); - const month = `${date.getMonth() + 1}`.padStart(2, "0") - const day = `${date.getDate()}`.padStart(2, "0") - const dateDefaultValue = `${year}-${month}-${day}`; - - const [dateValue, setDateValue] = useState(dateDefaultValue); - // 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. - const [navigateOnDatePickerSelection, setNavigateOnDatePickerSelection] = useState(true); - - //const [onFocus, isActive, ref] = useRovingTabIndex(); - - // Since we're using CustomInput with native JavaScript behavior, this - // tracks the date value changes as they come in. - const onDateValueInput = (e: React.ChangeEvent): void => { - console.log('onDateValueInput') - setDateValue(e.target.value); - }; - - // 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. - const onDateValueChange = (e: React.ChangeEvent): void => { - console.log('onDateValueChange') - setDateValue(e.target.value); - - // Don't auto navigate if they were manually typing out a date - if(navigateOnDatePickerSelection) { + const date = new Date(ts); + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, "0") + const day = `${date.getDate()}`.padStart(2, "0") + const dateDefaultValue = `${year}-${month}-${day}`; + + const [dateValue, setDateValue] = useState(dateDefaultValue); + // 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. + const [navigateOnDatePickerSelection, setNavigateOnDatePickerSelection] = useState(true); + + const ref = useRef(null); + const [onFocus, isActive] = useRovingTabIndex(ref); + + // Since we're using CustomInput with native JavaScript behavior, this + // tracks the date value changes as they come in. + const onDateValueInput = (e: React.ChangeEvent): void => { + console.log('onDateValueInput') + setDateValue(e.target.value); + }; + + // 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. + const onDateValueChange = (e: React.ChangeEvent): void => { + console.log('onDateValueChange') + setDateValue(e.target.value); + + // Don't auto navigate if they were manually typing out a date + if(navigateOnDatePickerSelection) { onDatePicked(dateValue); - } - }; - - const 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") { - onDatePicked(dateValue); - return; - } - - // When we see someone manually typing out a date, disable the auto - // submit on change. - setNavigateOnDatePickerSelection(false); - }; - - const onJumpToDateSubmit = (): void => { - console.log('onJumpToDateSubmit') - onDatePicked(dateValue); - } - - return ( -
    - - - { _t("Go") } - - - ) + } + }; + + const 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") { + onDatePicked(dateValue); + return; + } + + // When we see someone manually typing out a date, disable the auto + // submit on change. + setNavigateOnDatePickerSelection(false); + }; + + const onJumpToDateSubmit = (): void => { + console.log('onJumpToDateSubmit') + onDatePicked(dateValue); + } + + return ( +
    + + + { _t("Go") } + + + ) }; export default JumpToDatePicker; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index e8d090644ac..6d98b277190 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -23,7 +23,6 @@ import React, { } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; -import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/spaces/SpaceStore"; @@ -41,6 +40,7 @@ import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManag import { NotificationState } from "../../../stores/notifications/NotificationState"; import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; interface IButtonProps extends Omit, "title" | "onClick"> { From 2985888bf60df1be1a5dee70d92795c10dbf8f02 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sat, 11 Dec 2021 07:07:09 -0600 Subject: [PATCH 12/47] Fix date picker form focus 1. Only can tab to the datepicker input and "Go" when the "Jump to date" option is selected 1. Tabbing off the end of the ContextMenu will close it and return your focus back to where it is on the page. Note: nothing special coded for this, just prevented from tabbing off the end of the ContextMenu which was the end of the page. 1. Can navigate up and down the ContextMenu with Arrow keys as expected --- res/css/_components.scss | 1 + res/css/views/messages/_DateSeparator.scss | 19 ---------- res/css/views/messages/_JumpToDatePicker.scss | 18 ++++++++++ src/components/structures/ContextMenu.tsx | 8 ++++- .../views/messages/DateSeparator.tsx | 10 ++---- .../views/messages/JumpToDatePicker.tsx | 36 ++++++++++++++++--- 6 files changed, 61 insertions(+), 31 deletions(-) create mode 100644 res/css/views/messages/_JumpToDatePicker.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 01f8bbece33..88edfba59cb 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -180,6 +180,7 @@ @import "./views/messages/_CallEvent.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; +@import "./views/messages/_JumpToDatePicker.scss"; @import "./views/messages/_EventTileBubble.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; diff --git a/res/css/views/messages/_DateSeparator.scss b/res/css/views/messages/_DateSeparator.scss index bd9b77227db..93a61e2de0a 100644 --- a/res/css/views/messages/_DateSeparator.scss +++ b/res/css/views/messages/_DateSeparator.scss @@ -48,22 +48,3 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); background-color: $tertiary-content; } - -.mx_DateSeparator_jumpToDateMenuOption > .mx_IconizedContextMenu_label { - flex: initial; - width: auto; -} - -.mx_DateSeparator_datePickerForm { - display: flex; -} - -.mx_DateSeparator_datePicker { - flex: initial; - margin: 0; - margin-left: 8px; -} - -.mx_DateSeparator_datePickerSubmitButton { - margin-left: 8px; -} diff --git a/res/css/views/messages/_JumpToDatePicker.scss b/res/css/views/messages/_JumpToDatePicker.scss new file mode 100644 index 00000000000..7d3293ae516 --- /dev/null +++ b/res/css/views/messages/_JumpToDatePicker.scss @@ -0,0 +1,18 @@ + +.mx_JumpToDatePicker_form { + display: flex; + align-items: center; +} + +.mx_JumpToDatePicker_label { + font-size: $font-15px; +} + +.mx_JumpToDatePicker_datePicker { + margin: 0; + margin-left: 8px; +} + +.mx_JumpToDatePicker_submitButton { + margin-left: 8px; +} diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 714dcd26c7e..4b480e4dbdd 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -238,6 +238,12 @@ export default class ContextMenu extends React.PureComponent { ev.stopPropagation(); }; + private onKeyDown = (ev: React.KeyboardEvent) => { + if (ev.key === Key.TAB || ev.key === Key.ESCAPE) { + this.props.onFinished(); + } + } + // private onKeyDown = (ev: React.KeyboardEvent) => { // // don't let keyboard handling escape the context menu // ev.stopPropagation(); @@ -409,7 +415,7 @@ export default class ContextMenu extends React.PureComponent { // } return ( - + { ({ onKeyDownHandler }) => (
    { constructor(props, context) { super(props, context); this.state = {}; + + this.myRef = React.createRef(); } private onContextMenuOpenClick = (e: React.MouseEvent): void => { @@ -195,13 +197,7 @@ export default class DateSeparator extends React.Component { - {/* {}} - > */} - - {/* */} + ; } diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index cbefc765f3b..ed412ebb467 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -2,6 +2,7 @@ import React, { useRef, useState } from 'react'; import { _t } from '../../../languageHandler'; +import { Key } from "../../../Keyboard"; import Field from "../elements/Field"; import AccessibleButton from "../elements/AccessibleButton"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; @@ -89,6 +90,11 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { }; const onDateInputKeyDown = (e: React.KeyboardEvent): void => { + // Don't interfere with input default keydown behaviour. For example, + // without this, when pressing "Space" while in the input, it would + // scroll down the timeline. + e.stopPropagation(); + // Ignore the tab key which is probably just navigating focus around // with the keyboard if(e.key === "Tab") { @@ -106,19 +112,40 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { setNavigateOnDatePickerSelection(false); }; + const onFormKeyDown = (e: React.KeyboardEvent) => { + // Stop the ContextMenu from closing when we first tab from the form to + // the datePicker. + if(e.target === e.currentTarget && e.key === Key.TAB) { + e.stopPropagation(); + } + } + + const onGoKeyDown = (e: React.KeyboardEvent) => { + // Stop the ContextMenu from closing when we tab backwards to the + // datePicker from the "Go" button + // + // If they tab forwards off the end of the ContextMenu, we want to close + // the ContextMenu which will put the focus back where we were before + // opening the ContextMenu. + if (e.key === Key.TAB && e.shiftKey) { + e.stopPropagation(); + } + } + const onJumpToDateSubmit = (): void => { - console.log('onJumpToDateSubmit') onDatePicked(dateValue); } return (
    + Jump to date = ({ ts, onDatePicked }: IProps) => { onInput={onDateValueInput} onKeyDown={onDateInputKeyDown} value={dateValue} - className="mx_DateSeparator_datePicker" + className="mx_JumpToDatePicker_datePicker" label={_t("Pick a date to jump to")} // onFocus={onFocus} // inputRef={ref} @@ -134,8 +161,9 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { /> { _t("Go") } From 423fb930d4c00a522e24075363190534139f3efb Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sat, 11 Dec 2021 07:20:22 -0600 Subject: [PATCH 13/47] Clean up PR --- res/css/views/messages/_JumpToDatePicker.scss | 1 - src/accessibility/RovingTabIndex.tsx | 3 --- src/accessibility/context_menu/ContextMenuButton.tsx | 3 +-- src/components/structures/ContextMenu.tsx | 11 ----------- src/components/views/messages/DateSeparator.tsx | 2 -- src/components/views/messages/JumpToDatePicker.tsx | 2 +- src/components/views/spaces/QuickSettingsButton.tsx | 1 - src/components/views/spaces/SpaceCreateMenu.tsx | 1 - 8 files changed, 2 insertions(+), 22 deletions(-) diff --git a/res/css/views/messages/_JumpToDatePicker.scss b/res/css/views/messages/_JumpToDatePicker.scss index 7d3293ae516..ddde08a59dc 100644 --- a/res/css/views/messages/_JumpToDatePicker.scss +++ b/res/css/views/messages/_JumpToDatePicker.scss @@ -1,4 +1,3 @@ - .mx_JumpToDatePicker_form { display: flex; align-items: center; diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index b783c4a5bc3..769b0b683f9 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -194,7 +194,6 @@ export const RovingTabIndexProvider: React.FC = ({ } } - console.log('onKeyDownHandler', context.state.refs.length) let handled = false; let focusRef: RefObject; // Don't interfere with input default keydown behaviour @@ -224,7 +223,6 @@ export const RovingTabIndexProvider: React.FC = ({ if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); focusRef = findSiblingElement(context.state.refs, idx + 1); - console.log('up', focusRef, idx + 1) } } break; @@ -236,7 +234,6 @@ export const RovingTabIndexProvider: React.FC = ({ if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); focusRef = findSiblingElement(context.state.refs, idx - 1, true); - console.log('down', focusRef, idx - 1) } } break; diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 7e1092361eb..e211a4c9333 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -26,8 +26,7 @@ interface IProps extends React.ComponentProps { isExpanded: boolean; } -// Semantic component for representing the AccessibleButton which -// launches a +// Semantic component for representing the AccessibleButton which launches a export const ContextMenuButton: React.FC = ({ label, isExpanded, diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 4b480e4dbdd..f838ca518a4 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -19,7 +19,6 @@ limitations under the License. import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; -import FocusLock from "react-focus-lock"; import { Key } from "../../Keyboard"; import { Writeable } from "../../@types/common"; @@ -87,10 +86,6 @@ export interface IProps extends IPosition { // it will be mounted to a container at the root of the DOM. mountAsChild?: boolean; - // If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered - // within an existing FocusLock e.g inside a modal. - // focusLock?: boolean; - // Function to be called on menu close onFinished(); // on resize callback @@ -408,12 +403,6 @@ export default class ContextMenu extends React.PureComponent { { props.children } ; - // if (props.focusLock) { - // body = - // { body } - // ; - // } - return ( { ({ onKeyDownHandler }) => ( diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index be518616148..caeb86cce26 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -64,8 +64,6 @@ export default class DateSeparator extends React.Component { constructor(props, context) { super(props, context); this.state = {}; - - this.myRef = React.createRef(); } private onContextMenuOpenClick = (e: React.MouseEvent): void => { diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index ed412ebb467..ead7a84e5b8 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -39,7 +39,7 @@ class CustomInput extends React.Component {}} onInput={() => {}} />; diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 4e99c1ab090..79f44e29952 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -56,7 +56,6 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => { wrapperClassName="mx_QuickSettingsButton_ContextMenuWrapper" onFinished={closeMenu} managed={false} - focusLock={true} >

    { _t("Quick settings") }

    diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 5203bd2c90d..59c3a2e237a 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -356,7 +356,6 @@ const SpaceCreateMenu = ({ onFinished }) => { onFinished={onFinished} wrapperClassName="mx_SpaceCreateMenu_wrapper" managed={false} - focusLock={true} > { body }
    ; From 312af82cd8e4da2c85d4b31d25c8b41c66179ad8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sat, 11 Dec 2021 07:25:59 -0600 Subject: [PATCH 14/47] More cleanup --- src/components/structures/ContextMenu.tsx | 48 ------------------- .../views/messages/DateSeparator.tsx | 3 +- 2 files changed, 1 insertion(+), 50 deletions(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index f838ca518a4..ca8f407b605 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -239,54 +239,6 @@ export default class ContextMenu extends React.PureComponent { } } - // private onKeyDown = (ev: React.KeyboardEvent) => { - // // don't let keyboard handling escape the context menu - // ev.stopPropagation(); - - // if (!this.props.managed) { - // if (ev.key === Key.ESCAPE) { - // this.props.onFinished(); - // ev.preventDefault(); - // } - // return; - // } - - // // only handle escape when in an input field - // if (ev.key !== Key.ESCAPE && getInputableElement(ev.target as HTMLElement)) return; - - // let handled = true; - - // switch (ev.key) { - // // XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils - // // to inherit proper handling of unmount edge cases - // case Key.TAB: - // case Key.ESCAPE: - // case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a - // case Key.ARROW_RIGHT: - // this.props.onFinished(); - // break; - // case Key.ARROW_UP: - // this.onMoveFocus(ev.target as Element, true); - // break; - // case Key.ARROW_DOWN: - // this.onMoveFocus(ev.target as Element, false); - // break; - // case Key.HOME: - // this.onMoveFocusHomeEnd(this.state.contextMenuElem, true); - // break; - // case Key.END: - // this.onMoveFocusHomeEnd(this.state.contextMenuElem, false); - // break; - // default: - // handled = false; - // } - - // if (handled) { - // // consume all other keys in context menu - // ev.preventDefault(); - // } - // }; - protected renderMenu(hasBackground = this.props.hasBackground) { const position: Partial> = {}; const props = this.props; diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index caeb86cce26..277abc9d4a5 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -35,6 +35,7 @@ import IconizedContextMenu, { IconizedContextMenuRadio, } from "../context_menus/IconizedContextMenu"; import JumpToDatePicker from './JumpToDatePicker'; +import { Alignment } from '../elements/Tooltip'; function getDaysArray(): string[] { @@ -107,7 +108,6 @@ export default class DateSeparator extends React.Component { private pickDate = async (inputTimestamp): Promise => { console.log('pickDate', inputTimestamp) - const unixTimestamp = new Date(inputTimestamp).getTime(); const cli = MatrixClientPeg.get(); @@ -166,7 +166,6 @@ export default class DateSeparator extends React.Component { } private onDatePicked = (dateString): void => { - console.log('onDatePicked', dateString) this.pickDate(dateString); this.closeMenu(); } From 64eef3a4ce36516daacdb260de7b09a98c7677f0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Dec 2021 09:38:54 +0000 Subject: [PATCH 15/47] Fix handling vertical arrows in the wrong direction --- src/accessibility/RovingTabIndex.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 769b0b683f9..bb4e66f467c 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -216,9 +216,11 @@ export const RovingTabIndexProvider: React.FC = ({ } break; - case Key.ARROW_UP: + case Key.ARROW_DOWN: case Key.ARROW_RIGHT: - if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_RIGHT && handleLeftRight)) { + if ((ev.key === Key.ARROW_DOWN && handleUpDown) || + (ev.key === Key.ARROW_RIGHT && handleLeftRight) + ) { handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); @@ -227,9 +229,9 @@ export const RovingTabIndexProvider: React.FC = ({ } break; - case Key.ARROW_DOWN: + case Key.ARROW_UP: case Key.ARROW_LEFT: - if ((ev.key === Key.ARROW_DOWN && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) { + if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) { handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); From 6ce07989df4034884ab16796cce29118fbf982c6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Dec 2021 23:43:45 +0000 Subject: [PATCH 16/47] iterate PR --- src/accessibility/RovingTabIndex.tsx | 1 + src/components/views/messages/MessageActionBar.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index bb4e66f467c..65494a210d8 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -131,6 +131,7 @@ export const reducer = (state: IState, action: IAction) => { } case Type.SetFocus: { + // if the ref doesn't change just return the same object reference to skip a re-render if (state.activeRef === action.payload.ref) return state; // update active ref state.activeRef = action.payload.ref; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 74217b131a0..41adcdbaddf 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -67,6 +67,7 @@ const OptionsButton: React.FC = ({ const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually onFocus(); onFocusChange(menuDisplayed); }, [onFocus, onFocusChange, menuDisplayed]); @@ -113,8 +114,10 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually + onFocus(); onFocusChange(menuDisplayed); - }, [onFocusChange, menuDisplayed]); + }, [onFocus, onFocusChange, menuDisplayed]); let contextMenu; if (menuDisplayed) { From 2d0475d4d5fdec89253da3b5a989724c2db1560a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Dec 2021 00:58:05 +0000 Subject: [PATCH 17/47] delint --- src/components/views/rooms/EventTile.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 83c35e9bfae..80d5e3bbbae 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -67,12 +67,11 @@ import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import Toolbar from '../../../accessibility/Toolbar'; import { POLL_START_EVENT_TYPE } from '../../../polls/consts'; import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton'; -import ThreadListContextMenu from '../context_menus/ThreadListContextMenu'; +import { RovingThreadListContextMenu } from '../context_menus/ThreadListContextMenu'; import { ThreadNotificationState } from '../../../stores/notifications/ThreadNotificationState'; import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore'; import { NotificationStateEvents } from '../../../stores/notifications/NotificationState'; import { NotificationColor } from '../../../stores/notifications/NotificationColor'; -import { RovingThreadListContextMenu } from '../context_menus/ThreadListContextMenu'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', From 3f1271b023b42d211fbf3b3e6f09b7bdc52110c5 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 20:14:10 -0600 Subject: [PATCH 18/47] Fix conflit --- src/components/views/messages/MessageActionBar.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 6bc3f998ffc..dbe208dc17e 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -107,10 +107,8 @@ interface IReactButtonProps { const ReactButton: React.FC = ({ mxEvent, reactions, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); useEffect(() => { - // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually - onFocus(); onFocusChange(menuDisplayed); - }, [onFocus, onFocusChange, menuDisplayed]); + }, [onFocusChange, menuDisplayed]); let contextMenu; if (menuDisplayed) { From 4d5f9b365a06c10f9098d713bcaeab6092132e68 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 20:41:12 -0600 Subject: [PATCH 19/47] Fix missing ref when opening emoji reaction menu --- src/components/views/messages/MessageActionBar.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index dbe208dc17e..c2ac314bc2a 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -64,7 +64,7 @@ const OptionsButton: React.FC = ({ onFocusChange, getRelationsForEvent, }) => { - const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); useEffect(() => { onFocusChange(menuDisplayed); }, [onFocusChange, menuDisplayed]); @@ -74,7 +74,7 @@ const OptionsButton: React.FC = ({ const tile = getTile && getTile(); const replyChain = getReplyChain && getReplyChain(); - const buttonRect = button.current.getBoundingClientRect(); + const buttonRect = buttonRef.current.getBoundingClientRect(); contextMenu = = ({ title={_t("Options")} onClick={openMenu} isExpanded={menuDisplayed} + inputRef={buttonRef} /> { contextMenu } @@ -105,14 +106,14 @@ interface IReactButtonProps { } const ReactButton: React.FC = ({ mxEvent, reactions, onFocusChange }) => { - const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); useEffect(() => { onFocusChange(menuDisplayed); }, [onFocusChange, menuDisplayed]); let contextMenu; if (menuDisplayed) { - const buttonRect = button.current.getBoundingClientRect(); + const buttonRect = buttonRef.current.getBoundingClientRect(); contextMenu = ; @@ -124,6 +125,7 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC title={_t("React")} onClick={openMenu} isExpanded={menuDisplayed} + inputRef={buttonRef} /> { contextMenu } From 3352183de693ce5dc1bdf9d51c44aef5858b075c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 21:20:14 -0600 Subject: [PATCH 20/47] Restore focuslock for now See https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r767154279 --- src/components/structures/ContextMenu.tsx | 13 ++++++++++++- src/components/views/spaces/QuickSettingsButton.tsx | 1 + src/components/views/spaces/SpaceCreateMenu.tsx | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index f08c40b99f5..ceeb0909507 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -19,6 +19,7 @@ limitations under the License. import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; +import FocusLock from "react-focus-lock"; import { Key } from "../../Keyboard"; import { Writeable } from "../../@types/common"; @@ -86,6 +87,10 @@ export interface IProps extends IPosition { // it will be mounted to a container at the root of the DOM. mountAsChild?: boolean; + // If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered + // within an existing FocusLock e.g inside a modal. + focusLock?: boolean; + // Function to be called on menu close onFinished(); // on resize callback @@ -234,7 +239,7 @@ export default class ContextMenu extends React.PureComponent { }; private onKeyDown = (ev: React.KeyboardEvent) => { - if (ev.key === Key.TAB || ev.key === Key.ESCAPE) { + if ((ev.key === Key.TAB && !this.props.focusLock) || ev.key === Key.ESCAPE) { this.props.onFinished(); } } @@ -355,6 +360,12 @@ export default class ContextMenu extends React.PureComponent { { props.children } ; + if (props.focusLock) { + body = + { body } + ; + } + return ( { ({ onKeyDownHandler }) => ( diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 79f44e29952..4e99c1ab090 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -56,6 +56,7 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => { wrapperClassName="mx_QuickSettingsButton_ContextMenuWrapper" onFinished={closeMenu} managed={false} + focusLock={true} >

    { _t("Quick settings") }

    diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 59c3a2e237a..5203bd2c90d 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -356,6 +356,7 @@ const SpaceCreateMenu = ({ onFinished }) => { onFinished={onFinished} wrapperClassName="mx_SpaceCreateMenu_wrapper" managed={false} + focusLock={true} > { body }
    ; From 1ce394ff906f7e4203efd0debe80c784b8f75b04 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 21:41:29 -0600 Subject: [PATCH 21/47] Restore changes back to MessageActionBar --- .../views/messages/MessageActionBar.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index c2ac314bc2a..41adcdbaddf 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -29,7 +29,7 @@ import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } fr import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import Toolbar from "../../../accessibility/Toolbar"; -import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; +import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import MessageContextMenu, { canCancel } from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; @@ -64,17 +64,20 @@ const OptionsButton: React.FC = ({ onFocusChange, getRelationsForEvent, }) => { - const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually + onFocus(); onFocusChange(menuDisplayed); - }, [onFocusChange, menuDisplayed]); + }, [onFocus, onFocusChange, menuDisplayed]); let contextMenu: ReactElement | null; if (menuDisplayed) { const tile = getTile && getTile(); const replyChain = getReplyChain && getReplyChain(); - const buttonRect = buttonRef.current.getBoundingClientRect(); + const buttonRect = button.current.getBoundingClientRect(); contextMenu = = ({ title={_t("Options")} onClick={openMenu} isExpanded={menuDisplayed} - inputRef={buttonRef} + inputRef={ref} + onFocus={onFocus} + tabIndex={isActive ? 0 : -1} /> { contextMenu } @@ -106,14 +111,17 @@ interface IReactButtonProps { } const ReactButton: React.FC = ({ mxEvent, reactions, onFocusChange }) => { - const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually + onFocus(); onFocusChange(menuDisplayed); - }, [onFocusChange, menuDisplayed]); + }, [onFocus, onFocusChange, menuDisplayed]); let contextMenu; if (menuDisplayed) { - const buttonRect = buttonRef.current.getBoundingClientRect(); + const buttonRect = button.current.getBoundingClientRect(); contextMenu = ; @@ -125,7 +133,9 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC title={_t("React")} onClick={openMenu} isExpanded={menuDisplayed} - inputRef={buttonRef} + inputRef={ref} + onFocus={onFocus} + tabIndex={isActive ? 0 : -1} /> { contextMenu } From 5ec17b0d661361f73dd96bfafd1d8e5d2574b6bc Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 17 Dec 2021 18:12:05 -0600 Subject: [PATCH 22/47] Remove from diff --- src/components/views/spaces/SpaceTreeLevel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 6d98b277190..e8d090644ac 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -23,6 +23,7 @@ import React, { } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; +import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/spaces/SpaceStore"; @@ -40,7 +41,6 @@ import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManag import { NotificationState } from "../../../stores/notifications/NotificationState"; import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; interface IButtonProps extends Omit, "title" | "onClick"> { From 61f64360381a2712b713d8d017431ef51bd89f1e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 15:59:50 -0600 Subject: [PATCH 23/47] WIP: RovingTabIndex shared ref on CustomInput --- .../views/messages/JumpToDatePicker.tsx | 146 ++++++++++-------- 1 file changed, 85 insertions(+), 61 deletions(-) diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index ead7a84e5b8..b523c4d3ceb 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -5,10 +5,28 @@ import { _t } from '../../../languageHandler'; import { Key } from "../../../Keyboard"; import Field from "../elements/Field"; import AccessibleButton from "../elements/AccessibleButton"; -import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; - +import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; + +// via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd +function useCombinedRefs(...refs) { + const targetRef = React.useRef() + + React.useEffect(() => { + refs.forEach(ref => { + if (!ref) return + + if (typeof ref === 'function') { + ref(targetRef.current) + } else { + ref.current = targetRef.current + } + }) + }, [refs]) + + return targetRef + } -interface CustomInputProps { +interface CustomInputProps extends Omit, 'onChange' | 'onInput'> { onChange?: (event: Event) => void; onInput?: (event: Event) => void; } @@ -27,24 +45,22 @@ interface CustomInputProps { * 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 = (input: HTMLInputElement | null) => { - if (input) { - input.onchange = this.props.onChange ? this.props.onChange : null; - input.oninput = this.props.onInput ? this.props.onInput : null; - } - }; - - public render() { - return { + const registerCallbacks = (input: HTMLInputElement | null) => { + if (input) { + input.onchange = props.onChange; + input.oninput = props.onInput; + } + }; + + return {}} onInput={() => {}} - />; - } -} + />; +}); interface IProps { @@ -65,41 +81,50 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { // starts manually typing in the input instead of picking. const [navigateOnDatePickerSelection, setNavigateOnDatePickerSelection] = useState(true); - const ref = useRef(null); - const [onFocus, isActive] = useRovingTabIndex(ref); - // Since we're using CustomInput with native JavaScript behavior, this // tracks the date value changes as they come in. - const onDateValueInput = (e: React.ChangeEvent): void => { + const onDateValueInput = (e: Event): void => { console.log('onDateValueInput') - setDateValue(e.target.value); + setDateValue((e.target as HTMLInputElement).value); }; // 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. - const onDateValueChange = (e: React.ChangeEvent): void => { + const onDateValueChange = (e: Event): void => { console.log('onDateValueChange') - setDateValue(e.target.value); + setDateValue((e.target as HTMLInputElement).value); // Don't auto navigate if they were manually typing out a date if(navigateOnDatePickerSelection) { - onDatePicked(dateValue); + onDatePicked(dateValue); } }; + // const inputRef = React.useRef(null); + // const registerInputCallbacks = (input: HTMLInputElement | null) => { + // inputRef.current = input; + + // if (input) { + // input.onchange = onDateValueChange; + // input.oninput = onDateValueInput; + // } + // }; + + const [onFocus, isActive, ref] = useRovingTabIndex(); + const onDateInputKeyDown = (e: React.KeyboardEvent): void => { - // Don't interfere with input default keydown behaviour. For example, - // without this, when pressing "Space" while in the input, it would - // scroll down the timeline. - e.stopPropagation(); - - // Ignore the tab key which is probably just navigating focus around - // with the keyboard - if(e.key === "Tab") { - return; - } + // // Don't interfere with input default keydown behaviour. For example, + // // without this, when pressing "Space" while in the input, it would + // // scroll down the timeline. + // e.stopPropagation(); + + // // 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") { @@ -113,23 +138,23 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { }; const onFormKeyDown = (e: React.KeyboardEvent) => { - // Stop the ContextMenu from closing when we first tab from the form to - // the datePicker. - if(e.target === e.currentTarget && e.key === Key.TAB) { - e.stopPropagation(); - } + // // Stop the ContextMenu from closing when we first tab from the form to + // // the datePicker. + // if(e.target === e.currentTarget && e.key === Key.TAB) { + // e.stopPropagation(); + // } } const onGoKeyDown = (e: React.KeyboardEvent) => { - // Stop the ContextMenu from closing when we tab backwards to the - // datePicker from the "Go" button - // - // If they tab forwards off the end of the ContextMenu, we want to close - // the ContextMenu which will put the focus back where we were before - // opening the ContextMenu. - if (e.key === Key.TAB && e.shiftKey) { - e.stopPropagation(); - } + // // Stop the ContextMenu from closing when we tab backwards to the + // // datePicker from the "Go" button + // // + // // If they tab forwards off the end of the ContextMenu, we want to close + // // the ContextMenu which will put the focus back where we were before + // // opening the ContextMenu. + // if (e.key === Key.TAB && e.shiftKey) { + // e.stopPropagation(); + // } } const onJumpToDateSubmit = (): void => { @@ -141,33 +166,32 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { className="mx_JumpToDatePicker_form" onSubmit={onJumpToDateSubmit} onKeyDown={onFormKeyDown} - onFocus={onFocus} - ref={ref} - tabIndex={isActive ? 0 : -1} + // onFocus={onFocus} + // ref={ref} + // tabIndex={isActive ? 0 : -1} > Jump to date - - { _t("Go") } - + ) }; From 86c7baf6607cfb5c40a30b03e80a4b9ab6a5adbe Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 21:34:10 -0600 Subject: [PATCH 24/47] Use Field with CustomInput passed through --- src/components/views/elements/CustomInput.tsx | 58 ++++++++ src/components/views/elements/Field.tsx | 26 ++-- .../views/messages/JumpToDatePicker.tsx | 127 ++---------------- 3 files changed, 86 insertions(+), 125 deletions(-) create mode 100644 src/components/views/elements/CustomInput.tsx diff --git a/src/components/views/elements/CustomInput.tsx b/src/components/views/elements/CustomInput.tsx new file mode 100644 index 00000000000..865e1158d64 --- /dev/null +++ b/src/components/views/elements/CustomInput.tsx @@ -0,0 +1,58 @@ +import React, { useRef, useEffect } from 'react'; + +// via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd +function useCombinedRefs(...refs) { + const targetRef = useRef() + + useEffect(() => { + refs.forEach(ref => { + if (!ref) return + + if (typeof ref === 'function') { + ref(targetRef.current) + } else { + ref.current = targetRef.current + } + }) + }, [refs]) + + return targetRef +} + +interface CustomInputProps extends Omit, 'onChange' | 'onInput'> { + 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). +*/ +const CustomInput = React.forwardRef((props: CustomInputProps, ref) => { + const registerCallbacks = (input: HTMLInputElement | null) => { + if (input) { + input.onchange = props.onChange; + input.oninput = props.onInput; + } + }; + + return {}} + onInput={() => {}} + />; +}); + +export default CustomInput; diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 00f62b415aa..491ed529b87 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react'; +import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, RefObject } from 'react'; import classNames from 'classnames'; import { debounce } from "lodash"; @@ -79,6 +79,8 @@ interface IProps { } export interface IInputProps extends IProps, InputHTMLAttributes { + // The ref pass through to the input + inputRef?: RefObject; // The element to create. Defaults to "input". element?: "input"; // The input's value. This is a controlled component, so the value is required. @@ -86,6 +88,8 @@ export interface IInputProps extends IProps, InputHTMLAttributes { + // The ref pass through to the select + inputRef?: RefObject; // To define options for a select, use element: "select"; // The select's value. This is a controlled component, so the value is required. @@ -93,18 +97,20 @@ interface ISelectProps extends IProps, SelectHTMLAttributes { } interface ITextareaProps extends IProps, TextareaHTMLAttributes { + // The ref pass through to the textrea + inputRef?: RefObject; element: "textarea"; // The textarea's value. This is a controlled component, so the value is required. value: string; } export interface ICustomInputProps extends IProps, InputHTMLAttributes { + // The ref pass through to the input + inputRef?: RefObject; // 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; @@ -118,7 +124,7 @@ interface IState { export default class Field extends React.PureComponent { private id: string; - private input: HTMLInputElement; + private inputRef: RefObject; public static readonly defaultProps = { element: "input", @@ -156,7 +162,7 @@ export default class Field extends React.PureComponent { } public focus() { - this.input.focus(); + this.inputRef.current?.focus(); // programmatic does not fire onFocus handler this.setState({ focused: true, @@ -207,7 +213,7 @@ export default class Field extends React.PureComponent { if (!this.props.onValidate) { return; } - const value = this.input ? this.input.value : null; + const value = this.inputRef.current?.value ?? null; const { valid, feedback } = await this.props.onValidate({ value, focused, @@ -238,13 +244,13 @@ export default class Field extends React.PureComponent { public render() { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ - const { element, prefixComponent, postfixComponent, className, onValidate, children, + const { element, inputRef, prefixComponent, postfixComponent, className, onValidate, children, tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus, usePlaceholderAsHint, forceTooltipVisible, ...inputProps } = this.props; - // Set some defaults for the element - const ref = input => this.input = input; + this.inputRef = inputRef || React.createRef(); + inputProps.placeholder = inputProps.placeholder || inputProps.label; inputProps.id = this.id; // this overwrites the id from props @@ -253,7 +259,7 @@ export default class Field extends React.PureComponent { inputProps.onBlur = this.onBlur; // Appease typescript's inference - const inputProps_ = { ...inputProps, ref, list }; + const inputProps_ = { ...inputProps, ref: this.inputRef, list }; const fieldInput = React.createElement(this.props.element, inputProps_, children); diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index b523c4d3ceb..7efd1fdad2a 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -1,71 +1,14 @@ -import React, { useRef, useState } from 'react'; +import React, { useState } from 'react'; import { _t } from '../../../languageHandler'; -import { Key } from "../../../Keyboard"; import Field from "../elements/Field"; -import AccessibleButton from "../elements/AccessibleButton"; +import CustomInput from "../elements/CustomInput"; import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; -// via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd -function useCombinedRefs(...refs) { - const targetRef = React.useRef() - - React.useEffect(() => { - refs.forEach(ref => { - if (!ref) return - - if (typeof ref === 'function') { - ref(targetRef.current) - } else { - ref.current = targetRef.current - } - }) - }, [refs]) - - return targetRef - } - -interface CustomInputProps extends Omit, 'onChange' | 'onInput'> { - 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). -*/ -const CustomInput = React.forwardRef((props: CustomInputProps, ref) => { - const registerCallbacks = (input: HTMLInputElement | null) => { - if (input) { - input.onchange = props.onChange; - input.oninput = props.onInput; - } - }; - - return {}} - onInput={() => {}} - />; -}); - - interface IProps { - ts: number; - onDatePicked?: (dateString: string) => void; + ts: number; + onDatePicked?: (dateString: string) => void; } const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { @@ -83,18 +26,18 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { // Since we're using CustomInput with native JavaScript behavior, this // tracks the date value changes as they come in. - const onDateValueInput = (e: Event): void => { + const onDateValueInput = (e: React.ChangeEvent): void => { console.log('onDateValueInput') - setDateValue((e.target as HTMLInputElement).value); + setDateValue(e.target.value); }; // 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. - const onDateValueChange = (e: Event): void => { + const onDateValueChange = (e: React.ChangeEvent): void => { console.log('onDateValueChange') - setDateValue((e.target as HTMLInputElement).value); + setDateValue(e.target.value); // Don't auto navigate if they were manually typing out a date if(navigateOnDatePickerSelection) { @@ -102,30 +45,9 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { } }; - // const inputRef = React.useRef(null); - // const registerInputCallbacks = (input: HTMLInputElement | null) => { - // inputRef.current = input; - - // if (input) { - // input.onchange = onDateValueChange; - // input.oninput = onDateValueInput; - // } - // }; - const [onFocus, isActive, ref] = useRovingTabIndex(); const onDateInputKeyDown = (e: React.KeyboardEvent): void => { - // // Don't interfere with input default keydown behaviour. For example, - // // without this, when pressing "Space" while in the input, it would - // // scroll down the timeline. - // e.stopPropagation(); - - // // 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") { onDatePicked(dateValue); @@ -137,26 +59,6 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { setNavigateOnDatePickerSelection(false); }; - const onFormKeyDown = (e: React.KeyboardEvent) => { - // // Stop the ContextMenu from closing when we first tab from the form to - // // the datePicker. - // if(e.target === e.currentTarget && e.key === Key.TAB) { - // e.stopPropagation(); - // } - } - - const onGoKeyDown = (e: React.KeyboardEvent) => { - // // Stop the ContextMenu from closing when we tab backwards to the - // // datePicker from the "Go" button - // // - // // If they tab forwards off the end of the ContextMenu, we want to close - // // the ContextMenu which will put the focus back where we were before - // // opening the ContextMenu. - // if (e.key === Key.TAB && e.shiftKey) { - // e.stopPropagation(); - // } - } - const onJumpToDateSubmit = (): void => { onDatePicked(dateValue); } @@ -165,30 +67,25 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => {
    Jump to date - { _t("Go") } From 9513731a3bba912785a9880a9facf4d9fc92dd28 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 22:02:22 -0600 Subject: [PATCH 25/47] Clean up PR and add behind feature flag --- src/components/views/elements/Field.tsx | 2 +- .../views/messages/DateSeparator.tsx | 33 ++++++++++++++++--- .../views/messages/JumpToDatePicker.tsx | 2 -- yarn.lock | 2 +- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 491ed529b87..1bae6750e8a 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -97,7 +97,7 @@ interface ISelectProps extends IProps, SelectHTMLAttributes { } interface ITextareaProps extends IProps, TextareaHTMLAttributes { - // The ref pass through to the textrea + // The ref pass through to the textarea inputRef?: RefObject; element: "textarea"; // The textarea's value. This is a controlled component, so the value is required. diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index a4625458416..c8074827ab8 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -24,6 +24,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; import dis from '../../../dispatcher/dispatcher'; import { Action } from '../../../dispatcher/actions'; +import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from '../../../settings/SettingsStore'; import { UIFeature } from '../../../settings/UIFeature'; @@ -56,14 +57,29 @@ interface IProps { } interface IState { - contextMenuPosition?: DOMRect + contextMenuPosition?: DOMRect, + jumpToDateEnabled: boolean; } @replaceableComponent("views.messages.DateSeparator") export default class DateSeparator extends React.Component { + settingWatcherRef = null; + constructor(props, context) { super(props, context); - this.state = {}; + this.state = { + jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"), + }; + } + + componentWillMount() { + this.settingWatcherRef = SettingsStore.watchSetting("feature_jump_to_date", null, (settingName, roomId, level, newValAtLevel, newVal) => { + this.setState({ jumpToDateEnabled: newVal }); + }); + } + + componentWillUnmount() { + SettingsStore.unwatchSetting(this.settingWatcherRef); } private onContextMenuOpenClick = (e: React.MouseEvent): void => { @@ -107,7 +123,6 @@ export default class DateSeparator extends React.Component { } private pickDate = async (inputTimestamp): Promise => { - console.log('pickDate', inputTimestamp) const unixTimestamp = new Date(inputTimestamp).getTime(); const cli = MatrixClientPeg.get(); @@ -118,7 +133,7 @@ export default class DateSeparator extends React.Component { unixTimestamp, Direction.Forward ); - console.log(`/timestamp_to_event: found ${event_id} (${origin_server_ts}) for timestamp=${unixTimestamp}`) + logger.log(`/timestamp_to_event: found ${event_id} (${origin_server_ts}) for timestamp=${unixTimestamp} (looking forward)`) dis.dispatch({ action: Action.ViewRoom, @@ -215,11 +230,19 @@ export default class DateSeparator extends React.Component { render() { const label = this.getLabel(); + + let dateHeaderContent; + if(this.state.jumpToDateEnabled) { + dateHeaderContent = this.renderNotificationsMenu() + } else { + dateHeaderContent = + } + // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return


    - { this.renderNotificationsMenu() } + { dateHeaderContent }

    ; } diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 7efd1fdad2a..9e70506b9cd 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -27,7 +27,6 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { // Since we're using CustomInput with native JavaScript behavior, this // tracks the date value changes as they come in. const onDateValueInput = (e: React.ChangeEvent): void => { - console.log('onDateValueInput') setDateValue(e.target.value); }; @@ -36,7 +35,6 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { // 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. const onDateValueChange = (e: React.ChangeEvent): void => { - console.log('onDateValueChange') setDateValue(e.target.value); // Don't auto navigate if they were manually typing out a date diff --git a/yarn.lock b/yarn.lock index 3f4ce0ff642..22012e31653 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3706,7 +3706,7 @@ eslint-module-utils@^2.7.2: debug "^3.2.7" find-up "^2.1.0" -eslint-plugin-import@^2.25.4: +eslint-plugin-import@^2.25.2, eslint-plugin-import@^2.25.4: version "2.25.4" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== From 4ff303267b5a86f8e34c6cc0dc8df136acf9baf5 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 22:24:14 -0600 Subject: [PATCH 26/47] More cleanup --- src/components/views/elements/CustomInput.tsx | 2 +- src/components/views/messages/DateSeparator.tsx | 2 ++ src/components/views/messages/MessageActionBar.tsx | 8 ++------ src/components/views/rooms/RoomList.tsx | 2 ++ 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/CustomInput.tsx b/src/components/views/elements/CustomInput.tsx index 865e1158d64..6b52f1894c7 100644 --- a/src/components/views/elements/CustomInput.tsx +++ b/src/components/views/elements/CustomInput.tsx @@ -38,7 +38,7 @@ interface CustomInputProps extends Omit { +const CustomInput: React.FC = React.forwardRef((props: CustomInputProps, ref) => { const registerCallbacks = (input: HTMLInputElement | null) => { if (input) { input.onchange = props.onChange; diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index c8074827ab8..fbe8a95151e 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -73,6 +73,8 @@ export default class DateSeparator extends React.Component { } componentWillMount() { + // We're using a watcher so the date headers in the timeline are updated + // when the lab setting is toggled. this.settingWatcherRef = SettingsStore.watchSetting("feature_jump_to_date", null, (settingName, roomId, level, newValAtLevel, newVal) => { this.setState({ jumpToDateEnabled: newVal }); }); diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index be96e73ddb0..6213e3b050b 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -68,10 +68,8 @@ const OptionsButton: React.FC = ({ const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { - // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually - onFocus(); onFocusChange(menuDisplayed); - }, [onFocus, onFocusChange, menuDisplayed]); + }, [onFocusChange, menuDisplayed]); let contextMenu: ReactElement | null; if (menuDisplayed) { @@ -121,10 +119,8 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { - // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually - onFocus(); onFocusChange(menuDisplayed); - }, [onFocus, onFocusChange, menuDisplayed]); + }, [onFocusChange, menuDisplayed]); let contextMenu; if (menuDisplayed) { diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 37165638d66..8c90c1ed18d 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -169,6 +169,7 @@ const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonPro return <> { return <> Date: Fri, 14 Jan 2022 22:38:25 -0600 Subject: [PATCH 27/47] Restore tabindex because jump to date menu is still keyboard navigable *shrug* --- src/components/views/messages/DateSeparator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index fbe8a95151e..352afe56589 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -242,7 +242,7 @@ export default class DateSeparator extends React.Component { // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers - return

    + return


    { dateHeaderContent }
    From 87c17981294ab64c1fa7346d3382dc1e3cedf5f5 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 22:39:38 -0600 Subject: [PATCH 28/47] Revert random yarn.lock changes --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 22012e31653..3f4ce0ff642 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3706,7 +3706,7 @@ eslint-module-utils@^2.7.2: debug "^3.2.7" find-up "^2.1.0" -eslint-plugin-import@^2.25.2, eslint-plugin-import@^2.25.4: +eslint-plugin-import@^2.25.4: version "2.25.4" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== From 9acd854b69745ecf98659ed0599a3fe473797edd Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 22:59:53 -0600 Subject: [PATCH 29/47] Fix lints --- res/css/views/messages/_JumpToDatePicker.scss | 12 ++--- src/components/structures/MessagePanel.tsx | 6 ++- .../dialogs/MessageEditHistoryDialog.tsx | 2 +- src/components/views/elements/CustomInput.tsx | 14 ++--- src/components/views/elements/Field.tsx | 27 ++++++---- .../views/messages/DateSeparator.tsx | 51 ++++++++++--------- .../views/messages/JumpToDatePicker.tsx | 14 ++--- src/utils/exportUtils/HtmlExport.tsx | 6 ++- 8 files changed, 75 insertions(+), 57 deletions(-) diff --git a/res/css/views/messages/_JumpToDatePicker.scss b/res/css/views/messages/_JumpToDatePicker.scss index ddde08a59dc..0f0cb6b2d16 100644 --- a/res/css/views/messages/_JumpToDatePicker.scss +++ b/res/css/views/messages/_JumpToDatePicker.scss @@ -1,17 +1,17 @@ .mx_JumpToDatePicker_form { - display: flex; - align-items: center; + display: flex; + align-items: center; } .mx_JumpToDatePicker_label { - font-size: $font-15px; + font-size: $font-15px; } .mx_JumpToDatePicker_datePicker { - margin: 0; - margin-left: 8px; + margin: 0; + margin-left: 8px; } .mx_JumpToDatePicker_submitButton { - margin-left: 8px; + margin-left: 8px; } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 72d67ad3a07..a63a0712e2b 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -723,7 +723,11 @@ export default class MessagePanel extends React.Component { // do we need a date separator since the last event? const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); if (wantsDateSeparator && !isGrouped && this.props.room) { - const dateSeparator =
  • ; + const dateSeparator = ( +
  • + +
  • + ); ret.push(dateSeparator); } diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx index 74615046a45..005f77b05f2 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx +++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx @@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { - nodes.push(
  • ); + nodes.push(
  • ); } const isBaseEvent = e.getId() === baseEventId; nodes.push(( diff --git a/src/components/views/elements/CustomInput.tsx b/src/components/views/elements/CustomInput.tsx index 6b52f1894c7..b82275c8df5 100644 --- a/src/components/views/elements/CustomInput.tsx +++ b/src/components/views/elements/CustomInput.tsx @@ -2,21 +2,21 @@ import React, { useRef, useEffect } from 'react'; // via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd function useCombinedRefs(...refs) { - const targetRef = useRef() + const targetRef = useRef(); useEffect(() => { refs.forEach(ref => { - if (!ref) return + if (!ref) return; if (typeof ref === 'function') { - ref(targetRef.current) + ref(targetRef.current); } else { - ref.current = targetRef.current + ref.current = targetRef.current; } - }) - }, [refs]) + }); + }, [refs]); - return targetRef + return targetRef; } interface CustomInputProps extends Omit, 'onChange' | 'onInput'> { diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 1bae6750e8a..1e7e2da8ea1 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -273,17 +273,22 @@ export default class Field extends React.PureComponent { } const hasValidationFlag = forceValidity !== null && forceValidity !== undefined; - 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. - mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint, - mx_Field_placeholderIsHint: usePlaceholderAsHint, - mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true, - mx_Field_invalid: hasValidationFlag - ? !forceValidity - : onValidate && this.state.valid === false, - }); + 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. + mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint, + mx_Field_placeholderIsHint: usePlaceholderAsHint, + mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true, + mx_Field_invalid: hasValidationFlag + ? !forceValidity + : onValidate && this.state.valid === false, + }, + ); // Handle displaying feedback on validity // FIXME: Using an import will result in test failures diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 352afe56589..d61511ad816 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -16,16 +16,15 @@ limitations under the License. */ import React from 'react'; +import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; +import { logger } from "matrix-js-sdk/src/logger"; import { _t } from '../../../languageHandler'; import { formatFullDateNoTime } from '../../../DateUtils'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; import dis from '../../../dispatcher/dispatcher'; import { Action } from '../../../dispatcher/actions'; -import { logger } from "matrix-js-sdk/src/logger"; - import SettingsStore from '../../../settings/SettingsStore'; import { UIFeature } from '../../../settings/UIFeature'; import Modal from '../../../Modal'; @@ -51,13 +50,13 @@ function getDaysArray(): string[] { } interface IProps { - roomId: string, + roomId: string; ts: number; forExport?: boolean; } interface IState { - contextMenuPosition?: DOMRect, + contextMenuPosition?: DOMRect; jumpToDateEnabled: boolean; } @@ -71,15 +70,19 @@ export default class DateSeparator extends React.Component { jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"), }; } - + componentWillMount() { // We're using a watcher so the date headers in the timeline are updated // when the lab setting is toggled. - this.settingWatcherRef = SettingsStore.watchSetting("feature_jump_to_date", null, (settingName, roomId, level, newValAtLevel, newVal) => { - this.setState({ jumpToDateEnabled: newVal }); - }); + this.settingWatcherRef = SettingsStore.watchSetting( + "feature_jump_to_date", + null, + (settingName, roomId, level, newValAtLevel, newVal) => { + this.setState({ jumpToDateEnabled: newVal }); + }, + ); } - + componentWillUnmount() { SettingsStore.unwatchSetting(this.settingWatcherRef); } @@ -129,17 +132,20 @@ export default class DateSeparator extends React.Component { const cli = MatrixClientPeg.get(); try { - const roomId = this.props.roomId - const { event_id, origin_server_ts } = await cli.timestampToEvent( + const roomId = this.props.roomId; + const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent( roomId, unixTimestamp, - Direction.Forward + Direction.Forward, + ); + logger.log( + `/timestamp_to_event: ` + + `found ${eventId} (${originServerTs}) for timestamp=${unixTimestamp} (looking forward)`, ); - logger.log(`/timestamp_to_event: found ${event_id} (${origin_server_ts}) for timestamp=${unixTimestamp} (looking forward)`) dis.dispatch({ action: Action.ViewRoom, - event_id, + eventId, highlighted: true, room_id: roomId, }); @@ -158,7 +164,6 @@ export default class DateSeparator extends React.Component { } }; - private onLastWeekClicked = (): void => { const date = new Date(); // This just goes back 7 days. @@ -166,7 +171,7 @@ export default class DateSeparator extends React.Component { date.setDate(date.getDate() - 7); this.pickDate(date); this.closeMenu(); - } + }; private onLastMonthClicked = (): void => { const date = new Date(); @@ -174,18 +179,18 @@ export default class DateSeparator extends React.Component { date.setMonth(date.getMonth() - 1, 1); this.pickDate(date); this.closeMenu(); - } + }; private onTheBeginningClicked = (): void => { const date = new Date(0); this.pickDate(date); this.closeMenu(); - } + }; private onDatePicked = (dateString): void => { this.pickDate(dateString); this.closeMenu(); - } + }; private renderNotificationsMenu(): React.ReactElement { let contextMenu: JSX.Element; @@ -234,10 +239,10 @@ export default class DateSeparator extends React.Component { const label = this.getLabel(); let dateHeaderContent; - if(this.state.jumpToDateEnabled) { - dateHeaderContent = this.renderNotificationsMenu() + if (this.state.jumpToDateEnabled) { + dateHeaderContent = this.renderNotificationsMenu(); } else { - dateHeaderContent = + dateHeaderContent = ; } // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 9e70506b9cd..9b7c152f67d 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import { _t } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; import CustomInput from "../elements/CustomInput"; import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; @@ -14,8 +14,8 @@ interface IProps { const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { const date = new Date(ts); const year = date.getFullYear(); - const month = `${date.getMonth() + 1}`.padStart(2, "0") - const day = `${date.getDate()}`.padStart(2, "0") + const month = `${date.getMonth() + 1}`.padStart(2, "0"); + const day = `${date.getDate()}`.padStart(2, "0"); const dateDefaultValue = `${year}-${month}-${day}`; const [dateValue, setDateValue] = useState(dateDefaultValue); @@ -38,7 +38,7 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { setDateValue(e.target.value); // Don't auto navigate if they were manually typing out a date - if(navigateOnDatePickerSelection) { + if (navigateOnDatePickerSelection) { onDatePicked(dateValue); } }; @@ -47,7 +47,7 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { const onDateInputKeyDown = (e: React.KeyboardEvent): void => { // Go and navigate if they submitted - if(e.key === "Enter") { + if (e.key === "Enter") { onDatePicked(dateValue); return; } @@ -59,7 +59,7 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { const onJumpToDateSubmit = (): void => { onDatePicked(dateValue); - } + }; return ( = ({ ts, onDatePicked }: IProps) => { { _t("Go") } - ) + ); }; export default JumpToDatePicker; diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index ae0f9ca26e9..83dc5b03ddf 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -248,7 +248,11 @@ export default class HTMLExporter extends Exporter { protected getDateSeparator(event: MatrixEvent) { const ts = event.getTs(); - const dateSeparator =
  • ; + const dateSeparator = ( +
  • + +
  • + ); return renderToStaticMarkup(dateSeparator); } From 8c2488f297eb56488ac900ab63717e5fc0a9cb54 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 23:18:09 -0600 Subject: [PATCH 30/47] Add snapshot test when jump to date is enabled --- .../views/messages/DateSeparator.tsx | 2 - .../views/messages/DateSeparator-test.tsx | 26 +++++- .../__snapshots__/DateSeparator-test.tsx.snap | 84 +++++++++++++++++++ 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index d61511ad816..0bdd8a65b19 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -69,9 +69,7 @@ export default class DateSeparator extends React.Component { this.state = { jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"), }; - } - componentWillMount() { // We're using a watcher so the date headers in the timeline are updated // when the lab setting is toggled. this.settingWatcherRef = SettingsStore.watchSetting( diff --git a/test/components/views/messages/DateSeparator-test.tsx b/test/components/views/messages/DateSeparator-test.tsx index 0a3db003f19..e4784ce0d20 100644 --- a/test/components/views/messages/DateSeparator-test.tsx +++ b/test/components/views/messages/DateSeparator-test.tsx @@ -64,7 +64,11 @@ describe("DateSeparator", () => { beforeEach(() => { global.Date = MockDate as unknown as DateConstructor; - (SettingsStore.getValue as jest.Mock).mockReturnValue(true); + (SettingsStore.getValue as jest.Mock) = jest.fn((arg) => { + if(arg === UIFeature.TimelineEnableRelativeDates) { + return true; + } + }); }); afterAll(() => { @@ -89,10 +93,28 @@ describe("DateSeparator", () => { describe('when Settings.TimelineEnableRelativeDates is falsy', () => { beforeEach(() => { - (SettingsStore.getValue as jest.Mock).mockReturnValue(false); + (SettingsStore.getValue as jest.Mock) = jest.fn((arg) => { + if(arg === UIFeature.TimelineEnableRelativeDates) { + return false; + } + }); }); it.each(testCases)('formats date in full when current time is %s', (_d, ts) => { expect(getComponent({ ts, forExport: false }).text()).toEqual(formatFullDateNoTime(new Date(ts))); }); }); + + describe('when feature_jump_to_date is enabled', () => { + beforeEach(() => { + (SettingsStore.getValue as jest.Mock) = jest.fn((arg) => { + if(arg === "feature_jump_to_date") { + return true; + } + }); + }); + it('renders the date separator correctly', () => { + const component = getComponent(); + expect(component).toMatchSnapshot(); + }); + }); }); diff --git a/test/components/views/messages/__snapshots__/DateSeparator-test.tsx.snap b/test/components/views/messages/__snapshots__/DateSeparator-test.tsx.snap index 69b5a34defc..cc65b5cb1f5 100644 --- a/test/components/views/messages/__snapshots__/DateSeparator-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/DateSeparator-test.tsx.snap @@ -30,3 +30,87 @@ exports[`DateSeparator renders the date separator correctly 1`] = ` `; + +exports[`DateSeparator when feature_jump_to_date is enabled renders the date separator correctly 1`] = ` + + +

    +
    + + + +
    + +
    +
    + + + +
    +

    +
    +
    +`; From c408443040a0f2b7fb1115a1a0d83efbc51cb92b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 23:28:15 -0600 Subject: [PATCH 31/47] Fix lints --- test/components/views/messages/DateSeparator-test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/components/views/messages/DateSeparator-test.tsx b/test/components/views/messages/DateSeparator-test.tsx index e4784ce0d20..574a22289d7 100644 --- a/test/components/views/messages/DateSeparator-test.tsx +++ b/test/components/views/messages/DateSeparator-test.tsx @@ -65,7 +65,7 @@ describe("DateSeparator", () => { beforeEach(() => { global.Date = MockDate as unknown as DateConstructor; (SettingsStore.getValue as jest.Mock) = jest.fn((arg) => { - if(arg === UIFeature.TimelineEnableRelativeDates) { + if (arg === UIFeature.TimelineEnableRelativeDates) { return true; } }); @@ -94,7 +94,7 @@ describe("DateSeparator", () => { describe('when Settings.TimelineEnableRelativeDates is falsy', () => { beforeEach(() => { (SettingsStore.getValue as jest.Mock) = jest.fn((arg) => { - if(arg === UIFeature.TimelineEnableRelativeDates) { + if (arg === UIFeature.TimelineEnableRelativeDates) { return false; } }); @@ -107,7 +107,7 @@ describe("DateSeparator", () => { describe('when feature_jump_to_date is enabled', () => { beforeEach(() => { (SettingsStore.getValue as jest.Mock) = jest.fn((arg) => { - if(arg === "feature_jump_to_date") { + if (arg === "feature_jump_to_date") { return true; } }); From 5fb3c0b586c505af0f1793bae551c8a5ab08d664 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 14 Jan 2022 23:28:42 -0600 Subject: [PATCH 32/47] Update i18n --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cebb6005f47..6d661e1fba3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2060,8 +2060,6 @@ "Last month": "Last month", "The beginning of the room": "The beginning of the room", "Jump to date": "Jump to date", - "Pick a date to jump to": "Pick a date to jump to", - "Go": "Go", "Downloading": "Downloading", "Decrypting": "Decrypting", "Download": "Download", @@ -2073,6 +2071,8 @@ "Ignored attempt to disable encryption": "Ignored attempt to disable encryption", "Encryption not enabled": "Encryption not enabled", "The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.", + "Pick a date to jump to": "Pick a date to jump to", + "Go": "Go", "Error processing audio message": "Error processing audio message", "React": "React", "Edit": "Edit", From 24534e87d14d75169911c0f5dad34388060cee87 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 24 Jan 2022 23:29:42 -0600 Subject: [PATCH 33/47] Make border-radius on input match button Fix https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r785269753 --- res/css/views/messages/_JumpToDatePicker.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_JumpToDatePicker.scss b/res/css/views/messages/_JumpToDatePicker.scss index 0f0cb6b2d16..37795916e00 100644 --- a/res/css/views/messages/_JumpToDatePicker.scss +++ b/res/css/views/messages/_JumpToDatePicker.scss @@ -1,15 +1,19 @@ .mx_JumpToDatePicker_form { display: flex; - align-items: center; } .mx_JumpToDatePicker_label { + align-self: center; font-size: $font-15px; } .mx_JumpToDatePicker_datePicker { margin: 0; margin-left: 8px; + + &, & > input { + border-radius: 8px; + } } .mx_JumpToDatePicker_submitButton { From 726a569bb739753cdd0ab220cce039098e686622 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 24 Jan 2022 23:36:40 -0600 Subject: [PATCH 34/47] Add copyright headers --- res/css/views/messages/_JumpToDatePicker.scss | 16 ++++++++++++++++ src/components/views/elements/CustomInput.tsx | 16 ++++++++++++++++ .../views/messages/JumpToDatePicker.tsx | 15 +++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/res/css/views/messages/_JumpToDatePicker.scss b/res/css/views/messages/_JumpToDatePicker.scss index 37795916e00..5722ed1306e 100644 --- a/res/css/views/messages/_JumpToDatePicker.scss +++ b/res/css/views/messages/_JumpToDatePicker.scss @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + .mx_JumpToDatePicker_form { display: flex; } diff --git a/src/components/views/elements/CustomInput.tsx b/src/components/views/elements/CustomInput.tsx index b82275c8df5..6383efd6c47 100644 --- a/src/components/views/elements/CustomInput.tsx +++ b/src/components/views/elements/CustomInput.tsx @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React, { useRef, useEffect } from 'react'; // via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 9b7c152f67d..0f354727e0a 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -1,3 +1,18 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ import React, { useState } from 'react'; From 20939a2379e9b42c3d555f0acc061db5c7ced672 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 24 Jan 2022 23:38:12 -0600 Subject: [PATCH 35/47] Just use simple back 7 days logic See https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r785270097 --- src/components/views/messages/DateSeparator.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 0bdd8a65b19..8d5a1bb7d27 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -164,8 +164,6 @@ export default class DateSeparator extends React.Component { private onLastWeekClicked = (): void => { const date = new Date(); - // This just goes back 7 days. - // FIXME: Do we want this to go back to the last Sunday? https://upokary.com/how-to-get-last-monday-or-last-friday-or-any-last-day-in-javascript/ date.setDate(date.getDate() - 7); this.pickDate(date); this.closeMenu(); From 5f9802601196febc7bf1bdc1ed478e7e0fa00f4e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 24 Jan 2022 23:45:48 -0600 Subject: [PATCH 36/47] Some CustomInput cleanup --- src/components/views/elements/CustomInput.tsx | 26 +++----------- src/hooks/useCombinedRefs.ts | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useCombinedRefs.ts diff --git a/src/components/views/elements/CustomInput.tsx b/src/components/views/elements/CustomInput.tsx index 6383efd6c47..b8d46184562 100644 --- a/src/components/views/elements/CustomInput.tsx +++ b/src/components/views/elements/CustomInput.tsx @@ -14,31 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useRef, useEffect } from 'react'; +import React from 'react'; -// via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd -function useCombinedRefs(...refs) { - const targetRef = useRef(); +import { useCombinedRefs } from "../../../hooks/useCombinedRefs"; - useEffect(() => { - refs.forEach(ref => { - if (!ref) return; - - if (typeof ref === 'function') { - ref(targetRef.current); - } else { - ref.current = targetRef.current; - } - }); - }, [refs]); - - return targetRef; -} - -interface CustomInputProps extends Omit, 'onChange' | 'onInput'> { +interface IProps extends Omit, 'onChange' | 'onInput'> { 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 @@ -54,7 +38,7 @@ interface CustomInputProps extends Omit = React.forwardRef((props: CustomInputProps, ref) => { +const CustomInput: React.FC = React.forwardRef((props: IProps, ref) => { const registerCallbacks = (input: HTMLInputElement | null) => { if (input) { input.onchange = props.onChange; diff --git a/src/hooks/useCombinedRefs.ts b/src/hooks/useCombinedRefs.ts new file mode 100644 index 00000000000..2d1f71620e8 --- /dev/null +++ b/src/hooks/useCombinedRefs.ts @@ -0,0 +1,36 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useRef, useEffect } from 'react'; + +// via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd +export const useCombinedRefs = (...refs) => { + const targetRef = useRef(); + + useEffect(() => { + refs.forEach(ref => { + if (!ref) return; + + if (typeof ref === 'function') { + ref(targetRef.current); + } else { + ref.current = targetRef.current; + } + }); + }, [refs]); + + return targetRef; +} From 01101a2e4dd713dc9110a613eef02e1227d49ceb Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 25 Jan 2022 00:19:58 -0600 Subject: [PATCH 37/47] Use props.componentClass to define something custom See https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r791164064 --- src/components/views/elements/Field.tsx | 12 ++++++++---- src/components/views/messages/JumpToDatePicker.tsx | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 1e7e2da8ea1..78b1c189f52 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -83,6 +83,7 @@ export interface IInputProps extends IProps, InputHTMLAttributes; // The element to create. Defaults to "input". element?: "input"; + componentClass?: undefined; // The input's value. This is a controlled component, so the value is required. value: string; } @@ -92,6 +93,7 @@ interface ISelectProps extends IProps, SelectHTMLAttributes { inputRef?: RefObject; // To define options for a select, use element: "select"; + componentClass?: undefined; // The select's value. This is a controlled component, so the value is required. value: string; } @@ -100,6 +102,7 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes; element: "textarea"; + componentClass?: undefined; // The textarea's value. This is a controlled component, so the value is required. value: string; } @@ -107,8 +110,9 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes { // The ref pass through to the input inputRef?: RefObject; - // The element to create. - element: ComponentClass; + element: "input"; + // The custom component to render + componentClass: ComponentClass; // The input's value. This is a controlled component, so the value is required. value: string; } @@ -261,7 +265,7 @@ export default class Field extends React.PureComponent { // Appease typescript's inference const inputProps_ = { ...inputProps, ref: this.inputRef, list }; - const fieldInput = React.createElement(this.props.element, inputProps_, children); + const fieldInput = React.createElement(this.props.componentClass || this.props.element, inputProps_, children); let prefixContainer = null; if (prefixComponent) { @@ -275,7 +279,7 @@ export default class Field extends React.PureComponent { const hasValidationFlag = forceValidity !== null && forceValidity !== undefined; const fieldClasses = classNames( "mx_Field", - `mx_Field_${typeof this.props.element === "string" ? this.props.element : "input"}`, + `mx_Field_${this.props.element}`, className, { // If we have a prefix element, leave the label always at the top left and diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 0f354727e0a..05c9acf1ac2 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -83,7 +83,7 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { > Jump to date Date: Tue, 25 Jan 2022 00:21:25 -0600 Subject: [PATCH 38/47] Fix camelCase typo --- src/components/views/messages/DateSeparator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 8d5a1bb7d27..8efd48b36e4 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -143,7 +143,7 @@ export default class DateSeparator extends React.Component { dis.dispatch({ action: Action.ViewRoom, - eventId, + event_id: eventId, highlighted: true, room_id: roomId, }); From 26301ea88c7f0129447344d6d4af609be609f075 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 25 Jan 2022 00:24:54 -0600 Subject: [PATCH 39/47] Update labs flag to explain what else is added --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3da1e375f2a..d7eb599da00 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -890,7 +890,7 @@ "Meta Spaces": "Meta Spaces", "Use new room breadcrumbs": "Use new room breadcrumbs", "New spotlight search experience": "New spotlight search experience", - "Jump to date (adds /jumptodate)": "Jump to date (adds /jumptodate)", + "Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)", "Don't send read receipts": "Don't send read receipts", "Font size": "Font size", "Use custom size": "Use custom size", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 142559ef103..8906d8fe021 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -378,7 +378,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { // by default. We will conditionally show it depending on whether we can // detect MSC3030 support (see LabUserSettingsTab.tsx). // labsGroup: LabGroup.Messaging, - displayName: _td("Jump to date (adds /jumptodate)"), + displayName: _td("Jump to date (adds /jumptodate and jump to date headers)"), supportedLevels: LEVELS_FEATURE, default: false, }, From ee4829f31fad304693150f983cbb8480deaeebf6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 25 Jan 2022 00:27:10 -0600 Subject: [PATCH 40/47] Remove unrecognized prop from being passed on to the inner element --- src/components/views/elements/Field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 78b1c189f52..fc7b3b84c7c 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -248,7 +248,7 @@ export default class Field extends React.PureComponent { public render() { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ - const { element, inputRef, prefixComponent, postfixComponent, className, onValidate, children, + const { element, componentClass, inputRef, prefixComponent, postfixComponent, className, onValidate, children, tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus, usePlaceholderAsHint, forceTooltipVisible, ...inputProps } = this.props; From e34a977b68c9ab67e7b83856b5799b0412b07c19 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 25 Jan 2022 00:27:15 -0600 Subject: [PATCH 41/47] Add explicit member access See https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r791164383 --- src/components/views/messages/DateSeparator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 8efd48b36e4..c12dea88986 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -62,7 +62,7 @@ interface IState { @replaceableComponent("views.messages.DateSeparator") export default class DateSeparator extends React.Component { - settingWatcherRef = null; + private settingWatcherRef = null; constructor(props, context) { super(props, context); From 7ed9549d582595eba604b78c7a022fcf5b0249b5 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 25 Jan 2022 00:35:10 -0600 Subject: [PATCH 42/47] Remove unused styles now that other selects are less leaky See https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r791156711 We also added `:not(.mx_AccessibleButton_hasKind)` to the `res/css/views/context_menus/_IconizedContextMenu.scss` selectors so the style that was overriding this before is no longer interferring. --- res/css/views/elements/_Field.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 1083a324fe2..a97e7ee949e 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -127,7 +127,6 @@ limitations under the License. transform 0.25s ease-out 0s, background-color 0.25s ease-out 0s; font-size: $font-10px; - line-height: normal; transform: translateY(-13px); padding: 0 2px; background-color: $background; From fc789bfa37cf615b6b5fdd70e3025312d8d9b230 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 25 Jan 2022 01:52:46 -0600 Subject: [PATCH 43/47] Use semantic form submit button which makes the enter interaction work by default See https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r791167358 and https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r791166896 --- src/components/views/messages/JumpToDatePicker.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 05c9acf1ac2..aa513c26f34 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from 'react'; +import React, { useState, FormEvent } from 'react'; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; @@ -61,18 +61,14 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { const [onFocus, isActive, ref] = useRovingTabIndex(); const onDateInputKeyDown = (e: React.KeyboardEvent): void => { - // Go and navigate if they submitted - if (e.key === "Enter") { - onDatePicked(dateValue); - return; - } - // When we see someone manually typing out a date, disable the auto // submit on change. setNavigateOnDatePickerSelection(false); }; - const onJumpToDateSubmit = (): void => { + const onJumpToDateSubmit = (ev: FormEvent): void => { + ev.preventDefault(); + onDatePicked(dateValue); }; @@ -96,6 +92,8 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { tabIndex={isActive ? 0 : -1} /> Date: Tue, 25 Jan 2022 01:59:09 -0600 Subject: [PATCH 44/47] Fix lint --- src/hooks/useCombinedRefs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useCombinedRefs.ts b/src/hooks/useCombinedRefs.ts index 2d1f71620e8..61757c7ca70 100644 --- a/src/hooks/useCombinedRefs.ts +++ b/src/hooks/useCombinedRefs.ts @@ -33,4 +33,4 @@ export const useCombinedRefs = (...refs) => { }, [refs]); return targetRef; -} +}; From 9a6befabd2b96d97e222c46c0b99f18a56f61a9f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 25 Jan 2022 16:00:15 -0600 Subject: [PATCH 45/47] Rename input to be self descriptive Fux https://github.com/matrix-org/matrix-react-sdk/pull/7339#discussion_r791161813 --- ...ustomInput.tsx => NativeOnChangeInput.tsx} | 19 +++++++++++++++---- .../views/messages/JumpToDatePicker.tsx | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) rename src/components/views/elements/{CustomInput.tsx => NativeOnChangeInput.tsx} (73%) diff --git a/src/components/views/elements/CustomInput.tsx b/src/components/views/elements/NativeOnChangeInput.tsx similarity index 73% rename from src/components/views/elements/CustomInput.tsx rename to src/components/views/elements/NativeOnChangeInput.tsx index b8d46184562..a7f62f367cd 100644 --- a/src/components/views/elements/CustomInput.tsx +++ b/src/components/views/elements/NativeOnChangeInput.tsx @@ -25,7 +25,12 @@ interface IProps extends Omit, 'onCh /** * This component restores the native 'onChange' and 'onInput' behavior of -* JavaScript. via https://stackoverflow.com/a/62383569/796832 and +* JavaScript which have important differences for certain types. This is +* necessary because in React, the `onChange` handler behaves like the native +* `oninput` handler and there is no way to tell the difference between an +* `input` vs `change` event. +* +* via https://stackoverflow.com/a/62383569/796832 and * https://github.com/facebook/react/issues/9657#issuecomment-643970199 * * See: @@ -34,9 +39,15 @@ interface IProps extends Omit, 'onCh * - 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). +* Examples: +* +* We use this for the date picker so we can distinguish from +* a final date picker selection (onChange) vs navigating the months in the date +* picker (onInput). +* +* This is also potentially useful for because the native +* events behave in such a way that moving the slider around triggers an onInput +* event and releasing it triggers onChange. */ const CustomInput: React.FC = React.forwardRef((props: IProps, ref) => { const registerCallbacks = (input: HTMLInputElement | null) => { diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index aa513c26f34..040b670c816 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -18,7 +18,7 @@ import React, { useState, FormEvent } from 'react'; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; -import CustomInput from "../elements/CustomInput"; +import NativeOnChangeInput from "../elements/NativeOnChangeInput"; import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; interface IProps { @@ -79,7 +79,7 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { > Jump to date Date: Tue, 25 Jan 2022 16:16:19 -0600 Subject: [PATCH 46/47] Fix some naming --- src/components/views/elements/Field.tsx | 4 ++-- src/components/views/elements/NativeOnChangeInput.tsx | 4 ++-- src/components/views/messages/DateSeparator.tsx | 4 ++-- src/components/views/messages/JumpToDatePicker.tsx | 4 ++-- src/hooks/useCombinedRefs.ts | 2 ++ 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index fc7b3b84c7c..578ecb3333b 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -107,7 +107,7 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes { +export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes { // The ref pass through to the input inputRef?: RefObject; element: "input"; @@ -117,7 +117,7 @@ export interface ICustomInputProps extends IProps, InputHTMLAttributes, 'onCh * events behave in such a way that moving the slider around triggers an onInput * event and releasing it triggers onChange. */ -const CustomInput: React.FC = React.forwardRef((props: IProps, ref) => { +const NativeOnChangeInput: React.FC = React.forwardRef((props: IProps, ref) => { const registerCallbacks = (input: HTMLInputElement | null) => { if (input) { input.onchange = props.onChange; @@ -66,4 +66,4 @@ const CustomInput: React.FC = React.forwardRef((props: IProps, ref) => { />; }); -export default CustomInput; +export default NativeOnChangeInput; diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index c12dea88986..76040c1ad75 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -188,7 +188,7 @@ export default class DateSeparator extends React.Component { this.closeMenu(); }; - private renderNotificationsMenu(): React.ReactElement { + private renderJumpToDateMenu(): React.ReactElement { let contextMenu: JSX.Element; if (this.state.contextMenuPosition) { contextMenu = { let dateHeaderContent; if (this.state.jumpToDateEnabled) { - dateHeaderContent = this.renderNotificationsMenu(); + dateHeaderContent = this.renderJumpToDateMenu(); } else { dateHeaderContent = ; } diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 040b670c816..1af8fff8d49 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -39,13 +39,13 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { // starts manually typing in the input instead of picking. const [navigateOnDatePickerSelection, setNavigateOnDatePickerSelection] = useState(true); - // Since we're using CustomInput with native JavaScript behavior, this + // Since we're using NativeOnChangeInput with native JavaScript behavior, this // tracks the date value changes as they come in. const onDateValueInput = (e: React.ChangeEvent): void => { setDateValue(e.target.value); }; - // Since we're using CustomInput with native JavaScript behavior, the change + // Since we're using NativeOnChangeInput 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. diff --git a/src/hooks/useCombinedRefs.ts b/src/hooks/useCombinedRefs.ts index 61757c7ca70..0b1e2a6c0c7 100644 --- a/src/hooks/useCombinedRefs.ts +++ b/src/hooks/useCombinedRefs.ts @@ -16,6 +16,8 @@ limitations under the License. import { useRef, useEffect } from 'react'; +// Takes in multiple React refs and combines them to reference the same target/element +// // via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd export const useCombinedRefs = (...refs) => { const targetRef = useRef(); From 0a79ddb3f16dfb54e1240547ffc2c459acdcaddc Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 26 Jan 2022 23:56:54 -0600 Subject: [PATCH 47/47] Fix roomId missing after develop merge --- src/components/structures/MessagePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 5724b408dfa..3cb67cc74e4 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -1448,7 +1448,7 @@ class HiddenEventGrouper extends BaseGrouper { if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { const ts = this.events[0].getTs(); ret.push( -
  • , +
  • , ); }