Skip to content

Commit 9cda542

Browse files
committed
fix(scrollbar.style): various
1 parent 87fd543 commit 9cda542

File tree

5 files changed

+218
-104
lines changed

5 files changed

+218
-104
lines changed

.changeset/silly-dragons-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
Apply various fixes to the new scrollbar style.

src/tasty/styles.test.ts

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -115,70 +115,4 @@ describe('Tasty style tests', () => {
115115
'margin-bottom': '1rem',
116116
});
117117
});
118-
119-
it('should return undefined for undefined scrollbar', () => {
120-
expect(scrollbarStyle({})).toBeUndefined();
121-
});
122-
123-
it('should return correct styles for two colors', () => {
124-
expect(scrollbarStyle({ scrollbar: '#dark #clear' })).toEqual({
125-
'scrollbar-color': 'var(--dark-color) var(--clear-color)',
126-
'&::-webkit-scrollbar-corner': {
127-
background: 'var(--clear-color)',
128-
},
129-
});
130-
});
131-
132-
it('should return correct styles for `thin` scrollbar', () => {
133-
expect(scrollbarStyle({ scrollbar: 'thin' })).toEqual({
134-
'scrollbar-width': 'thin',
135-
'scrollbar-color': 'var(--scrollbar-thumb-color) transparent',
136-
});
137-
});
138-
139-
it('should return correct styles for `none` scrollbar', () => {
140-
expect(scrollbarStyle({ scrollbar: 'none' })).toEqual({
141-
'scrollbar-color': 'var(--scrollbar-thumb-color) transparent',
142-
'&::-webkit-scrollbar': {
143-
width: 'none',
144-
height: 'none',
145-
},
146-
});
147-
});
148-
149-
it('should handle custom overflow with scrollbar', () => {
150-
expect(scrollbarStyle({ scrollbar: 'always', overflow: 'scroll' })).toEqual(
151-
{
152-
overflow: 'scroll',
153-
'scrollbar-gutter': 'always',
154-
'scrollbar-color': 'var(--scrollbar-thumb-color) transparent',
155-
},
156-
);
157-
});
158-
159-
it('should handle styled scrollbar', () => {
160-
expect(scrollbarStyle({ scrollbar: 'styled' })).toEqual({
161-
'scrollbar-width': 'thin',
162-
'scrollbar-color': 'var(--scrollbar-thumb-color) transparent',
163-
'&::-webkit-scrollbar': {
164-
width: '8px',
165-
height: '8px',
166-
background: 'var(--scrollbar-track-color)',
167-
transition:
168-
'background var(--transition), border-radius var(--transition), box-shadow var(--transition), width var(--transition), height var(--transition), border var(--transition)',
169-
},
170-
'&::-webkit-scrollbar-thumb': {
171-
background: 'var(--scrollbar-thumb-color)',
172-
borderRadius: '8px',
173-
minHeight: '24px',
174-
transition:
175-
'background var(--transition), border-radius var(--transition), box-shadow var(--transition), width var(--transition), height var(--transition), border var(--transition)',
176-
},
177-
'&::-webkit-scrollbar-corner': {
178-
background: 'var(--scrollbar-track-color)',
179-
transition:
180-
'background var(--transition), border-radius var(--transition), box-shadow var(--transition), width var(--transition), height var(--transition), border var(--transition)',
181-
},
182-
});
183-
});
184118
});

src/tasty/styles/scrollbar.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { scrollbarStyle } from './scrollbar';
2+
3+
describe('scrollbarStyle', () => {
4+
it('returns undefined when scrollbar is not defined', () => {
5+
expect(scrollbarStyle({})).toBeUndefined();
6+
});
7+
8+
it('handles boolean true value as thin', () => {
9+
const result = scrollbarStyle({ scrollbar: true });
10+
expect(result['scrollbar-width']).toBe('thin');
11+
});
12+
13+
it('handles number value as size', () => {
14+
const result = scrollbarStyle({ scrollbar: 10 });
15+
expect(result['&::-webkit-scrollbar']['width']).toBe('10');
16+
expect(result['&::-webkit-scrollbar']['height']).toBe('10');
17+
});
18+
19+
it('handles "none" modifier', () => {
20+
const result = scrollbarStyle({ scrollbar: 'none' });
21+
expect(result['scrollbar-width']).toBe('none');
22+
expect(result['scrollbar-color']).toBe('transparent transparent');
23+
expect(result['&::-webkit-scrollbar']['width']).toBe('0px');
24+
});
25+
26+
it('handles "styled" modifier with proper defaults', () => {
27+
const result = scrollbarStyle({ scrollbar: 'styled' });
28+
expect(result['scrollbar-width']).toBe('thin');
29+
expect(result['&::-webkit-scrollbar']['width']).toBe('8px');
30+
expect(result['&::-webkit-scrollbar-thumb']['border-radius']).toBe('8px');
31+
expect(result['&::-webkit-scrollbar-thumb']['min-height']).toBe('24px');
32+
});
33+
34+
it('handles custom colors', () => {
35+
const result = scrollbarStyle({ scrollbar: '#red #blue #green' });
36+
expect(result['scrollbar-color']).toBe(
37+
'var(--red-color) var(--blue-color)',
38+
);
39+
expect(result['&::-webkit-scrollbar-track']['background']).toBe(
40+
'var(--blue-color)',
41+
);
42+
expect(result['&::-webkit-scrollbar-thumb']['background']).toBe(
43+
'var(--red-color)',
44+
);
45+
expect(result['&::-webkit-scrollbar-corner']['background']).toBe(
46+
'var(--green-color)',
47+
);
48+
});
49+
50+
it('handles "always" modifier with overflow', () => {
51+
const result = scrollbarStyle({ scrollbar: 'always', overflow: 'auto' });
52+
expect(result['overflow']).toBe('auto');
53+
expect(result['scrollbar-gutter']).toBe('stable');
54+
expect(result['&::-webkit-scrollbar']['display']).toBe('block');
55+
});
56+
57+
it('combines modifiers correctly', () => {
58+
const result = scrollbarStyle({ scrollbar: 'thin styled #red' });
59+
expect(result['scrollbar-width']).toBe('thin');
60+
expect(result['scrollbar-color']).toBe(
61+
'var(--red-color) var(--scrollbar-track-color)',
62+
);
63+
expect(result['&::-webkit-scrollbar-thumb']['background']).toBe(
64+
'var(--red-color)',
65+
);
66+
});
67+
68+
it('applies custom colors to styled scrollbars', () => {
69+
const result = scrollbarStyle({
70+
scrollbar: 'styled #purple #dark #light-grey',
71+
});
72+
expect(result['scrollbar-color']).toBe(
73+
'var(--purple-color) var(--dark-color)',
74+
);
75+
expect(result['&::-webkit-scrollbar']['background']).toBe(
76+
'var(--dark-color)',
77+
);
78+
expect(result['&::-webkit-scrollbar-track']['background']).toBe(
79+
'var(--dark-color)',
80+
);
81+
expect(result['&::-webkit-scrollbar-thumb']['background']).toBe(
82+
'var(--purple-color)',
83+
);
84+
expect(result['&::-webkit-scrollbar-corner']['background']).toBe(
85+
'var(--light-grey-color)',
86+
);
87+
});
88+
89+
it('applies partial custom colors with defaults', () => {
90+
const result = scrollbarStyle({ scrollbar: 'styled #danger' });
91+
// Only thumb color specified, track should use default
92+
expect(result['scrollbar-color']).toBe(
93+
'var(--danger-color) var(--scrollbar-track-color)',
94+
);
95+
expect(result['&::-webkit-scrollbar-thumb']['background']).toBe(
96+
'var(--danger-color)',
97+
);
98+
expect(result['&::-webkit-scrollbar-track']['background']).toBe(
99+
'var(--scrollbar-track-color)',
100+
);
101+
});
102+
103+
it('ensures all CSS properties are kebab-cased', () => {
104+
const result = scrollbarStyle({ scrollbar: 'styled thin' });
105+
// Check that camelCase properties are converted to kebab-case
106+
expect(result['&::-webkit-scrollbar-thumb']['border-radius']).toBe('8px');
107+
expect(result['&::-webkit-scrollbar-thumb']['min-height']).toBe('24px');
108+
});
109+
});

src/tasty/styles/scrollbar.ts

Lines changed: 102 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,63 @@
11
import { parseStyle } from '../utils/styles';
22

3-
export function scrollbarStyle({
4-
scrollbar,
5-
overflow,
6-
}: {
3+
interface ScrollbarStyleProps {
74
scrollbar?: string | boolean | number;
85
overflow?: string;
9-
}) {
6+
}
7+
8+
/**
9+
* Creates cross-browser compatible scrollbar styles
10+
*
11+
* Supports both Firefox (scrollbar-width, scrollbar-color) and
12+
* WebKit/Chromium browsers (::-webkit-scrollbar)
13+
*/
14+
export function scrollbarStyle({ scrollbar, overflow }: ScrollbarStyleProps) {
1015
// Check if scrollbar is defined
1116
if (!scrollbar && scrollbar !== 0) return;
1217

1318
// Support true as alias for thin
14-
let value = scrollbar === true ? 'thin' : scrollbar;
19+
const value = scrollbar === true || scrollbar === '' ? 'thin' : scrollbar;
1520
const { mods, colors, values } = parseStyle(String(value));
1621
const style = {};
1722

18-
style['scrollbar-color'] = 'var(--scrollbar-thumb-color) transparent';
23+
// Default colors for scrollbar
24+
const defaultThumbColor = 'var(--scrollbar-thumb-color)';
25+
const defaultTrackColor = 'var(--scrollbar-track-color)';
26+
27+
// Setup default Firefox scrollbar style
28+
style['scrollbar-color'] = `${defaultThumbColor} transparent`;
1929

20-
// Modifiers
30+
// Default scrollbar size
31+
const defaultSize = '8px';
32+
const sizeValue = values[0] || defaultSize;
33+
34+
// Process modifiers
2135
if (mods.includes('thin')) {
2236
style['scrollbar-width'] = 'thin';
23-
}
24-
if (mods.includes('none')) {
37+
} else if (values.includes('none')) {
2538
style['scrollbar-width'] = 'none';
2639
style['scrollbar-color'] = 'transparent transparent';
27-
}
28-
if (mods.includes('auto')) {
40+
// Also hide WebKit scrollbars
41+
style['&::-webkit-scrollbar'] = {
42+
width: '0px',
43+
height: '0px',
44+
display: 'none',
45+
};
46+
47+
return style;
48+
} else if (mods.includes('auto')) {
2949
style['scrollbar-width'] = 'auto';
3050
}
51+
52+
// Handle scrollbar gutter behavior
3153
if (mods.includes('stable') || mods.includes('both-edges')) {
54+
// scrollbar-gutter is supported in newer browsers only
3255
style['scrollbar-gutter'] = mods.includes('both-edges')
3356
? 'stable both-edges'
3457
: 'stable';
3558
}
3659

37-
// Custom size (all values are sizes)
38-
const sizeValue = values[0];
60+
// Custom size setup for WebKit
3961
if (sizeValue) {
4062
style['&::-webkit-scrollbar'] = {
4163
...(style['&::-webkit-scrollbar'] || {}),
@@ -44,25 +66,57 @@ export function scrollbarStyle({
4466
};
4567
}
4668

47-
// Colors (support up to 3: thumb, track, corner)
69+
// Extract colors (support up to 3: thumb, track, corner)
70+
// These will be used in various places throughout the function
71+
const thumbColor = colors && colors[0] ? colors[0] : defaultThumbColor;
72+
const trackColor = colors && colors[1] ? colors[1] : defaultTrackColor;
73+
const cornerColor = colors && colors[2] ? colors[2] : trackColor;
74+
75+
// Apply colors if they are specified
4876
if (colors && colors.length) {
49-
const thumb = colors[0] || 'var(--scrollbar-thumb-color)';
50-
const track = colors[1] || 'var(--scrollbar-track-color)';
51-
const corner = colors[2] || track;
52-
style['scrollbar-color'] = `${thumb} ${track}`;
53-
if (!style['&::-webkit-scrollbar-corner']) {
54-
style['&::-webkit-scrollbar-corner'] = {};
77+
// Firefox
78+
style['scrollbar-color'] = `${thumbColor} ${trackColor}`;
79+
80+
// WebKit - always set these for consistency
81+
if (!style['&::-webkit-scrollbar']) {
82+
style['&::-webkit-scrollbar'] = {};
5583
}
56-
style['&::-webkit-scrollbar-corner'].background = corner;
84+
style['&::-webkit-scrollbar']['background'] = trackColor;
85+
86+
style['&::-webkit-scrollbar-track'] = {
87+
...(style['&::-webkit-scrollbar-track'] || {}),
88+
background: trackColor,
89+
};
90+
91+
style['&::-webkit-scrollbar-thumb'] = {
92+
...(style['&::-webkit-scrollbar-thumb'] || {}),
93+
background: thumbColor,
94+
};
95+
96+
style['&::-webkit-scrollbar-corner'] = {
97+
...(style['&::-webkit-scrollbar-corner'] || {}),
98+
background: cornerColor,
99+
};
57100
}
58101

59-
// always: force scrollbars to show (requires overflow)
102+
// Handle 'always' mode: force scrollbars to show
60103
if (mods.includes('always')) {
61104
style['overflow'] = overflow || 'scroll';
62-
style['scrollbar-gutter'] = style['scrollbar-gutter'] || 'always';
105+
106+
// Use auto for WebKit browsers since they don't support 'always'
107+
// This is closer to the expected behavior
108+
if (!style['scrollbar-gutter']) {
109+
style['scrollbar-gutter'] = 'stable';
110+
}
111+
112+
// Ensure scrollbars appear in WebKit even with little content
113+
if (!style['&::-webkit-scrollbar']) {
114+
style['&::-webkit-scrollbar'] = {};
115+
}
116+
style['&::-webkit-scrollbar']['display'] = 'block';
63117
}
64118

65-
// Legacy styled mod
119+
// Enhanced 'styled' mode with better transitions and appearance
66120
if (mods.includes('styled')) {
67121
const baseTransition = [
68122
'background var(--transition)',
@@ -72,27 +126,39 @@ export function scrollbarStyle({
72126
'height var(--transition)',
73127
'border var(--transition)',
74128
].join(', ');
129+
130+
// Firefox
75131
style['scrollbar-width'] = style['scrollbar-width'] || 'thin';
76132
style['scrollbar-color'] =
77-
style['scrollbar-color'] ||
78-
'var(--scrollbar-thumb-color) var(--scrollbar-track-color)';
133+
style['scrollbar-color'] || `${defaultThumbColor} ${defaultTrackColor}`;
134+
135+
// WebKit
79136
style['&::-webkit-scrollbar'] = {
80-
...(style['&::-webkit-scrollbar'] || {}),
81-
width: sizeValue || '8px',
82-
height: sizeValue || '8px',
83-
background: 'var(--scrollbar-track-color)',
137+
width: sizeValue,
138+
height: sizeValue,
84139
transition: baseTransition,
140+
background: defaultTrackColor,
141+
...(style['&::-webkit-scrollbar'] || {}),
85142
};
143+
86144
style['&::-webkit-scrollbar-thumb'] = {
87-
background: 'var(--scrollbar-thumb-color)',
88-
borderRadius: '8px',
89-
minHeight: '24px',
145+
'border-radius': '8px',
146+
'min-height': '24px',
90147
transition: baseTransition,
148+
background: defaultThumbColor,
149+
...(style['&::-webkit-scrollbar-thumb'] || {}),
91150
};
151+
152+
style['&::-webkit-scrollbar-track'] = {
153+
background: defaultTrackColor,
154+
transition: baseTransition,
155+
...(style['&::-webkit-scrollbar-track'] || {}),
156+
};
157+
92158
style['&::-webkit-scrollbar-corner'] = {
93-
...(style['&::-webkit-scrollbar-corner'] || {}),
94-
background: 'var(--scrollbar-track-color)',
159+
background: defaultTrackColor,
95160
transition: baseTransition,
161+
...(style['&::-webkit-scrollbar-corner'] || {}),
96162
};
97163
}
98164

src/tasty/utils/styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -590,8 +590,8 @@ export function hexToRgb(hex) {
590590
.match(/.{2}/g)
591591
.map((x, i) => parseInt(x, 16) * (i === 3 ? 1 / 255 : 1));
592592

593-
if (Number.isNaN(rgba[0])) {
594-
return 'rgb(0 0 0 / 1)';
593+
if (rgba.some((v) => Number.isNaN(v))) {
594+
return null;
595595
}
596596

597597
if (rgba.length >= 3) {

0 commit comments

Comments
 (0)