Skip to content

Commit 08dc4b6

Browse files
authored
fix: align chat view sidebars and attachment preview remove icon (#3004)
### 🎯 Goal Fix two UI regressions: - the attachment preview dismiss/remove icon was not respecting dark mode tokens - ChatView channel/thread toggling caused width jumps because the channel and thread sidebars used different width contracts ### 🛠 Implementation details - Remove the nested SVG color override from `src/components/MessageInput/styling/RemoveAttachmentPreviewButton.scss` so the icon inherits the existing close-control tokens. - Restore the failing `AttachmentPreviewList` test suite by fixing the missing React import in `PlayButton`, aligning the tests with the current media preview test IDs/override props, and seeding edited-message state through `messageComposer`. - Give `ThreadList` the same sidebar width/flex contract as `ChannelList`, and ensure the thread detail pane flexes correctly inside `ChatView.Threads`. - In the Vite example, define a shared `350px` sidebar baseline for both channel and thread views instead of separate ad hoc widths. ### 🎨 UI Changes - Attachment preview remove icons now inherit the correct dark mode colors. - Channel and thread sidebars now stay width-aligned when toggling in ChatView. - The sample app uses a shared `350px` sidebar baseline for both views.
1 parent fbe28d0 commit 08dc4b6

7 files changed

Lines changed: 123 additions & 62 deletions

File tree

examples/vite/src/index.scss

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ body {
2525

2626
@layer stream-overrides {
2727
.str-chat {
28+
--app-chat-view-sidebar-width: 350px;
29+
--str-chat__channel-list-width: var(--app-chat-view-sidebar-width);
30+
--str-chat__thread-list-width: var(--app-chat-view-sidebar-width);
31+
2832
height: 100%;
2933
width: 100%;
3034
}
@@ -48,11 +52,6 @@ body {
4852

4953
.str-chat__channel-list {
5054
height: 100%;
51-
52-
&.str-chat__channel-list--open {
53-
flex: 0 0 300px;
54-
max-width: 300px;
55-
}
5655
}
5756

5857
.str-chat__main-panel {
@@ -132,11 +131,6 @@ body {
132131
width: 100%;
133132
max-width: 360px;
134133
}
135-
136-
.str-chat__thread-list-container.str-chat__thread-list-container--open {
137-
width: 100%;
138-
max-width: 360px;
139-
}
140134
}
141135

142136
@container (max-width: 860px) {

src/components/Button/PlayButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import { Button } from './Button';
23
import type { ComponentProps } from 'react';
34
import clsx from 'clsx';

src/components/ChatView/styling/ChatView.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,13 @@
144144
flex-grow: 1;
145145
min-width: 0;
146146
position: relative;
147+
148+
> .str-chat__dropzone-root--thread,
149+
> .str-chat__thread-container {
150+
flex: 1 1 auto;
151+
min-width: 0;
152+
max-width: none;
153+
width: 100%;
154+
}
147155
}
148156
}

src/components/MessageInput/__tests__/AttachmentPreviewList.test.js

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,41 @@ import {
1919
generateVoiceRecordingAttachment,
2020
initClientWithChannels,
2121
} from '../../../mock-builders';
22-
import { MessageProvider, WithComponents } from '../../../context';
22+
import { WithComponents } from '../../../context';
2323

2424
jest.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation();
2525

26-
const RETRY_BTN_TEST_ID = 'file-preview-item-retry-button';
27-
const RETRY_BTN_IMAGE_TEST_ID = 'image-preview-item-retry-button';
28-
const DELETE_BTN_TEST_ID = 'file-preview-item-delete-button';
29-
const DELETE_BTN_IMAGE_TEST_ID = 'image-preview-item-delete-button';
3026
const LOADING_INDICATOR_TEST_ID = 'loading-indicator';
3127
const ATTACHMENT_PREVIEW_LIST_TEST_ID = 'attachment-preview-list';
28+
const ATTACHMENT_PREVIEW_TEST_IDS = {
29+
audio: {
30+
delete: 'audio-preview-item-delete-button',
31+
retry: 'file-preview-item-retry-button',
32+
},
33+
file: {
34+
delete: 'file-preview-item-delete-button',
35+
retry: 'file-preview-item-retry-button',
36+
},
37+
image: {
38+
delete: 'video-preview-item-delete-button',
39+
retry: 'video-preview-item-retry-button',
40+
},
41+
unsupported: {
42+
delete: 'file-preview-item-delete-button',
43+
retry: 'file-preview-item-retry-button',
44+
},
45+
video: {
46+
delete: 'video-preview-item-delete-button',
47+
retry: 'video-preview-item-retry-button',
48+
},
49+
};
50+
const PREVIEW_COMPONENT_PROP_NAMES = {
51+
audio: 'AudioAttachmentPreview',
52+
file: 'FileAttachmentPreview',
53+
image: 'ImageAttachmentPreview',
54+
unsupported: 'UnsupportedAttachmentPreview',
55+
video: 'VideoAttachmentPreview',
56+
};
3257

3358
const renderComponent = async ({
3459
attachments,
@@ -47,6 +72,15 @@ const renderComponent = async ({
4772
client = initiated.client;
4873
channel = initiated.channels[0];
4974
}
75+
const composerMessage = editedMessage
76+
? {
77+
...editedMessage,
78+
cid: channel.cid,
79+
id: channel.id,
80+
type: channel.type,
81+
}
82+
: undefined;
83+
channel.messageComposer.initState({ composition: composerMessage });
5084
channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
5185
if (coords) channel.messageComposer.locationComposer.setData(coords);
5286
let result;
@@ -55,22 +89,7 @@ const renderComponent = async ({
5589
<WithComponents overrides={components}>
5690
<Chat client={client}>
5791
<Channel channel={channel}>
58-
{editedMessage ? (
59-
<MessageProvider
60-
value={{
61-
message: {
62-
...editedMessage,
63-
cid: channel.cid,
64-
id: channel.id,
65-
type: channel.type,
66-
},
67-
}}
68-
>
69-
<Component {...props} />
70-
</MessageProvider>
71-
) : (
72-
<Component {...props} />
73-
)}
92+
<Component {...props} />
7493
</Channel>
7594
</Chat>
7695
</WithComponents>,
@@ -202,9 +221,7 @@ describe('AttachmentPreviewList', () => {
202221
client,
203222
});
204223

205-
const retryButton = screen.getByTestId(
206-
type === 'image' ? RETRY_BTN_IMAGE_TEST_ID : RETRY_BTN_TEST_ID,
207-
);
224+
const retryButton = screen.getByTestId(ATTACHMENT_PREVIEW_TEST_IDS[type].retry);
208225

209226
fireEvent.click(retryButton);
210227

@@ -229,7 +246,9 @@ describe('AttachmentPreviewList', () => {
229246
});
230247

231248
expect(screen.queryByTestId(LOADING_INDICATOR_TEST_ID)).toBeInTheDocument();
232-
expect(screen.queryByTestId(RETRY_BTN_TEST_ID)).not.toBeInTheDocument();
249+
expect(
250+
screen.queryByTestId(ATTACHMENT_PREVIEW_TEST_IDS[type].retry),
251+
).not.toBeInTheDocument();
233252
});
234253

235254
it('removes retry button on successful upload', async () => {
@@ -247,7 +266,9 @@ describe('AttachmentPreviewList', () => {
247266
attachments: [localAttachment],
248267
});
249268

250-
expect(screen.queryByTestId(RETRY_BTN_TEST_ID)).not.toBeInTheDocument();
269+
expect(
270+
screen.queryByTestId(ATTACHMENT_PREVIEW_TEST_IDS[type].retry),
271+
).not.toBeInTheDocument();
251272
});
252273

253274
it('removes the preview', async () => {
@@ -277,25 +298,14 @@ describe('AttachmentPreviewList', () => {
277298
client,
278299
});
279300

280-
fireEvent.click(
281-
screen.getByTestId(
282-
type === 'image' ? DELETE_BTN_IMAGE_TEST_ID : DELETE_BTN_TEST_ID,
283-
),
284-
);
301+
fireEvent.click(screen.getByTestId(ATTACHMENT_PREVIEW_TEST_IDS[type].delete));
285302

286303
expect(removeAttachmentsSpy).toHaveBeenCalledWith([
287304
localAttachment.localMetadata.id,
288305
]);
289306
});
290307

291308
it('renders custom preview component', async () => {
292-
const previewComponentNames = {
293-
audio: 'AudioAttachmentPreview',
294-
file: 'FileAttachmentPreview',
295-
image: 'ImageAttachmentPreview',
296-
unsupported: 'UnsupportedAttachmentPreview',
297-
video: 'MediaAttachmentPreview',
298-
};
299309
const title = `${type}-attachment`;
300310
const id = `${type}-id`;
301311
const uploadedAttachmentData = generate[type]({
@@ -310,7 +320,7 @@ describe('AttachmentPreviewList', () => {
310320
const CustomPreviewComponent = () => <div>{text}</div>;
311321
await renderComponent({
312322
attachments: [localAttachment],
313-
props: { [previewComponentNames[type]]: CustomPreviewComponent },
323+
props: { [PREVIEW_COMPONENT_PROP_NAMES[type]]: CustomPreviewComponent },
314324
});
315325

316326
expect(screen.queryByText(text)).toBeInTheDocument();

src/components/MessageInput/__tests__/__snapshots__/AttachmentPreviewList.test.js.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ exports[`AttachmentPreviewList should render custom BaseImage component 1`] = `
7171
</div>
7272
<button
7373
aria-label="Remove attachment"
74-
class="str-chat__button str-chat__button--secondary str-chat__button--solid str-chat__button--circular str-chat__attachment-preview__remove-button"
74+
class="str-chat__button str-chat__button--circular str-chat__attachment-preview__remove-button"
7575
data-testid="video-preview-item-delete-button"
7676
disabled=""
7777
type="button"
@@ -149,7 +149,7 @@ exports[`AttachmentPreviewList should render custom BaseImage component 1`] = `
149149
</div>
150150
<button
151151
aria-label="Remove attachment"
152-
class="str-chat__button str-chat__button--secondary str-chat__button--solid str-chat__button--circular str-chat__attachment-preview__remove-button"
152+
class="str-chat__button str-chat__button--circular str-chat__attachment-preview__remove-button"
153153
data-testid="video-preview-item-delete-button"
154154
disabled=""
155155
type="button"

src/components/MessageInput/styling/RemoveAttachmentPreviewButton.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,4 @@
77
border: 3px solid var(--str-chat__attachment-preview-close-icon-border-color);
88
color: var(--str-chat__attachment-preview-close-icon-color);
99
border-radius: var(--radius-max);
10-
11-
svg {
12-
color: var(--control-play-control-icon);
13-
}
1410
}

src/components/Threads/ThreadList/styling/ThreadList.scss

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,65 @@
99
--str-chat__thread-list-border-inline-start: none;
1010
--str-chat__thread-list-border-inline-end: 1px solid var(--str-chat__surface-color);
1111
--str-chat__thread-list-box-shadow: none;
12+
--str-chat__thread-list-transition-duration: var(
13+
--str-chat__channel-list-transition-duration,
14+
180ms
15+
);
16+
--str-chat__thread-list-transition-easing: var(
17+
--str-chat__channel-list-transition-easing,
18+
ease
19+
);
20+
--str-chat__thread-list-transition-offset: var(
21+
--str-chat__channel-list-transition-offset,
22+
8px
23+
);
24+
--str-chat__thread-list-width: var(--str-chat__channel-list-width, 30%);
25+
--str-chat__thread-list-mobile-width: var(--str-chat__channel-list-mobile-width, 100%);
26+
--str-chat__thread-list-min-width: 280px;
1227
}
1328

1429
.str-chat__thread-list-container {
1530
@include utils.component-layer-overrides('thread-list');
1631
display: flex;
32+
flex: 0 0 var(--str-chat__thread-list-width);
1733
flex-direction: column;
1834
height: 100%;
35+
max-width: 100%;
36+
min-width: var(--str-chat__thread-list-min-width);
37+
opacity: 1;
38+
transform: translateX(0);
39+
transition:
40+
flex-basis var(--str-chat__thread-list-transition-duration)
41+
var(--str-chat__thread-list-transition-easing),
42+
min-width var(--str-chat__thread-list-transition-duration)
43+
var(--str-chat__thread-list-transition-easing),
44+
width var(--str-chat__thread-list-transition-duration)
45+
var(--str-chat__thread-list-transition-easing),
46+
max-width var(--str-chat__thread-list-transition-duration)
47+
var(--str-chat__thread-list-transition-easing),
48+
opacity var(--str-chat__thread-list-transition-duration)
49+
var(--str-chat__thread-list-transition-easing),
50+
transform var(--str-chat__thread-list-transition-duration)
51+
var(--str-chat__thread-list-transition-easing);
52+
width: var(--str-chat__thread-list-width);
1953

2054
/* Mobile: hide when nav closed; when open show as overlay. */
2155
@media (max-width: 767px) {
56+
flex-basis: auto;
2257
inset-inline-start: 0;
58+
max-width: 100%;
59+
min-width: 0;
2360
pointer-events: none;
2461
position: absolute;
2562
top: 0;
2663
bottom: 0;
27-
transform: translateX(
28-
calc(0px - var(--str-chat__channel-list-transition-offset, 8px))
29-
);
64+
transform: translateX(calc(0px - var(--str-chat__thread-list-transition-offset)));
3065
transition:
31-
transform var(--str-chat__channel-list-transition-duration, 180ms)
32-
var(--str-chat__channel-list-transition-easing, ease),
33-
visibility 0s linear var(--str-chat__channel-list-transition-duration, 180ms);
66+
transform var(--str-chat__thread-list-transition-duration)
67+
var(--str-chat__thread-list-transition-easing),
68+
visibility 0s linear var(--str-chat__thread-list-transition-duration);
3469
visibility: hidden;
35-
width: 100%;
70+
width: var(--str-chat__thread-list-mobile-width);
3671
z-index: 1;
3772

3873
&.str-chat__thread-list-container--open {
@@ -51,14 +86,31 @@
5186

5287
/* Desktop (≥768px): collapse when nav closed so main content uses space. */
5388
@media (min-width: 768px) {
89+
&.str-chat__thread-list-container--open {
90+
flex-basis: var(--str-chat__thread-list-width);
91+
max-width: 100%;
92+
min-width: var(--str-chat__thread-list-min-width);
93+
opacity: 1;
94+
pointer-events: auto;
95+
transform: translateX(0);
96+
width: var(--str-chat__thread-list-width);
97+
}
98+
5499
&:not(.str-chat__thread-list-container--open) {
55100
flex: 0 0 0;
56101
width: 0;
57102
min-width: 0;
58103
max-width: 0;
104+
opacity: 0;
59105
overflow: hidden;
106+
pointer-events: none;
107+
transform: translateX(calc(0px - var(--str-chat__thread-list-transition-offset)));
60108
}
61109
}
110+
111+
@media (prefers-reduced-motion: reduce) {
112+
transition: none;
113+
}
62114
}
63115

64116
.str-chat__thread-list {

0 commit comments

Comments
 (0)