Skip to content

Commit 7d52f76

Browse files
Fix scrollbars for all macOS settings
1 parent fe2a012 commit 7d52f76

File tree

10 files changed

+218
-185
lines changed

10 files changed

+218
-185
lines changed

stylesheets/_modules.scss

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4856,8 +4856,9 @@ button.module-calling-participants-list__contact {
48564856
.module-conversation-list {
48574857
$normal-row-height: 72px;
48584858

4859-
padding-inline-start: 10px;
4860-
padding-inline-end: 1px; /* leaving room for scrollbar */
4859+
scrollbar-gutter: stable;
4860+
padding-inline-start: 11px;
4861+
padding-inline-end: calc(11px - var(--axo-scrollbar-gutter-thin-vertical));
48614862

48624863
@include mixins.scrollbar-on-hover;
48634864

@@ -4867,13 +4868,6 @@ button.module-calling-participants-list__contact {
48674868
padding-inline: 0;
48684869
}
48694870

4870-
// Center chat list icons in narrow mode by reserving scrollbar space, preventing
4871-
// scrollbar from pushing content
4872-
&--width-narrow {
4873-
padding-inline: 10px 1px;
4874-
scrollbar-gutter: stable;
4875-
}
4876-
48774871
&--has-dialog-padding {
48784872
padding-block-start: 8px;
48794873
}

stylesheets/components/CallsTab.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@
209209
}
210210

211211
.CallsList__List {
212+
scrollbar-gutter: stable;
213+
padding-inline-start: 11px;
214+
padding-inline-end: calc(11px - var(--axo-scrollbar-gutter-thin-vertical));
212215
@include mixins.scrollbar-on-hover;
213216
}
214217

@@ -310,6 +313,8 @@
310313
// Override .ListTile
311314
.ListTile.CallsList__ItemTile {
312315
padding-block: 10px;
316+
border: none;
317+
border-radius: 12px;
313318

314319
// Override .ListTile__subtitle with correct font size
315320
.ListTile__subtitle {

stylesheets/components/Preferences.scss

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ $secondary-text-color: light-dark(
5454
&__scroll-area {
5555
overflow-y: scroll;
5656
max-height: 100%;
57+
58+
scrollbar-gutter: stable;
59+
padding-inline-start: 11px;
60+
padding-inline-end: calc(11px - var(--axo-scrollbar-gutter-thin-vertical));
61+
62+
@include mixins.scrollbar-on-hover;
5763
}
5864

5965
&__padding {
@@ -71,10 +77,6 @@ $secondary-text-color: light-dark(
7177
flex-direction: row;
7278
align-items: center;
7379

74-
width: calc(100% - 11px);
75-
margin-inline-start: 10px;
76-
margin-inline-end: 1px;
77-
7880
margin-bottom: 4px;
7981
border-radius: 8px;
8082

@@ -212,12 +214,10 @@ $secondary-text-color: light-dark(
212214
@include mixins.font-body-1;
213215
align-items: center;
214216
display: flex;
217+
width: 100%;
215218
height: 40px;
216-
width: calc(100% - 11px);
217219
padding-block: 14px;
218220
padding-inline: 0;
219-
margin-inline-start: 10px;
220-
margin-inline-end: 1px;
221221
border-radius: 10px;
222222
margin-bottom: 4px;
223223
}

stylesheets/components/Stories.scss

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,11 @@
105105
flex-direction: column;
106106
flex: 1;
107107
overflow-y: auto;
108-
padding-inline: 16px;
109-
108+
scrollbar-gutter: stable;
109+
padding-inline-start: 11px;
110+
padding-inline-end: calc(
111+
11px - var(--axo-scrollbar-gutter-thin-vertical)
112+
);
110113
@include mixins.scrollbar-on-hover;
111114

112115
&--empty {

ts/axo/AxoDialog.dom.stories.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
// Copyright 2025 Signal Messenger, LLC
22
// SPDX-License-Identifier: AGPL-3.0-only
3-
import type { CSSProperties, ReactNode } from 'react';
3+
import type { ReactNode } from 'react';
44
import React, { useId, useMemo, useState } from 'react';
55
import type { Meta } from '@storybook/react';
66
import { action } from '@storybook/addon-actions';
77
import { AxoDialog } from './AxoDialog.dom.js';
88
import { AxoButton } from './AxoButton.dom.js';
99
import { tw } from './tw.dom.js';
1010
import { AxoCheckbox } from './AxoCheckbox.dom.js';
11-
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
1211

1312
export default {
1413
title: 'Axo/AxoDialog',
@@ -306,21 +305,14 @@ function ExampleItem(props: { label: string; description: string }) {
306305
const labelId = useId();
307306
const descriptionId = useId();
308307

309-
const style = useMemo((): CSSProperties => {
310-
return {
311-
paddingInline: 24 - getScrollbarGutters('thin', 'custom').vertical,
312-
};
313-
}, []);
314-
315308
return (
316309
<div
317310
role="option"
318311
aria-selected={false}
319312
aria-labelledby={labelId}
320313
aria-describedby={descriptionId}
321314
tabIndex={0}
322-
className={tw('rounded-lg py-2.5 hover:bg-fill-secondary')}
323-
style={style}
315+
className={tw('rounded-lg px-[13px] py-2.5 hover:bg-fill-secondary')}
324316
>
325317
<div
326318
id={labelId}
@@ -365,7 +357,13 @@ export function ExampleLanguageDialog(): JSX.Element {
365357
/>
366358
</AxoDialog.ExperimentalSearch>
367359
<AxoDialog.Body padding="only-scrollbar-gutter">
368-
<div role="listbox">
360+
<div
361+
role="listbox"
362+
style={{
363+
paddingInline:
364+
'calc(11px - var(--axo-scrollbar-gutter-thin-vertical)',
365+
}}
366+
>
369367
<ExampleItem label="System Language" description="English" />
370368
<ExampleItem label="Afrikaans" description="Afrikaans" />
371369
<ExampleItem label="Arabic" description="العربية" />

ts/axo/AxoDialog.dom.tsx

Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,13 @@ import { AxoBaseDialog } from './_internal/AxoBaseDialog.dom.js';
1414
import { AxoSymbol } from './AxoSymbol.dom.js';
1515
import { tw } from './tw.dom.js';
1616
import { AxoScrollArea } from './AxoScrollArea.dom.js';
17-
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
1817
import { AxoButton } from './AxoButton.dom.js';
1918
import { AxoIconButton } from './AxoIconButton.dom.js';
2019

2120
const Namespace = 'AxoDialog';
2221

2322
const { useContentEscapeBehavior } = AxoBaseDialog;
2423

25-
// We want to have 24px of padding on either side of header/body/footer, but
26-
// it's import that we remain aligned with the vertical scrollbar gutters that
27-
// we need to measure in the browser to know the value of.
28-
//
29-
// Chrome currently renders vertical scrollbars as 11px with
30-
// `scrollbar-width: thin` but that could change someday or based on some OS
31-
// settings. So we'll target 24px but we'll tolerate different values.
32-
function getPadding(target: number, scrollbars: boolean): number {
33-
const scrollbarWidthExpected = 11;
34-
const paddingBeforeScrollbarWidth = target - scrollbarWidthExpected;
35-
36-
if (scrollbars) {
37-
// If this element has scrollbars we should just rely on the rendered gutter
38-
return paddingBeforeScrollbarWidth;
39-
}
40-
41-
const scrollbarWidthActual = getScrollbarGutters('thin', 'custom').vertical;
42-
43-
// If this element doesn't have scrollbars, we need to add the exact value of
44-
// the actual scrollbar gutter
45-
return scrollbarWidthActual + paddingBeforeScrollbarWidth;
46-
}
47-
4824
export namespace AxoDialog {
4925
/**
5026
* Component: <AxoDialog.Root>
@@ -140,18 +116,12 @@ export namespace AxoDialog {
140116
}>;
141117

142118
export const Header: FC<HeaderProps> = memo(props => {
143-
const style = useMemo(() => {
144-
return {
145-
paddingInline: getPadding(10, false),
146-
};
147-
}, []);
148119
return (
149120
<div
150121
className={tw(
151-
'grid items-center py-2.5',
122+
'grid items-center p-2.5',
152123
'grid-cols-[[back-slot]_1fr_[title-slot]_auto_[close-slot]_1fr]'
153124
)}
154-
style={style}
155125
>
156126
{props.children}
157127
</div>
@@ -204,20 +174,14 @@ export namespace AxoDialog {
204174
}>;
205175

206176
export const Title: FC<TitleProps> = memo(props => {
207-
const style = useMemo(() => {
208-
return {
209-
paddingInline: 24 - getPadding(10, false),
210-
};
211-
}, []);
212177
return (
213178
<Dialog.Title
214179
className={tw(
215-
'col-[title-slot] py-0.5',
180+
'col-[title-slot] px-3.5 py-0.5',
216181
'truncate text-center',
217182
'type-body-medium font-semibold text-label-primary',
218183
props.screenReaderOnly && 'sr-only'
219184
)}
220-
style={style}
221185
>
222186
{props.children}
223187
</Dialog.Title>
@@ -281,14 +245,7 @@ export namespace AxoDialog {
281245
}>;
282246

283247
export const ExperimentalSearch: FC<ExperimentalSearchProps> = memo(props => {
284-
const style = useMemo(() => {
285-
return { paddingInline: getPadding(16, false) };
286-
}, []);
287-
return (
288-
<div style={style} className={tw('pb-2')}>
289-
{props.children}
290-
</div>
291-
);
248+
return <div className={tw('px-4 pb-2')}>{props.children}</div>;
292249
});
293250

294251
ExperimentalSearch.displayName = `${Namespace}.ExperimentalSearch`;
@@ -308,9 +265,13 @@ export namespace AxoDialog {
308265
export const Body: FC<BodyProps> = memo(props => {
309266
const { padding = 'normal' } = props;
310267

311-
const style = useMemo((): CSSProperties => {
268+
const style = useMemo((): CSSProperties | undefined => {
269+
if (padding === 'only-scrollbar-gutter') {
270+
return;
271+
}
272+
312273
return {
313-
paddingInline: padding === 'normal' ? getPadding(24, true) : undefined,
274+
paddingInline: 'calc(24px - var(--axo-scrollbar-gutter-thin-vertical))',
314275
};
315276
}, [padding]);
316277

@@ -358,17 +319,8 @@ export namespace AxoDialog {
358319
}>;
359320

360321
export const Footer: FC<FooterProps> = memo(props => {
361-
const style = useMemo((): CSSProperties => {
362-
return {
363-
paddingInline: getPadding(12, false),
364-
};
365-
}, []);
366-
367322
return (
368-
<div
369-
className={tw('flex flex-wrap items-center gap-3 py-2.5')}
370-
style={style}
371-
>
323+
<div className={tw('flex flex-wrap items-center gap-3 px-3 py-2.5')}>
372324
{props.children}
373325
</div>
374326
);
@@ -386,12 +338,10 @@ export namespace AxoDialog {
386338
}>;
387339

388340
export const FooterContent: FC<FooterContentProps> = memo(props => {
389-
const style = useMemo(() => {
390-
return { paddingInlineStart: 24 - getPadding(12, false) };
391-
}, []);
392341
return (
393342
<div
394343
className={tw(
344+
'px-3',
395345
// Allow the flex layout to place it in the same row as the actions
396346
// if it can be wrapped to fit within the available space:
397347
'basis-[min-content]',
@@ -402,7 +352,6 @@ export namespace AxoDialog {
402352
'flex-grow',
403353
'type-body-large text-label-primary'
404354
)}
405-
style={style}
406355
>
407356
{props.children}
408357
</div>

ts/axo/AxoProvider.dom.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
// Copyright 2025 Signal Messenger, LLC
22
// SPDX-License-Identifier: AGPL-3.0-only
33
import type { FC, ReactNode } from 'react';
4-
import React, { memo } from 'react';
4+
import React, { memo, useInsertionEffect } from 'react';
55
import { Direction } from 'radix-ui';
6+
import { createScrollbarGutterCssProperties } from './_internal/scrollbars.dom.js';
67

78
type AxoProviderProps = Readonly<{
89
dir: 'ltr' | 'rtl';
910
children: ReactNode;
1011
}>;
1112

13+
let runOnceGlobally = false;
14+
1215
export const AxoProvider: FC<AxoProviderProps> = memo(props => {
16+
useInsertionEffect(() => {
17+
if (runOnceGlobally) {
18+
return;
19+
}
20+
runOnceGlobally = true;
21+
22+
const unsubscribe = createScrollbarGutterCssProperties();
23+
24+
return () => {
25+
unsubscribe();
26+
runOnceGlobally = false;
27+
};
28+
});
1329
return (
1430
<Direction.Provider dir={props.dir}>{props.children}</Direction.Provider>
1531
);

ts/axo/AxoScrollArea.dom.stories.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
// Copyright 2025 Signal Messenger, LLC
22
// SPDX-License-Identifier: AGPL-3.0-only
33
import type { ReactNode } from 'react';
4-
import React, { useMemo } from 'react';
4+
import React from 'react';
55
import type { Meta } from '@storybook/react';
66
import { AxoScrollArea } from './AxoScrollArea.dom.js';
77
import { tw } from './tw.dom.js';
8-
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
98
import { AxoSymbol } from './AxoSymbol.dom.js';
109

1110
export default {
@@ -39,18 +38,9 @@ function VerticalTemplate(props: {
3938
hints?: boolean;
4039
mask?: boolean;
4140
}) {
42-
const paddingInline = useMemo(() => {
43-
return getScrollbarGutters('thin', 'custom').vertical;
44-
}, []);
45-
4641
return (
4742
<div className={tw('w-64 rounded-2xl bg-background-secondary')}>
48-
<h1
49-
className={tw('pt-3 pb-2 type-title-large')}
50-
style={{ paddingInline }}
51-
>
52-
Header
53-
</h1>
43+
<h1 className={tw('px-3 pt-3 pb-2 type-title-large')}>Header</h1>
5444
<div className={tw(props.fit || 'h-100')}>
5545
<AxoScrollArea.Root
5646
scrollbarWidth="thin"
@@ -76,9 +66,7 @@ function VerticalTemplate(props: {
7666
</MaybeMask>
7767
</AxoScrollArea.Root>
7868
</div>
79-
<p className={tw('pt-2 pb-3 type-title-large')} style={{ paddingInline }}>
80-
Footer
81-
</p>
69+
<p className={tw('px-3 pt-2 pb-3 type-title-large')}>Footer</p>
8270
</div>
8371
);
8472
}

0 commit comments

Comments
 (0)