Skip to content

Commit b07ddfe

Browse files
Next iteration
1 parent 59fadfb commit b07ddfe

10 files changed

Lines changed: 305 additions & 248 deletions

File tree

src/components/Dialog/hooks/usePopoverPosition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function autoMiddlewareFor(p: PopperLikePlacement) {
2121
return autoPlacement({ alignment });
2222
}
2323

24-
type OffsetOpt =
24+
export type OffsetOpt =
2525
| number
2626
| { mainAxis?: number; crossAxis?: number; alignmentAxis?: number }
2727
| [crossAxis: number, mainAxis: number]; // keep your tuple compat

src/components/Dialog/service/DialogAnchor.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,54 @@ import React, { useEffect, useRef, useState } from 'react';
44
import { FocusScope } from '@react-aria/focus';
55
import { DialogPortalEntry } from './DialogPortal';
66
import { useDialog, useDialogIsOpen } from '../hooks';
7-
import { usePopoverPosition } from '../hooks/usePopoverPosition';
7+
import { type OffsetOpt, usePopoverPosition } from '../hooks/usePopoverPosition';
88
import type { PopperLikePlacement } from '../hooks';
9+
import type { Placement } from '@floating-ui/react';
910

1011
export interface DialogAnchorOptions {
1112
open: boolean;
1213
placement: PopperLikePlacement;
1314
referenceElement: HTMLElement | null;
1415
allowFlip?: boolean;
1516
updateKey?: unknown;
17+
updatePositionOnContentResize?: boolean;
18+
offset?: OffsetOpt;
1619
}
1720

1821
export function useDialogAnchor<T extends HTMLElement>({
1922
allowFlip,
23+
offset,
2024
open,
2125
placement,
2226
referenceElement,
2327
updateKey,
28+
updatePositionOnContentResize = false,
2429
}: DialogAnchorOptions) {
2530
const [popperElement, setPopperElement] = useState<T | null>(null);
26-
const { refs, strategy, update, x, y } = usePopoverPosition({
31+
// keeps track of the first "chosen" placement (after popperElement is set) to avoid popper "jumping" to a different placement when it updates and finds a better fit; resets when popperElement is unset (!open)
32+
const [stabilisedChosenPlacement, setStabilisedChosenPlacement] =
33+
useState<Placement | null>(null);
34+
35+
const {
36+
placement: chosenPlacement,
37+
refs,
38+
strategy,
39+
update,
40+
x,
41+
y,
42+
} = usePopoverPosition({
2743
allowFlip,
2844
freeze: true,
29-
placement,
45+
offset,
46+
placement: stabilisedChosenPlacement ?? placement,
3047
});
3148

49+
if (!stabilisedChosenPlacement && popperElement && placement !== chosenPlacement) {
50+
setStabilisedChosenPlacement(chosenPlacement);
51+
} else if (stabilisedChosenPlacement && !popperElement) {
52+
setStabilisedChosenPlacement(null);
53+
}
54+
3255
// Freeze reference when dialog opens so submenus (e.g. ContextMenu level 2+) stay aligned to the original anchor
3356
const frozenReferenceRef = useRef<HTMLElement | null>(null);
3457
if (open && referenceElement && !frozenReferenceRef.current) {
@@ -57,6 +80,18 @@ export function useDialogAnchor<T extends HTMLElement>({
5780
}
5881
}, [open, placement, popperElement, update, updateKey, effectiveReference]);
5982

83+
useEffect(() => {
84+
if (!popperElement || !updatePositionOnContentResize) return;
85+
86+
const resizeObserver = new ResizeObserver(update);
87+
88+
resizeObserver.observe(popperElement);
89+
90+
return () => {
91+
resizeObserver.disconnect();
92+
};
93+
}, [popperElement, update, updatePositionOnContentResize]);
94+
6095
if (popperElement && !open) {
6196
setPopperElement(null);
6297
}
@@ -85,22 +120,26 @@ export const DialogAnchor = ({
85120
dialogManagerId,
86121
focus = true,
87122
id,
123+
offset,
88124
placement = 'auto',
89125
referenceElement = null,
90126
tabIndex,
91127
trapFocus,
92128
updateKey,
129+
updatePositionOnContentResize,
93130
...restDivProps
94131
}: DialogAnchorProps) => {
95132
const dialog = useDialog({ dialogManagerId, id });
96133
const open = useDialogIsOpen(id, dialogManagerId);
97134

98135
const { setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
99136
allowFlip,
137+
offset,
100138
open,
101139
placement,
102140
referenceElement,
103141
updateKey,
142+
updatePositionOnContentResize,
104143
});
105144

106145
useEffect(() => {

src/components/Message/hooks/useReactionsFetcher.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useChatContext, useTranslationContext } from '../../../context';
2+
import { useStableCallback } from '../../../utils/useStableCallback';
23
import type {
34
LocalMessage,
45
ReactionResponse,
@@ -22,15 +23,15 @@ export function useReactionsFetcher(
2223
const { t } = useTranslationContext('useReactionFetcher');
2324
const { getErrorNotification, notify } = notifications;
2425

25-
return async (reactionType?: ReactionType, sort?: ReactionSort) => {
26+
return useStableCallback(async (reactionType?: ReactionType, sort?: ReactionSort) => {
2627
try {
2728
return await fetchMessageReactions(client, message.id, reactionType, sort);
2829
} catch (e) {
2930
const errorMessage = getErrorNotification?.(message);
3031
notify?.(errorMessage || t('Error fetching reactions'), 'error');
3132
throw e;
3233
}
33-
};
34+
});
3435
}
3536

3637
async function fetchMessageReactions(

src/components/Reactions/ReactionsList.tsx

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import React, { type ComponentPropsWithoutRef, useMemo, useState } from 'react';
1+
import React, {
2+
type ComponentPropsWithoutRef,
3+
type ComponentRef,
4+
useMemo,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import clsx from 'clsx';
39

4-
import type { ReactionsListModalProps } from './ReactionsListModal';
510
import { ReactionsListModal as DefaultReactionsListModal } from './ReactionsListModal';
611
import { useProcessReactions } from './hooks/useProcessReactions';
712
import type { MessageContextValue } from '../../context';
8-
import { useComponentContext, useTranslationContext } from '../../context';
13+
import {
14+
useComponentContext,
15+
useMessageContext,
16+
useTranslationContext,
17+
} from '../../context';
918

1019
import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks';
1120

@@ -16,6 +25,7 @@ import type {
1625
ReactionsComparator,
1726
ReactionType,
1827
} from './types';
28+
import { DialogAnchor, useDialogOnNearestManager } from '../Dialog';
1929

2030
export type ReactionsListProps = Partial<
2131
Pick<MessageContextValue, 'handleFetchReactions' | 'reactionDetailsSort'>
@@ -87,13 +97,20 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
8797
);
8898
const { t } = useTranslationContext('ReactionsList');
8999
const { ReactionsListModal = DefaultReactionsListModal } = useComponentContext();
100+
const { isMyMessage, message } = useMessageContext('ReactionsList');
101+
102+
const divRef = useRef<ComponentRef<'div'>>(null);
103+
const dialogId = `message-reactions-detail-${message.id}`;
104+
const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
90105

91106
const handleReactionButtonClick = (reactionType: ReactionType | null) => {
92107
if (totalReactionCount > MAX_MESSAGE_REACTIONS_TO_FETCH) {
93108
return;
94109
}
95110

96111
setSelectedReactionType(reactionType);
112+
113+
dialog.open();
97114
};
98115

99116
/**
@@ -127,13 +144,14 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
127144
[`str-chat__message-reactions--${visualStyle}`]:
128145
typeof visualStyle === 'string',
129146
})}
147+
ref={divRef}
130148
role='figure'
131149
>
132150
<FragmentOrButton
133151
buttonIf={visualStyle === 'clustered'}
134152
className='str-chat__message-reactions__list-button'
135153
onClick={() =>
136-
setSelectedReactionType(existingReactions[0]?.reactionType ?? null)
154+
handleReactionButtonClick(existingReactions[0]?.reactionType ?? null)
137155
}
138156
>
139157
<ul className='str-chat__message-reactions__list'>
@@ -149,12 +167,12 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
149167
className='str-chat__message-reactions__list-item-button'
150168
onClick={() => handleReactionButtonClick(reactionType)}
151169
>
152-
<span className='str-chat__message-reactions__item-icon'>
170+
<span className='str-chat__message-reactions__list-item-icon'>
153171
<EmojiComponent />
154172
</span>
155173
{visualStyle === 'segmented' && reactionCount > 1 && (
156174
<span
157-
className='str-chat__message-reactions__item-count'
175+
className='str-chat__message-reactions__list-item-count'
158176
data-testclass='message-reactions-item-count'
159177
>
160178
{reactionCount}
@@ -174,7 +192,9 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
174192
)
175193
}
176194
>
177-
+{totalReactionCount - cappedExistingReactions.reactionCountToDisplay}
195+
<span className='str-chat__message-reactions__overflow-count'>
196+
+{totalReactionCount - cappedExistingReactions.reactionCountToDisplay}
197+
</span>
178198
</button>
179199
</li>
180200
)}
@@ -186,19 +206,25 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
186206
)}
187207
</FragmentOrButton>
188208
</div>
189-
{selectedReactionType !== null && (
209+
210+
<DialogAnchor
211+
dialogManagerId={dialogManager?.id}
212+
id={dialogId}
213+
offset={8}
214+
placement={isMyMessage() ? 'bottom-end' : 'bottom-start'}
215+
referenceElement={divRef.current}
216+
trapFocus
217+
updatePositionOnContentResize
218+
>
190219
<ReactionsListModal
191220
handleFetchReactions={handleFetchReactions}
192-
onClose={() => setSelectedReactionType(null)}
193-
onSelectedReactionTypeChange={
194-
setSelectedReactionType as ReactionsListModalProps['onSelectedReactionTypeChange']
195-
}
196-
open={selectedReactionType !== null}
221+
onSelectedReactionTypeChange={setSelectedReactionType}
197222
reactions={existingReactions}
198223
selectedReactionType={selectedReactionType}
199224
sortReactionDetails={sortReactionDetails}
225+
totalReactionCount={totalReactionCount}
200226
/>
201-
)}
227+
</DialogAnchor>
202228
</>
203229
);
204230
};

0 commit comments

Comments
 (0)