Skip to content

Commit 6c06e04

Browse files
authored
fix: assorted UI/UX fixes (Giphy, polls, dialogs, composer, headers) (#3081)
### 🎯 Goal Bundle of UI/UX fixes across multiple components. ### πŸ›  Implementation details **fix(Attachment): use pixel-based sizing constraints for Giphy images** Giphy images used percentage-based CSS constraints which don't resolve to pixel values. Replaced with pixel-based constraints via `var(--str-chat__attachment-max-width)` and the `clamped-height-from-original-image-dimensions` mixin. **fix(Message): prevent editing Giphy messages** Giphy messages (`command === 'giphy'`) are no longer editable since their content is generated by the /giphy command. **fix(Form): remove redundant onClick on SwitchField label** `SwitchFieldLabel` had both `htmlFor` and a programmatic `onClick`, causing the checkbox to toggle twice when clicking label text. Removed the redundant `onClick`. **fix(Poll): fall back to user ID instead of "Anonymous" for voters without a display name** `PollVoteAuthor` fell back to "Anonymous" when `vote.user.name` was falsy, making public polls appear anonymous. Now falls back to `vote.user.id`. **fix(Dialog): prevent dialog overlay from blocking pointer events** The dialog overlay expanded to the nearest positioned ancestor, blocking the channel list. Set `pointer-events: none` on the overlay with `pointer-events: auto` on dialog contents. **fix(MessageComposer): hide textarea scrollbar inside rounded compose area** Hidden with `scrollbar-width: none`, matching the pattern used in polls, reactions, and search. **fix(MessageComposer): restore pre-edit composer state when cancelling edit** When exiting edit mode, the composer restores to its pre-edit state using a WeakMap-based snapshot mechanism. The snapshot is discarded after a successful edit save. **fix(ChannelHeader, ThreadHeader): truly center title text** The title wasn't centered because flanking elements had different widths. Wrapped the toggle button and avatar/close button in always-rendered start/end container divs with `flex: 1`, so the center element is truly centered regardless of flanking content. **style(AudioPlayback): bump PlaybackRateButton min-width** Increased from 40px to 48px to prevent text clipping. **fix(ChannelList): scope dialog overlay with `position: relative`** The ChannelList lacked `position: relative`, so its dialog overlay (`position: absolute; inset: 0`) escaped to the nearest positioned ancestor higher in the tree β€” covering the entire sidebar when consumers override `transform: none` on the channel list. **fix(Modal): restore pointer events on GlobalModal** `GlobalModal` portals into `.str-chat__dialog-overlay` (which has `pointer-events: none`) without using `DialogAnchor`, so its content never received the `.str-chat__dialog-contents` class that restores `pointer-events: auto`. The modal was visible but completely click-through. ### 🎨 UI Changes - Giphy images properly size-constrained with CDN resizing - Giphy messages no longer show "Edit Message" in actions menu - Poll creation toggles respond correctly when clicking label text - Poll vote results show user ID instead of "Anonymous" for nameless users - Dialog overlay no longer blocks interaction with the channel list - Composer textarea no longer shows a scrollbar inside the rounded input area - Cancelling edit mode restores the composer to its pre-edit state - Channel and thread header titles are truly centered - ChannelList dialog overlay scoped to the channel list area - GlobalModal is interactive again (no longer click-through)
1 parent 887a326 commit 6c06e04

File tree

23 files changed

+312
-67
lines changed

23 files changed

+312
-67
lines changed

β€Žsrc/components/Attachment/styling/Giphy.scssβ€Ž

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
justify-content: center;
1515
overflow: hidden;
1616

17-
img {
17+
img.str-chat__base-image {
18+
@include utils.clamped-height-from-original-image-dimensions(
19+
'--str-chat__attachment-max-width',
20+
'--str-chat__attachment-max-width'
21+
);
22+
1823
object-fit: contain;
19-
max-height: 100%;
20-
max-width: 100%;
21-
height: 100%;
24+
max-height: var(--str-chat__attachment-max-width);
25+
max-width: var(--str-chat__attachment-max-width);
2226
width: 100%;
2327
cursor: default;
2428
}

β€Žsrc/components/AudioPlayback/styling/PlaybackRateButton.scssβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
justify-content: center;
77
align-items: center;
88
gap: var(--spacing-xs);
9-
min-width: 40px;
9+
min-width: 48px;
1010
min-height: 24px;
1111
max-height: 24px;
1212
padding: var(--button-padding-y-sm) var(--spacing-xs);

β€Žsrc/components/ChannelHeader/ChannelHeader.tsxβ€Ž

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,25 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
7171
'str-chat__channel-header--sidebar-collapsed': !navOpen,
7272
})}
7373
>
74-
<ToggleSidebarButton mode='expand'>
75-
<MenuIcon />
76-
</ToggleSidebarButton>
74+
<div className='str-chat__channel-header__start'>
75+
<ToggleSidebarButton mode='expand'>
76+
<MenuIcon />
77+
</ToggleSidebarButton>
78+
</div>
7779
<div className='str-chat__channel-header__data'>
7880
<div className='str-chat__channel-header__data__title'>{displayTitle}</div>
7981
<ChannelHeaderSubtitle />
8082
</div>
81-
<Avatar
82-
className='str-chat__avatar--channel-header'
83-
displayMembers={groupChannelDisplayInfo?.members}
84-
imageUrl={displayImage}
85-
overflowCount={groupChannelDisplayInfo?.overflowCount}
86-
size='lg'
87-
userName={displayTitle}
88-
/>
83+
<div className='str-chat__channel-header__end'>
84+
<Avatar
85+
className='str-chat__avatar--channel-header'
86+
displayMembers={groupChannelDisplayInfo?.members}
87+
imageUrl={displayImage}
88+
overflowCount={groupChannelDisplayInfo?.overflowCount}
89+
size='lg'
90+
userName={displayTitle}
91+
/>
92+
</div>
8993
</div>
9094
);
9195
};

β€Žsrc/components/ChannelHeader/styling/ChannelHeader.scssβ€Ž

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,22 @@
3434
.str-chat__channel-header {
3535
@include utils.component-layer-overrides('channel-header');
3636
@include utils.header-layout;
37-
flex: 1;
37+
38+
.str-chat__channel-header__start {
39+
flex: 1;
40+
display: flex;
41+
}
3842

3943
.str-chat__channel-header__data {
4044
@include utils.header-text-layout;
4145
}
4246

47+
.str-chat__channel-header__end {
48+
flex: 1;
49+
display: flex;
50+
justify-content: flex-end;
51+
}
52+
4353
.str-chat__channel-header__data__title,
4454
.str-chat__channel-header__data__subtitle {
4555
@include utils.ellipsis-text;

β€Žsrc/components/ChannelList/styling/ChannelList.scssβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
flex-direction: column;
104104
height: 100%;
105105
max-width: 100%;
106+
position: relative;
106107
min-width: 280px;
107108
opacity: 1;
108109
transform: translateX(0);

β€Žsrc/components/Dialog/service/DialogPortal.tsxβ€Ž

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ export const DialogPortalDestination = () => {
1717
const openedDialogCount = useOpenedDialogCount({ dialogManagerId: dialogManager?.id });
1818
const [destinationRoot, setDestinationRoot] = useState<HTMLDivElement | null>(null);
1919

20-
// Handle clicks outside the overlay-covered area.
21-
// Dismiss policy is manager-level (`dialogManager.closeOnClickOutside`).
20+
// Handle clicks outside dialog contents.
21+
// Clicks on the overlay itself or outside it close open dialogs;
22+
// only clicks on dialog contents (descendants) are ignored.
2223
useEffect(() => {
2324
if (!destinationRoot || !dialogManager) return;
2425

2526
const handleDocumentClick = (event: MouseEvent) => {
26-
if (destinationRoot.contains(event.target as Node)) return;
27+
const target = event.target as Node;
28+
if (target !== destinationRoot && destinationRoot.contains(target)) return;
2729
// Defer so target onClick handlers (e.g. context-menu toggle buttons) can run first.
2830
setTimeout(() => {
2931
Object.values(dialogManager.state.getLatestValue().dialogsById).forEach(
@@ -56,23 +58,6 @@ export const DialogPortalDestination = () => {
5658
className='str-chat__dialog-overlay'
5759
data-str-chat__portal-id={dialogManager?.id}
5860
data-testid='str-chat__dialog-overlay'
59-
onClick={(event) => {
60-
if (!dialogManager) return;
61-
if (event.target !== event.currentTarget) return;
62-
Object.values(dialogManager.state.getLatestValue().dialogsById).forEach(
63-
(dialog) => {
64-
if (!dialog.isOpen) return;
65-
if (
66-
!shouldCloseOnOutsideClick({
67-
dialog,
68-
managerCloseOnClickOutside: dialogManager.closeOnClickOutside,
69-
})
70-
)
71-
return;
72-
dialogManager.close(dialog.id);
73-
},
74-
);
75-
}}
7661
ref={setDestinationRoot}
7762
style={
7863
{

β€Žsrc/components/Dialog/styling/Dialog.scssβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@
44
height: var(--str-chat__dialog-overlay-height);
55
width: 100%;
66
z-index: 2;
7+
pointer-events: none;
8+
9+
.str-chat__dialog-contents {
10+
pointer-events: auto;
11+
}
712
}

β€Žsrc/components/Form/SwitchField.tsxβ€Ž

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,7 @@ export const SwitchField = ({
5555
ref={inputRef}
5656
/>
5757
{title ? (
58-
<SwitchFieldLabel
59-
description={description}
60-
htmlFor={id}
61-
onClick={() => inputRef.current?.click()}
62-
title={title}
63-
></SwitchFieldLabel>
58+
<SwitchFieldLabel description={description} htmlFor={id} title={title} />
6459
) : (
6560
children
6661
)}

β€Žsrc/components/Message/hooks/useUserRole.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const useUserRole = (
3838

3939
const canEdit =
4040
!message.poll &&
41+
message.command !== 'giphy' &&
4142
((!onlySenderCanEdit && channelCapabilities['update-any-message']) ||
4243
(isMyMessage && channelCapabilities['update-own-message']));
4344

β€Žsrc/components/MessageActions/MessageActions.defaults.tsxβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from '../Icons';
2828
import { isUserMuted } from '../Message/utils';
2929
import { useMessageComposerController } from '../MessageComposer/hooks/useMessageComposerController';
30+
import { savePreEditSnapshot } from '../MessageComposer/preEditSnapshot';
3031
import { addNotificationTargetTag, useNotificationTarget } from '../Notifications';
3132
import { useMessageReminder } from '../Message/hooks/useMessageReminder';
3233
import { ReactionSelectorWithButton } from '../Reactions/ReactionSelectorWithButton';
@@ -175,6 +176,7 @@ const DefaultMessageActionComponents = {
175176
className={msgActionsBoxButtonClassName}
176177
Icon={IconEdit}
177178
onClick={() => {
179+
savePreEditSnapshot(messageComposer);
178180
messageComposer.initState({ composition: message });
179181
closeMenu();
180182
}}

0 commit comments

Comments
Β (0)