Skip to content

Commit fe8d0b0

Browse files
committed
Merge remote-tracking branch 'upstream/delete-msg-options-updated' into delete-msg-options-updated
2 parents 83e8f15 + 3cd2183 commit fe8d0b0

File tree

52 files changed

+1197
-943
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1197
-943
lines changed

ts/components/OutgoingLightBox.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export const OutgoingLightBox = (props: NonNullable<OutgoingLightBoxOptions>) =>
106106

107107
return (
108108
<SessionFocusTrap
109+
focusTrapId="OutgoingLightBox"
109110
initialFocus={() => ref.current}
110111
allowOutsideClick={true}
111112
returnFocusOnDeactivate={false}

ts/components/SessionFocusTrap.tsx

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,102 @@
1-
import { FocusTrap } from 'focus-trap-react';
2-
import { ReactNode } from 'react';
1+
import { FocusTrap, type FocusTrapProps } from 'focus-trap-react';
2+
import { type ReactNode, useEffect, useState } from 'react';
33
import type { CSSProperties } from 'styled-components';
4+
import { windowErrorFilters } from '../util/logger/renderer_process_logging';
5+
import { getFeatureFlagMemo } from '../state/ducks/types/releasedFeaturesReduxTypes';
6+
7+
const focusTrapErrorSource = 'focus-trap';
8+
9+
type SessionFocusTrapProps = FocusTrapProps['focusTrapOptions'] & {
10+
/** id used for debugging */
11+
focusTrapId: string;
12+
children: ReactNode;
13+
active?: boolean;
14+
containerDivStyle?: CSSProperties;
15+
/** Suppress errors thrown from inside the focus trap, preventing logging or global error emission */
16+
suppressErrors?: boolean;
17+
/** Allows the focus trap to exist without detectable tabbable elements. This is required if the children
18+
* are within a Shadow DOM. Internally sets suppressErrors to true. */
19+
allowNoTabbableNodes?: boolean;
20+
};
421

5-
/**
6-
* Focus trap which activates on mount.
7-
*/
822
export function SessionFocusTrap({
23+
focusTrapId,
924
children,
25+
active = true,
1026
allowOutsideClick = true,
11-
returnFocusOnDeactivate,
12-
initialFocus,
1327
containerDivStyle,
14-
}: {
15-
children: ReactNode;
16-
allowOutsideClick?: boolean;
17-
returnFocusOnDeactivate?: boolean;
18-
initialFocus: () => HTMLElement | null;
19-
containerDivStyle?: CSSProperties;
20-
}) {
28+
suppressErrors,
29+
allowNoTabbableNodes,
30+
onActivate,
31+
onPostActivate,
32+
onDeactivate,
33+
onPostDeactivate,
34+
...rest
35+
}: SessionFocusTrapProps) {
36+
const debugFocusTrap = getFeatureFlagMemo('debugFocusTrap');
37+
const defaultTabIndex = allowNoTabbableNodes ? 0 : -1;
38+
const _suppressErrors = suppressErrors || allowNoTabbableNodes;
39+
/**
40+
* NOTE: the tab index tricks the focus trap into thinking it has
41+
* tabbable children by setting a tab index on the empty div child. When
42+
* the trap activates it will see the div in the tab list and render without
43+
* error, then remove that div from the tab index list. Then when the trap
44+
* deactivates the state is reset.
45+
*/
46+
const [tabIndex, setTabIndex] = useState<0 | 1 | -1>(defaultTabIndex);
47+
48+
const _onActivate = () => {
49+
if (debugFocusTrap) {
50+
window.log.debug(`[SessionFocusTrap] onActivate - ${focusTrapId}`);
51+
}
52+
onActivate?.();
53+
};
54+
55+
const _onPostActivate = () => {
56+
if (allowNoTabbableNodes) {
57+
setTabIndex(-1);
58+
}
59+
onPostActivate?.();
60+
};
61+
62+
const _onDeactivate = () => {
63+
if (debugFocusTrap) {
64+
window.log.debug(`[SessionFocusTrap] onDeactivate - ${focusTrapId}`);
65+
}
66+
if (allowNoTabbableNodes) {
67+
setTabIndex(defaultTabIndex);
68+
}
69+
onDeactivate?.();
70+
};
71+
72+
useEffect(() => {
73+
if (!active || !_suppressErrors) {
74+
return;
75+
}
76+
windowErrorFilters.add(focusTrapErrorSource);
77+
// eslint-disable-next-line consistent-return -- This return is the destructor
78+
return () => {
79+
windowErrorFilters.remove(focusTrapErrorSource);
80+
};
81+
}, [_suppressErrors, active]);
82+
2183
return (
2284
<FocusTrap
23-
active={true}
85+
active={active}
2486
focusTrapOptions={{
25-
initialFocus,
87+
...rest,
2688
allowOutsideClick,
27-
returnFocusOnDeactivate,
89+
onActivate: _onActivate,
90+
onPostActivate: _onPostActivate,
91+
onDeactivate: _onDeactivate,
92+
onPostDeactivate,
2893
}}
2994
>
30-
{/* Note: not too sure why, but without this div, the focus trap doesn't work */}
31-
<div style={containerDivStyle}>{children}</div>
95+
{/* Note: without this div, the focus trap doesn't work */}
96+
<div style={containerDivStyle}>
97+
{allowNoTabbableNodes ? <div tabIndex={tabIndex} /> : null}
98+
{children}
99+
</div>
32100
</FocusTrap>
33101
);
34102
}

ts/components/SessionSearchInput.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ export const SessionSearchInput = ({ searchType }: { searchType: SearchType }) =
133133
iconColor="var(--text-secondary-color)"
134134
iconSize={iconSize}
135135
unicode={LUCIDE_ICONS_UNICODE.X}
136-
tabIndex={0}
136+
// NOTE: we don't want this clear button in the tab index list
137+
// as the Escape key already does the clear action for keyboard
138+
// users and we want the next tab after the search input to
139+
// be the first search result
140+
tabIndex={-1}
137141
onClick={() => {
138142
setCurrentSearchTerm('');
139143
dispatch(searchActions.clearSearch());

ts/components/SessionTooltip.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,20 @@ export const getTriggerPositionFromId = (id: string): PopoverTriggerPosition =>
7373
};
7474

7575
// Returns null if the ref is null
76-
export const useTriggerPosition = (
77-
ref: RefObject<HTMLElement | null>
78-
): PopoverTriggerPosition | null => {
76+
export const getTriggerPosition = (ref: RefObject<HTMLElement | null>) => {
7977
if (!ref.current) {
8078
return null;
8179
}
8280
return getTriggerPositionFromBoundingClientRect(ref.current.getBoundingClientRect());
8381
};
8482

83+
// Returns null if the ref is null
84+
export const useTriggerPosition = (
85+
ref: RefObject<HTMLElement | null>
86+
): PopoverTriggerPosition | null => {
87+
return getTriggerPosition(ref);
88+
};
89+
8590
export const SessionTooltip = ({
8691
children,
8792
content,

ts/components/SessionWrapperModal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,11 @@ export const SessionWrapperModal = (props: SessionWrapperModalType & { onClose?:
452452
props.headerChildren && moveHeaderIntoScrollableBody ? props.headerChildren : null;
453453

454454
return (
455-
<SessionFocusTrap allowOutsideClick={allowOutsideClick} initialFocus={() => modalRef.current}>
455+
<SessionFocusTrap
456+
focusTrapId="SessionWrapperModal"
457+
allowOutsideClick={allowOutsideClick}
458+
initialFocus={() => modalRef.current}
459+
>
456460
<IsModalScrolledContext.Provider value={scrolled}>
457461
<ModalHasActionButtonContext.Provider value={!!buttonChildren}>
458462
<OnModalCloseContext.Provider value={onClose ?? null}>

ts/components/conversation/SessionEmojiPanel.tsx

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import Picker from '@emoji-mart/react';
2-
import { forwardRef } from 'react';
2+
import { type RefObject } from 'react';
33
import styled from 'styled-components';
4-
import clsx from 'clsx';
54

65
import { usePrimaryColor } from '../../state/selectors/primaryColor';
76
import { useIsDarkTheme, useTheme } from '../../state/theme/selectors/theme';
87
import { COLORS, THEMES, ThemeStateType, type ColorsType } from '../../themes/constants/colors';
98
import { FixedBaseEmoji } from '../../types/Reaction';
109
import { i18nEmojiData } from '../../util/emoji';
1110
import { hexColorToRGB } from '../../util/hexColorToRGB';
11+
import { SessionFocusTrap } from '../SessionFocusTrap';
1212

1313
export const StyledEmojiPanel = styled.div<{
1414
$isModal: boolean;
@@ -19,20 +19,11 @@ export const StyledEmojiPanel = styled.div<{
1919
}>`
2020
${props => (!props.$isModal ? 'padding: var(--margins-lg);' : '')}
2121
z-index: 5;
22-
opacity: 0;
23-
visibility: hidden;
24-
// this disables the slide-in animation when showing the emoji picker from a right click on a message
25-
/* transition: var(--default-duration); */
2622
2723
button:focus {
2824
outline: none;
2925
}
3026
31-
&.show {
32-
opacity: 1;
33-
visibility: visible;
34-
}
35-
3627
em-emoji-picker {
3728
${props => props.$panelBackgroundRGB && `background-color: rgb(${props.$panelBackgroundRGB})`};
3829
border: var(--default-borders);
@@ -75,21 +66,18 @@ export const StyledEmojiPanel = styled.div<{
7566
`;
7667

7768
type Props = {
69+
ref?: RefObject<HTMLDivElement | null>;
7870
onEmojiClicked: (emoji: FixedBaseEmoji) => void;
79-
show: boolean;
8071
isModal?: boolean;
8172
onClose?: () => void;
73+
show: boolean;
8274
};
8375

84-
const pickerProps = {
85-
title: '',
86-
showPreview: true,
87-
autoFocus: true,
88-
skinTonePosition: 'preview',
76+
export const SessionEmojiPanel = (props: Props) => {
77+
return props.show ? <EmojiPanel {...props} /> : null;
8978
};
9079

91-
export const SessionEmojiPanel = forwardRef<HTMLDivElement, Props>((props: Props, ref) => {
92-
const { onEmojiClicked, show, isModal = false, onClose } = props;
80+
const EmojiPanel = ({ ref, onEmojiClicked, isModal = false, onClose }: Props) => {
9381
const _primaryColor = usePrimaryColor();
9482
const theme = useTheme();
9583
const isDarkTheme = useIsDarkTheme();
@@ -125,22 +113,31 @@ export const SessionEmojiPanel = forwardRef<HTMLDivElement, Props>((props: Props
125113
);
126114

127115
return (
128-
<StyledEmojiPanel
129-
$isModal={isModal}
130-
$primaryColor={primaryColor}
131-
$theme={theme}
132-
$panelBackgroundRGB={panelBackgroundRGB}
133-
$panelTextRGB={panelTextRGB}
134-
className={clsx(show && 'show')}
135-
ref={ref}
116+
<SessionFocusTrap
117+
focusTrapId="SessionEmojiPanel"
118+
clickOutsideDeactivates={true}
119+
allowNoTabbableNodes={true}
120+
onDeactivate={onClose}
136121
>
137-
<Picker
138-
theme={isDarkTheme ? 'dark' : 'light'}
139-
i18n={i18nEmojiData}
140-
onEmojiSelect={onEmojiClicked}
141-
onClose={onClose}
142-
{...pickerProps}
143-
/>
144-
</StyledEmojiPanel>
122+
<StyledEmojiPanel
123+
$isModal={isModal}
124+
$primaryColor={primaryColor}
125+
$theme={theme}
126+
$panelBackgroundRGB={panelBackgroundRGB}
127+
$panelTextRGB={panelTextRGB}
128+
ref={ref}
129+
>
130+
<Picker
131+
theme={isDarkTheme ? 'dark' : 'light'}
132+
i18n={i18nEmojiData}
133+
onEmojiSelect={onEmojiClicked}
134+
onClose={onClose}
135+
title=""
136+
showPreview={true}
137+
skinTonePosition={'preview'}
138+
autoFocus={true}
139+
/>
140+
</StyledEmojiPanel>
141+
</SessionFocusTrap>
145142
);
146-
});
143+
};

ts/components/conversation/SessionEmojiPanelPopover.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,36 @@ const EMOJI_PANEL_WIDTH_PX = 354;
99
const EMOJI_PANEL_HEIGHT_PX = 435;
1010

1111
export function SessionEmojiPanelPopover({
12-
triggerPos,
1312
emojiPanelRef,
14-
onEmojiClicked,
13+
triggerPosition,
14+
onEmojiClick,
1515
open,
1616
onClose,
1717
}: {
18-
triggerPos: PopoverTriggerPosition | null;
18+
emojiPanelRef?: RefObject<HTMLDivElement | null>;
19+
triggerPosition: PopoverTriggerPosition | null;
1920
open: boolean;
20-
emojiPanelRef: RefObject<HTMLDivElement | null>;
21-
onEmojiClicked: (emoji: FixedBaseEmoji) => void;
21+
onEmojiClick: (emoji: FixedBaseEmoji) => void;
2222
onClose: () => void;
2323
}) {
24-
const _open = open && !!triggerPos;
24+
const _open = open && !!triggerPosition;
2525
return (
2626
<SessionPopoverContent
27-
triggerPosition={triggerPos}
27+
triggerPosition={triggerPosition}
2828
open={_open}
2929
isTooltip={false}
3030
verticalPosition="bottom"
3131
horizontalPosition="center"
3232
fallbackContentWidth={EMOJI_PANEL_WIDTH_PX}
3333
fallbackContentHeight={EMOJI_PANEL_HEIGHT_PX}
3434
>
35-
{_open ? (
36-
<SessionEmojiPanel
37-
ref={emojiPanelRef}
38-
show={true}
39-
onEmojiClicked={onEmojiClicked}
40-
onClose={onClose}
41-
isModal={true}
42-
/>
43-
) : null}
35+
<SessionEmojiPanel
36+
ref={emojiPanelRef}
37+
onEmojiClicked={onEmojiClick}
38+
onClose={onClose}
39+
isModal={true}
40+
show={_open}
41+
/>
4442
</SessionPopoverContent>
4543
);
4644
}

0 commit comments

Comments
 (0)