From 85c247eaf04f84260fdc2f74b38f057522bda6d2 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 9 Apr 2024 10:15:11 +1000 Subject: [PATCH] fix: focustrap when no buttons are mounted right away --- ts/components/SessionFocusTrap.tsx | 27 +++++++++++++++++++ ts/components/SessionWrapperModal.tsx | 8 +++--- .../ConversationHeaderSelectionOverlay.tsx | 1 + .../leftpane/MessageRequestsBanner.tsx | 4 +-- .../ConversationListItem.tsx | 2 +- ts/themes/SessionTheme.tsx | 8 +++--- 6 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 ts/components/SessionFocusTrap.tsx diff --git a/ts/components/SessionFocusTrap.tsx b/ts/components/SessionFocusTrap.tsx new file mode 100644 index 000000000..95c254565 --- /dev/null +++ b/ts/components/SessionFocusTrap.tsx @@ -0,0 +1,27 @@ +import FocusTrap from 'focus-trap-react'; +import React from 'react'; +import styled from 'styled-components'; + +const DefaultFocusButton = styled.button` + opacity: 0; + width: 0; + height: 0; +`; + +/** + * The FocusTrap always require at least one element to be tabbable. + * On some dialogs, we don't have any until the content is loaded, and depending on what is loaded, we might not have any tabbable elements in it. + * This component renders the children inside a FocusTrap and always adds an invisible button to make FocusTrap happy. + */ +export function SessionDialogFocusTrap(props: { children: React.ReactNode }) { + // FocusTrap needs a single child so props.children and the default button needs to be wrapped in a container. + // This might cause some styling issues, but I didn't find any with our current dialogs + return ( + +
+ + {props.children} +
+
+ ); +} diff --git a/ts/components/SessionWrapperModal.tsx b/ts/components/SessionWrapperModal.tsx index 42753784e..9fbe21a75 100644 --- a/ts/components/SessionWrapperModal.tsx +++ b/ts/components/SessionWrapperModal.tsx @@ -1,11 +1,11 @@ import classNames from 'classnames'; -import FocusTrap from 'focus-trap-react'; import React, { useRef } from 'react'; import useKey from 'react-use/lib/useKey'; import { SessionIconButton } from './icon'; import { SessionButton, SessionButtonColor, SessionButtonType } from './basic/SessionButton'; +import { SessionDialogFocusTrap } from './SessionFocusTrap'; export type SessionWrapperModalType = { title?: string; @@ -17,7 +17,7 @@ export type SessionWrapperModalType = { cancelText?: string; showExitIcon?: boolean; headerIconButtons?: Array; - children: any; + children: React.ReactNode; headerReverse?: boolean; additionalClassName?: string; }; @@ -64,7 +64,7 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => { }; return ( - +
{
-
+ ); }; diff --git a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx index 663081eae..c567969c3 100644 --- a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx +++ b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx @@ -1,5 +1,6 @@ import FocusTrap from 'focus-trap-react'; import React from 'react'; + import { useDispatch, useSelector } from 'react-redux'; import useKey from 'react-use/lib/useKey'; diff --git a/ts/components/leftpane/MessageRequestsBanner.tsx b/ts/components/leftpane/MessageRequestsBanner.tsx index ccfae4bd8..8f06487f5 100644 --- a/ts/components/leftpane/MessageRequestsBanner.tsx +++ b/ts/components/leftpane/MessageRequestsBanner.tsx @@ -4,10 +4,10 @@ import { createPortal } from 'react-dom'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { getUnreadConversationRequests } from '../../state/selectors/conversations'; +import { isSearching } from '../../state/selectors/search'; import { getHideMessageRequestBanner } from '../../state/selectors/userConfig'; import { SessionIcon, SessionIconSize, SessionIconType } from '../icon'; import { MessageRequestBannerContextMenu } from '../menu/MessageRequestBannerContextMenu'; -import { isSearching } from '../../state/selectors/search'; const StyledMessageRequestBanner = styled.div` height: 64px; @@ -137,6 +137,6 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { ); }; -const Portal = ({ children }: { children: any }) => { +const Portal = ({ children }: { children: React.ReactNode }) => { return createPortal(children, document.querySelector('.inbox.index') as Element); }; diff --git a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx index d1ce3e5f7..a15090ceb 100644 --- a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx +++ b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx @@ -34,7 +34,7 @@ type PropsHousekeeping = { type Props = { conversationId: string } & PropsHousekeeping; -const Portal = ({ children }: { children: any }) => { +const Portal = ({ children }: { children: React.ReactNode }) => { return createPortal(children, document.querySelector('.inbox.index') as Element); }; diff --git a/ts/themes/SessionTheme.tsx b/ts/themes/SessionTheme.tsx index 3337df271..01231796b 100644 --- a/ts/themes/SessionTheme.tsx +++ b/ts/themes/SessionTheme.tsx @@ -1,10 +1,10 @@ import { ipcRenderer } from 'electron'; import React from 'react'; import { createGlobalStyle } from 'styled-components'; -import { switchThemeTo } from './switchTheme'; -import { classicDark } from './classicDark'; import { getOppositeTheme, isThemeMismatched } from '../util/theme'; -import { declareCSSVariables, THEME_GLOBALS } from './globals'; +import { classicDark } from './classicDark'; +import { THEME_GLOBALS, declareCSSVariables } from './globals'; +import { switchThemeTo } from './switchTheme'; // Defaults to Classic Dark theme const SessionGlobalStyles = createGlobalStyle` @@ -14,7 +14,7 @@ const SessionGlobalStyles = createGlobalStyle` }; `; -export const SessionTheme = ({ children }: { children: any }) => ( +export const SessionTheme = ({ children }: { children: React.ReactNode }) => ( <> {children}