From f632e2124f0f4dd0f2ed4432c6ba7f780e0b8ed9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Dec 2023 08:50:42 +0000 Subject: [PATCH] Switch AccessibleButton and derivatives to using `forwardRef` (#12054) * Prevent Cypress typechecking react-sdk components without strict mode This prevented us from switching to `forwardRef` in a bunch of places due to it behaving different with & without strict mode. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update global.d.ts * Switch AccessibleButton and derivatives to using `forwardRef` Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add missing ref={ref} Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Ensure RefObjects are used consistently Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Re-add WysiwygAutocomplete displayname Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix forwardRef types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add comments Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unused export Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Readd comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate types & comments Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Apply suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Add comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- .../context_menu/ContextMenuButton.tsx | 17 +++----- .../context_menu/ContextMenuTooltipButton.tsx | 16 +++---- .../roving/RovingAccessibleButton.tsx | 2 +- .../roving/RovingAccessibleTooltipButton.tsx | 2 +- .../structures/GenericDropdownMenu.tsx | 2 +- src/components/structures/SpaceHierarchy.tsx | 5 ++- src/components/structures/SpaceRoomView.tsx | 2 +- src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/UserMenu.tsx | 2 +- .../views/audio_messages/PlayPauseButton.tsx | 5 ++- .../auth/InteractiveAuthEntryComponents.tsx | 2 +- .../views/context_menus/KebabContextMenu.tsx | 2 +- .../context_menus/ThreadListContextMenu.tsx | 2 +- .../views/dialogs/spotlight/Option.tsx | 2 +- .../views/dialogs/spotlight/TooltipOption.tsx | 2 +- .../views/elements/AccessibleButton.tsx | 43 +++++++++++-------- .../elements/AccessibleTooltipButton.tsx | 21 ++++----- src/components/views/elements/AppTile.tsx | 2 +- src/components/views/elements/Dropdown.tsx | 2 +- src/components/views/elements/ImageView.tsx | 2 +- .../views/elements/PollCreateDialog.tsx | 2 +- src/components/views/messages/CallEvent.tsx | 2 +- .../views/messages/MessageActionBar.tsx | 4 +- .../views/messages/ReactionsRow.tsx | 2 +- .../views/right_panel/WidgetCard.tsx | 2 +- .../views/rooms/CollapsibleButton.tsx | 18 +++++--- .../views/rooms/LegacyRoomHeader.tsx | 4 +- .../views/rooms/ReadReceiptGroup.tsx | 2 +- .../views/rooms/RoomBreadcrumbs.tsx | 2 +- src/components/views/rooms/RoomList.tsx | 4 +- src/components/views/rooms/RoomListHeader.tsx | 4 +- src/components/views/rooms/RoomSublist.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 2 +- .../views/spaces/QuickSettingsButton.tsx | 2 +- .../views/spaces/SpaceTreeLevel.tsx | 2 +- src/components/views/voip/CallView.tsx | 2 +- .../LegacyCallView/LegacyCallViewButtons.tsx | 6 +-- 37 files changed, 102 insertions(+), 95 deletions(-) diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 396af5748db..6ef6afef379 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps } from "react"; +import React, { ComponentProps, forwardRef, Ref } from "react"; import AccessibleButton from "../../components/views/elements/AccessibleButton"; @@ -27,14 +27,10 @@ type Props = ComponentProps -export const ContextMenuButton = ({ - label, - isExpanded, - children, - onClick, - onContextMenu, - ...props -}: Props): JSX.Element => { +export const ContextMenuButton = forwardRef(function ( + { label, isExpanded, children, onClick, onContextMenu, ...props }: Props, + ref: Ref, +) { return ( ({ aria-label={label} aria-haspopup={true} aria-expanded={isExpanded} + ref={ref} > {children} ); -}; +}); diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index 0b71b616467..34986627601 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps } from "react"; +import React, { ComponentProps, forwardRef, Ref } from "react"; import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; @@ -26,13 +26,10 @@ type Props = ComponentProps -export const ContextMenuTooltipButton = ({ - isExpanded, - children, - onClick, - onContextMenu, - ...props -}: Props): JSX.Element => { +export const ContextMenuTooltipButton = forwardRef(function ( + { isExpanded, children, onClick, onContextMenu, ...props }: Props, + ref: Ref, +) { return ( ( aria-haspopup={true} aria-expanded={isExpanded} forceHide={isExpanded} + ref={ref} > {children} ); -}; +}); diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index 849ef2db428..56c9052714a 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -48,7 +48,7 @@ export const RovingAccessibleButton = ({ if (focusOnMouseOver) onFocusInternal(); onMouseOver?.(event); }} - inputRef={ref} + ref={ref} tabIndex={isActive ? 0 : -1} /> ); diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index 642329e69b7..5607089c6e2 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -41,7 +41,7 @@ export const RovingAccessibleTooltipButton = ); diff --git a/src/components/structures/GenericDropdownMenu.tsx b/src/components/structures/GenericDropdownMenu.tsx index 0a38db3dbe4..e0fd3b7f9b6 100644 --- a/src/components/structures/GenericDropdownMenu.tsx +++ b/src/components/structures/GenericDropdownMenu.tsx @@ -195,7 +195,7 @@ export function GenericDropdownMenu({ <> { openMenu(); diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index aa57114e5ac..feeacb45818 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React, { + ComponentProps, Dispatch, KeyboardEvent, KeyboardEventHandler, @@ -349,7 +350,7 @@ const Tile: React.FC = ({ })} onClick={hasPermissions && onToggleClick ? onToggleClick : onPreviewClick} onKeyDown={onKeyDown} - inputRef={ref} + ref={ref} onFocus={onFocus} tabIndex={isActive ? 0 : -1} > @@ -664,7 +665,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set const disabled = !selectedRelations.length || removing || saving; let Button: React.ComponentType> = AccessibleButton; - let props = {}; + let props: Partial> = {}; if (!selectedRelations.length) { Button = AccessibleTooltipButton; props = { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 031d44d7f10..dc79a25489a 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -196,7 +196,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { <> { openMenu(); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 2d82a5c412e..1ed20add205 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -453,7 +453,7 @@ export default class UserMenu extends React.Component { , "title" | "onClick" | "disabled" | "element"> & { +type Props = Omit< + ComponentProps, + "title" | "onClick" | "disabled" | "element" | "ref" +> & { // Playback instance to manipulate. Cannot change during the component lifecycle. playback: Playback; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 008f92af92b..0a7ed19b2ab 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -974,7 +974,7 @@ export class FallbackAuthEntry extends React.Component { } return (
- + {_t("auth|uia|fallback_button")} {errorSection} diff --git a/src/components/views/context_menus/KebabContextMenu.tsx b/src/components/views/context_menus/KebabContextMenu.tsx index b81c6aef6f4..7a6b09668dd 100644 --- a/src/components/views/context_menus/KebabContextMenu.tsx +++ b/src/components/views/context_menus/KebabContextMenu.tsx @@ -39,7 +39,7 @@ export const KebabContextMenu: React.FC = ({ options, tit return ( <> - + {menuDisplayed && ( diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index 0fb3831b237..76abc23740e 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -94,7 +94,7 @@ const ThreadListContextMenu: React.FC = ({ onClick={openMenu} title={_t("right_panel|thread_list|context_menu_label")} isExpanded={menuDisplayed} - inputRef={button} + ref={button} data-testid="threadlist-dropdown-button" /> {menuDisplayed && ( diff --git a/src/components/views/dialogs/spotlight/Option.tsx b/src/components/views/dialogs/spotlight/Option.tsx index a1dc41a8524..c7d504aa0b4 100644 --- a/src/components/views/dialogs/spotlight/Option.tsx +++ b/src/components/views/dialogs/spotlight/Option.tsx @@ -35,7 +35,7 @@ export const Option: React.FC = ({ inputRef, children, endAdornment {...props} className={classNames(className, "mx_SpotlightDialog_option")} onFocus={onFocus} - inputRef={ref} + ref={ref} tabIndex={-1} aria-selected={isActive} role="option" diff --git a/src/components/views/dialogs/spotlight/TooltipOption.tsx b/src/components/views/dialogs/spotlight/TooltipOption.tsx index ef4c142f10f..2233e762d46 100644 --- a/src/components/views/dialogs/spotlight/TooltipOption.tsx +++ b/src/components/views/dialogs/spotlight/TooltipOption.tsx @@ -32,7 +32,7 @@ export const TooltipOption: React.FC = ({ inputRef, classNam {...props} className={classNames(className, "mx_SpotlightDialog_option")} onFocus={onFocus} - inputRef={ref} + ref={ref} tabIndex={-1} aria-selected={isActive} role="option" diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index fc54e8517a4..1eb03792b8d 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -14,7 +14,7 @@ limitations under the License. */ -import React, { HTMLAttributes, InputHTMLAttributes } from "react"; +import React, { forwardRef, FunctionComponent, HTMLAttributes, InputHTMLAttributes, Ref } from "react"; import classnames from "classnames"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; @@ -66,7 +66,6 @@ type DynamicElementProps = Partial< * Extends props accepted by the underlying element specified using the `element` prop. */ type Props = DynamicHtmlElementProps & { - inputRef?: React.Ref; /** * The base element type. "div" by default. */ @@ -101,22 +100,26 @@ interface RenderedElementProps extends React.InputHTMLAttributes { * as a button. Identifies the element as a button, setting proper tab * indexing and keyboard activation behavior. * + * If a ref is passed, it will be forwarded to the rendered element as specified using the `element` prop. + * * @param {Object} props react element properties * @returns {Object} rendered react */ -export default function AccessibleButton({ - element = "div" as T, - onClick, - children, - kind, - disabled, - inputRef, - className, - onKeyDown, - onKeyUp, - triggerOnMouseDown, - ...restProps -}: Props): JSX.Element { +const AccessibleButton = forwardRef(function ( + { + element = "div" as T, + onClick, + children, + kind, + disabled, + className, + onKeyDown, + onKeyUp, + triggerOnMouseDown, + ...restProps + }: Props, + ref: Ref, +): JSX.Element { const newProps: RenderedElementProps = restProps; if (disabled) { newProps["aria-disabled"] = true; @@ -170,7 +173,7 @@ export default function AccessibleButton( } // Pass through the ref - used for keyboard shortcut access to some buttons - newProps.ref = inputRef; + newProps.ref = ref; newProps.className = classnames("mx_AccessibleButton", className, { mx_AccessibleButton_hasKind: kind, @@ -180,11 +183,13 @@ export default function AccessibleButton( // React.createElement expects InputHTMLAttributes return React.createElement(element, newProps, children); -} +}); -AccessibleButton.defaultProps = { +// Type assertion required due to forwardRef type workaround in react.d.ts +(AccessibleButton as FunctionComponent).defaultProps = { role: "button", tabIndex: 0, }; +(AccessibleButton as FunctionComponent).displayName = "AccessibleButton"; -AccessibleButton.displayName = "AccessibleButton"; +export default AccessibleButton; diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index c561b1b8b58..0af5cc9625f 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { SyntheticEvent, FocusEvent, useEffect, useState } from "react"; +import React, { SyntheticEvent, FocusEvent, forwardRef, useEffect, Ref, useState, ComponentProps } from "react"; import AccessibleButton from "./AccessibleButton"; import Tooltip, { Alignment } from "./Tooltip"; @@ -25,7 +25,7 @@ import Tooltip, { Alignment } from "./Tooltip"; * * Extends that of {@link AccessibleButton}. */ -type Props = React.ComponentProps> & { +type Props = ComponentProps> & { /** * Title to show in the tooltip and use as aria-label */ @@ -60,16 +60,10 @@ type Props = React.ComponentProps({ - title, - tooltip, - children, - forceHide, - alignment, - onHideTooltip, - tooltipClassName, - ...props -}: Props): JSX.Element { +const AccessibleTooltipButton = forwardRef(function ( + { title, tooltip, children, forceHide, alignment, onHideTooltip, tooltipClassName, ...props }: Props, + ref: Ref, +) { const [hover, setHover] = useState(false); useEffect(() => { @@ -108,12 +102,13 @@ function AccessibleTooltipButton({ onFocus={onFocus} onBlur={hideTooltip} aria-label={title || props["aria-label"]} + ref={ref} > {children} {props.label} {(tooltip || title) && tip} ); -} +}); export default AccessibleTooltipButton; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 642e2bd4769..56a1969c1b0 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -789,7 +789,7 @@ export default class AppTile extends React.Component { className="mx_AppTileMenuBar_widgets_button" label={_t("common|options")} isExpanded={this.state.menuDisplayed} - inputRef={this.contextMenuButton} + ref={this.contextMenuButton} onClick={this.onContextMenuClick} > diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 0a5786a1cb2..d017ea8641a 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -392,7 +392,7 @@ export default class Dropdown extends React.Component { aria-haspopup="listbox" aria-expanded={this.state.expanded} disabled={this.props.disabled} - inputRef={this.buttonRef} + ref={this.buttonRef} aria-label={this.props.label} aria-describedby={`${this.props.id}_value`} aria-owns={`${this.props.id}_input`} diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index da05239746d..6f5815e95a1 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -506,7 +506,7 @@ export default class ImageView extends React.Component { className="mx_ImageView_button mx_ImageView_button_more" title={_t("common|options")} onClick={this.onOpenContextMenu} - inputRef={this.contextMenuButton} + ref={this.contextMenuButton} isExpanded={this.state.contextMenuDisplayed} /> ); diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 6451bdcbb8c..5049a3b0162 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -248,7 +248,7 @@ export default class PollCreateDialog extends ScrollableBaseModal= MAX_OPTIONS} kind="secondary" className="mx_PollCreateDialog_addOption" - inputRef={this.addOptionRef} + ref={this.addOptionRef} > {_t("poll|options_add_button")} diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 5718c7a4126..dd2e29f51c8 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -28,7 +28,7 @@ import { import defaultDispatcher from "../../../dispatcher/dispatcher"; import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; -import { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton"; +import type { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton"; import MemberAvatar from "../avatars/MemberAvatar"; import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary"; import FacePile from "../elements/FacePile"; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 60eee55c2d0..9cf35922089 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -126,7 +126,7 @@ const OptionsButton: React.FC = ({ onClick={onOptionsClick} onContextMenu={onOptionsClick} isExpanded={menuDisplayed} - inputRef={button} + ref={button} onFocus={onFocus} tabIndex={isActive ? 0 : -1} > @@ -183,7 +183,7 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC onClick={onClick} onContextMenu={onClick} isExpanded={menuDisplayed} - inputRef={button} + ref={button} onFocus={onFocus} tabIndex={isActive ? 0 : -1} > diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index 3aeee9e0ff1..e57326edd73 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -61,7 +61,7 @@ const ReactButton: React.FC = ({ mxEvent, reactions }) => { openMenu(); }} isExpanded={menuDisplayed} - inputRef={button} + ref={button} /> {contextMenu} diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index cbcc961b972..ca7cdebb21f 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -78,7 +78,7 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { { +interface Props extends Omit, "element"> { + inputRef?: Ref; title: string; iconClassName: string; } -export const CollapsibleButton: React.FC = ({ +export const CollapsibleButton: React.FC = ({ title, children, className, iconClassName, + inputRef, ...props }) => { const inOverflowMenu = !!useContext(OverflowMenuContext); if (inOverflowMenu) { - return ; + return ; } return ( - + {children} ); diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx index cad8cb88149..6729ba118fe 100644 --- a/src/components/views/rooms/LegacyRoomHeader.tsx +++ b/src/components/views/rooms/LegacyRoomHeader.tsx @@ -234,7 +234,7 @@ const VideoCallButton: FC = ({ room, busy, setBusy, behavi return ( <> = ({ call }) => { return ( <> v title={room.name} tooltipClassName="mx_RoomBreadcrumbs_Tooltip" onFocus={onFocus} - inputRef={ref} + ref={ref} tabIndex={isActive ? 0 : -1} > = ({ tabIndex, dispatcher = default aria-label={_t("action|add_people")} title={_t("action|add_people")} isExpanded={menuDisplayed} - inputRef={handle} + ref={handle} /> {contextMenu} @@ -356,7 +356,7 @@ const UntaggedAuxButton: React.FC = ({ tabIndex }) => { aria-label={_t("room_list|add_room_label")} title={_t("room_list|add_room_label")} isExpanded={menuDisplayed} - inputRef={handle} + ref={handle} /> {contextMenu} diff --git a/src/components/views/rooms/RoomListHeader.tsx b/src/components/views/rooms/RoomListHeader.tsx index 8f32062fb73..4f8b4f8c324 100644 --- a/src/components/views/rooms/RoomListHeader.tsx +++ b/src/components/views/rooms/RoomListHeader.tsx @@ -389,7 +389,7 @@ const RoomListHeader: React.FC = ({ onVisibilityChange }) => { let contextMenuButton: JSX.Element =
{title}
; if (canShowMainMenu) { const commonProps = { - inputRef: mainMenuHandle, + ref: mainMenuHandle, onClick: openMainMenu, isExpanded: mainMenuDisplayed, className: "mx_RoomListHeader_contextMenuButton", @@ -418,7 +418,7 @@ const RoomListHeader: React.FC = ({ onVisibilityChange }) => { ) : null} {canShowPlusMenu && ( {