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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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 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 08/11] 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 09/11] 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 10/11] 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 c7b246a78d03286d9892ea96ca7e045b99e9d76a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Dec 2021 09:43:43 +0000 Subject: [PATCH 11/11] tidy up --- .../views/messages/MessageActionBar.tsx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 41adcdbaddf..87433fb4deb 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -67,10 +67,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) { @@ -93,7 +91,13 @@ const OptionsButton: React.FC = ({ { + openMenu(); + // when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks + // the element that is currently focused is skipped. So we want to call onFocus manually to keep the + // position in the page even when someone is clicking around. + onFocus(); + }} isExpanded={menuDisplayed} inputRef={ref} onFocus={onFocus} @@ -114,10 +118,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) { @@ -131,7 +133,13 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC { + openMenu(); + // when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks + // the element that is currently focused is skipped. So we want to call onFocus manually to keep the + // position in the page even when someone is clicking around. + onFocus(); + }} isExpanded={menuDisplayed} inputRef={ref} onFocus={onFocus} @@ -200,10 +208,7 @@ export default class MessageActionBar extends React.PureComponent { - if (!this.props.onFocusChange) { - return; - } - this.props.onFocusChange(focused); + this.props.onFocusChange?.(focused); }; private onReplyClick = (ev: React.MouseEvent): void => {