Skip to content

Commit 887a326

Browse files
authored
feat(examples): add RTL direction toggle to vite example app (#3084)
### 🎯 Goal Add an RTL direction toggle to the vite example app so we can easily test and demo RTL layouts. ### πŸ›  Implementation details - Added `direction: 'ltr' | 'rtl'` to `ThemeSettingsState` in the app settings store - Direction is persisted to localStorage (`stream-chat-react:example-direction`) and applied via `<html dir="...">` attribute - Added a sidebar toggle button (bottom section, next to the theme toggle) using a text-direction icon - Added a new "General" tab in the settings modal with LTR/RTL button group - Fixed `SidebarThemeToggle` to spread `...theme` so toggling dark/light mode doesn't lose the direction value ### 🎨 UI Changes _No visual changes to the SDK β€” this only affects the vite example app's settings UI._
1 parent a82bdcb commit 887a326

File tree

4 files changed

+127
-3
lines changed

4 files changed

+127
-3
lines changed

β€Žexamples/vite/src/AppSettings/AppSettings.tsxβ€Ž

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,31 @@ const IconLightBulb = createIcon(
2424
</>,
2525
{ viewBox: '0 0 16 16' },
2626
);
27+
const IconTextDirection = createIcon(
28+
'IconTextDirection',
29+
<path d='M9.5 2H12.5C12.7761 2 13 2.22386 13 2.5C13 2.77614 12.7761 3 12.5 3H11V12.5C11 12.7761 10.7761 13 10.5 13C10.2239 13 10 12.7761 10 12.5V3H9V12.5C9 12.7761 8.77614 13 8.5 13C8.22386 13 8 12.7761 8 12.5V8H7.5C5.567 8 4 6.433 4 4.5C4 2.567 5.567 1 7.5 1H9.5V2ZM8 3H7.5C6.11929 3 5 4.11929 5 4.5C5 5.88071 6.11929 7 7.5 7H8V3ZM3.85355 13.8536C3.65829 14.0488 3.34171 14.0488 3.14645 13.8536L1.14645 11.8536C0.951184 11.6583 0.951184 11.3417 1.14645 11.1464C1.34171 10.9512 1.65829 10.9512 1.85355 11.1464L3 12.2929V9.5C3 9.22386 3.22386 9 3.5 9C3.77614 9 4 9.22386 4 9.5V12.2929L5.14645 11.1464C5.34171 10.9512 5.65829 10.9512 5.85355 11.1464C6.04882 11.3417 6.04882 11.6583 5.85355 11.8536L3.85355 13.8536Z' />,
30+
{ viewBox: '0 0 16 16' },
31+
);
32+
2733
import { ActionsMenu } from './ActionsMenu';
34+
import { GeneralTab } from './tabs/General';
2835
import { NotificationsTab } from './tabs/Notifications';
2936
import { ReactionsTab } from './tabs/Reactions';
3037
import { SidebarTab } from './tabs/Sidebar';
3138
import { appSettingsStore, useAppSettingsState } from './state';
3239

33-
type TabId = 'notifications' | 'reactions' | 'sidebar';
40+
type TabId = 'general' | 'notifications' | 'reactions' | 'sidebar';
3441

3542
const tabConfig: { Icon: ComponentType; id: TabId; title: string }[] = [
43+
{ Icon: IconGear, id: 'general', title: 'General' },
3644
{ Icon: IconBell, id: 'notifications', title: 'Notifications' },
3745
{ Icon: IconMessageBubble, id: 'sidebar', title: 'Sidebar' },
3846
{ Icon: IconEmoji, id: 'reactions', title: 'Reactions' },
3947
];
4048

4149
const SidebarThemeToggle = ({ iconOnly = true }: { iconOnly?: boolean }) => {
4250
const {
51+
theme,
4352
theme: { mode },
4453
} = useAppSettingsState();
4554
const nextMode = mode === 'dark' ? 'light' : 'dark';
@@ -55,7 +64,7 @@ const SidebarThemeToggle = ({ iconOnly = true }: { iconOnly?: boolean }) => {
5564
isActive={mode === 'dark'}
5665
onClick={() =>
5766
appSettingsStore.partialNext({
58-
theme: { mode: nextMode },
67+
theme: { ...theme, mode: nextMode },
5968
})
6069
}
6170
role='switch'
@@ -65,14 +74,42 @@ const SidebarThemeToggle = ({ iconOnly = true }: { iconOnly?: boolean }) => {
6574
);
6675
};
6776

77+
const SidebarRtlToggle = ({ iconOnly = true }: { iconOnly?: boolean }) => {
78+
const {
79+
theme,
80+
theme: { direction },
81+
} = useAppSettingsState();
82+
const isRtl = direction === 'rtl';
83+
84+
return (
85+
<ChatViewSelectorButton
86+
aria-checked={isRtl}
87+
aria-label={`Switch to ${isRtl ? 'LTR' : 'RTL'} direction`}
88+
aria-selected={isRtl}
89+
className='app__settings-group_button'
90+
iconOnly={iconOnly}
91+
Icon={IconTextDirection}
92+
isActive={isRtl}
93+
onClick={() =>
94+
appSettingsStore.partialNext({
95+
theme: { ...theme, direction: isRtl ? 'ltr' : 'rtl' },
96+
})
97+
}
98+
role='switch'
99+
text={isRtl ? 'RTL' : 'LTR'}
100+
/>
101+
);
102+
};
103+
68104
export const AppSettings = ({ iconOnly = true }: { iconOnly?: boolean }) => {
69-
const [activeTab, setActiveTab] = useState<TabId>('sidebar');
105+
const [activeTab, setActiveTab] = useState<TabId>('general');
70106
const [open, setOpen] = useState(false);
71107

72108
return (
73109
<div className='app__settings-group'>
74110
<ActionsMenu iconOnly={iconOnly} />
75111
<SidebarThemeToggle iconOnly={iconOnly} />
112+
<SidebarRtlToggle iconOnly={iconOnly} />
76113
<ChatViewSelectorButton
77114
className='app__settings-group_button'
78115
iconOnly={iconOnly}
@@ -113,6 +150,7 @@ export const AppSettings = ({ iconOnly = true }: { iconOnly?: boolean }) => {
113150
id={`${activeTab}-content`}
114151
role='tabpanel'
115152
>
153+
{activeTab === 'general' && <GeneralTab />}
116154
{activeTab === 'notifications' && <NotificationsTab />}
117155
{activeTab === 'sidebar' && <SidebarTab />}
118156
{activeTab === 'reactions' && <ReactionsTab />}

β€Žexamples/vite/src/AppSettings/state.tsβ€Ž

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type ChatViewSettingsState = {
1212
};
1313

1414
export type ThemeSettingsState = {
15+
direction: 'ltr' | 'rtl';
1516
mode: 'dark' | 'light';
1617
};
1718

@@ -47,6 +48,7 @@ export type AppSettingsState = {
4748

4849
const panelLayoutStorageKey = 'stream-chat-react:example-panel-layout';
4950
const themeStorageKey = 'stream-chat-react:example-theme-mode';
51+
const directionStorageKey = 'stream-chat-react:example-direction';
5052
const themeUrlParam = 'theme';
5153

5254
const clamp = (value: number, min: number, max?: number) => {
@@ -83,10 +85,25 @@ const defaultAppSettingsState: AppSettingsState = {
8385
visualStyle: 'clustered',
8486
},
8587
theme: {
88+
direction: 'ltr',
8689
mode: 'light',
8790
},
8891
};
8992

93+
const getStoredDirection = (): ThemeSettingsState['direction'] | undefined => {
94+
if (typeof window === 'undefined') return;
95+
96+
try {
97+
const stored = window.localStorage.getItem(directionStorageKey);
98+
99+
if (stored === 'ltr' || stored === 'rtl') {
100+
return stored;
101+
}
102+
} catch {
103+
return;
104+
}
105+
};
106+
90107
const getStoredThemeMode = (): ThemeSettingsState['mode'] | undefined => {
91108
if (typeof window === 'undefined') return;
92109

@@ -166,6 +183,21 @@ const getThemeModeFromUrl = (): ThemeSettingsState['mode'] | undefined => {
166183
}
167184
};
168185

186+
const persistDirection = (direction: ThemeSettingsState['direction']) => {
187+
if (typeof window === 'undefined') return;
188+
189+
try {
190+
window.localStorage.setItem(directionStorageKey, direction);
191+
} catch {
192+
// ignore persistence failures in environments where localStorage is unavailable
193+
}
194+
};
195+
196+
const applyDirection = (direction: ThemeSettingsState['direction']) => {
197+
if (typeof document === 'undefined') return;
198+
document.documentElement.setAttribute('dir', direction);
199+
};
200+
169201
const persistThemeMode = (themeMode: ThemeSettingsState['mode']) => {
170202
if (typeof window === 'undefined') return;
171203

@@ -207,6 +239,7 @@ const initialAppSettingsState: AppSettingsState = {
207239
panelLayout: getStoredPanelLayoutSettings() ?? defaultAppSettingsState.panelLayout,
208240
theme: {
209241
...defaultAppSettingsState.theme,
242+
direction: getStoredDirection() ?? defaultAppSettingsState.theme.direction,
210243
mode:
211244
getThemeModeFromUrl() ?? getStoredThemeMode() ?? defaultAppSettingsState.theme.mode,
212245
},
@@ -222,6 +255,17 @@ appSettingsStore.subscribeWithSelector(
222255
},
223256
);
224257

258+
appSettingsStore.subscribeWithSelector(
259+
({ theme }) => ({ direction: theme.direction }),
260+
({ direction }) => {
261+
persistDirection(direction);
262+
applyDirection(direction);
263+
},
264+
);
265+
266+
// Apply initial direction on load
267+
applyDirection(initialAppSettingsState.theme.direction);
268+
225269
appSettingsStore.subscribeWithSelector(
226270
({ panelLayout }) => panelLayout,
227271
(panelLayout) => {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Button } from 'stream-chat-react';
2+
import { appSettingsStore, useAppSettingsState } from '../../state';
3+
4+
export const GeneralTab = () => {
5+
const {
6+
theme,
7+
theme: { direction },
8+
} = useAppSettingsState();
9+
10+
return (
11+
<div className='app__settings-modal__content-stack'>
12+
<div className='app__settings-modal__field'>
13+
<div className='app__settings-modal__field-label'>Text direction</div>
14+
<div className='app__settings-modal__options-row'>
15+
<Button
16+
aria-pressed={direction === 'ltr'}
17+
className='app__settings-modal__option-button str-chat__button--outline str-chat__button--secondary str-chat__button--size-sm'
18+
onClick={() =>
19+
appSettingsStore.partialNext({
20+
theme: { ...theme, direction: 'ltr' },
21+
})
22+
}
23+
>
24+
LTR
25+
</Button>
26+
<Button
27+
aria-pressed={direction === 'rtl'}
28+
className='app__settings-modal__option-button str-chat__button--outline str-chat__button--secondary str-chat__button--size-sm'
29+
onClick={() =>
30+
appSettingsStore.partialNext({
31+
theme: { ...theme, direction: 'rtl' },
32+
})
33+
}
34+
>
35+
RTL
36+
</Button>
37+
</div>
38+
</div>
39+
</div>
40+
);
41+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { GeneralTab } from './GeneralTab';

0 commit comments

Comments
Β (0)