Skip to content

Commit c81534b

Browse files
authored
feat!: userSettings for package (#3243)
1 parent 29a3dc4 commit c81534b

File tree

26 files changed

+1149
-246
lines changed

26 files changed

+1149
-246
lines changed

src/components/Drawer/Drawer.tsx

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,25 @@ import React from 'react';
33
import {Xmark} from '@gravity-ui/icons';
44
import {DrawerItem, Drawer as GravityDrawer} from '@gravity-ui/navigation';
55
import {ActionTooltip, Button, Flex, Icon, Text} from '@gravity-ui/uikit';
6+
import {debounce} from 'lodash';
67

78
import {cn} from '../../utils/cn';
8-
import {isNumeric} from '../../utils/utils';
9+
import {useSetting} from '../../utils/hooks/useSetting';
910
import {CopyLinkButton} from '../CopyLinkButton/CopyLinkButton';
1011
import {Portal} from '../Portal/Portal';
1112

1213
import {useDrawerContext} from './DrawerContext';
14+
import {
15+
normalizeDrawerWidthFromResize,
16+
normalizeDrawerWidthFromSavedString,
17+
} from './DrawerWidthUtils';
1318

1419
import './Drawer.scss';
1520

1621
const DEFAULT_DRAWER_WIDTH_PERCENTS = 60;
1722
const DEFAULT_DRAWER_WIDTH = 600;
1823
const DRAWER_WIDTH_KEY = 'drawer-width';
24+
const SAVE_DEBOUNCE_MS = 200;
1925
const b = cn('ydb-drawer');
2026

2127
type DrawerEvent = MouseEvent & {
@@ -49,13 +55,24 @@ const DrawerPaneContentWrapper = ({
4955
isPercentageWidth,
5056
hideVeil = true,
5157
}: DrawerPaneContentWrapperProps) => {
52-
const [drawerWidth, setDrawerWidth] = React.useState(() => {
53-
const savedWidth = localStorage.getItem(storageKey);
54-
return isNumeric(savedWidth) ? Number(savedWidth) : defaultWidth;
55-
});
58+
const [savedWidthString, setSavedWidthString] = useSetting<string | undefined>(storageKey);
59+
const [userDrawerWidth, setUserDrawerWidth] = React.useState<number | undefined>(undefined);
5660

5761
const drawerRef = React.useRef<HTMLDivElement>(null);
5862
const {containerWidth, itemContainerRef} = useDrawerContext();
63+
64+
const derivedDrawerWidth = React.useMemo(() => {
65+
return normalizeDrawerWidthFromSavedString({
66+
savedWidthString,
67+
defaultWidth,
68+
isPercentageWidth,
69+
containerWidth,
70+
defaultPercents: DEFAULT_DRAWER_WIDTH_PERCENTS,
71+
defaultPx: DEFAULT_DRAWER_WIDTH,
72+
});
73+
}, [containerWidth, defaultWidth, isPercentageWidth, savedWidthString]);
74+
75+
const drawerWidth = userDrawerWidth ?? derivedDrawerWidth;
5976
// Calculate drawer width based on container width percentage if specified
6077
const calculatedWidth = React.useMemo(() => {
6178
if (isPercentageWidth && containerWidth > 0) {
@@ -91,19 +108,30 @@ const DrawerPaneContentWrapper = ({
91108
};
92109
}, [isVisible, onClose, detectClickOutside]);
93110

111+
const saveWidthDebounced = React.useMemo(() => {
112+
return debounce((value: string) => setSavedWidthString(value), SAVE_DEBOUNCE_MS);
113+
}, [setSavedWidthString]);
114+
115+
React.useEffect(() => {
116+
return () => {
117+
saveWidthDebounced.cancel();
118+
};
119+
}, [saveWidthDebounced]);
120+
94121
const handleResizeDrawer = (width: number) => {
95-
if (isPercentageWidth && containerWidth > 0) {
96-
const percentageWidth = Math.round((width / containerWidth) * 100);
97-
setDrawerWidth(percentageWidth);
98-
localStorage.setItem(storageKey, percentageWidth.toString());
99-
} else {
100-
setDrawerWidth(width);
101-
localStorage.setItem(storageKey, width.toString());
102-
}
122+
const normalized = normalizeDrawerWidthFromResize({
123+
resizedWidthPx: width,
124+
isPercentageWidth,
125+
containerWidth,
126+
});
127+
128+
setUserDrawerWidth(normalized.drawerWidth);
129+
saveWidthDebounced(normalized.savedWidthString);
103130
};
104131

105132
const handleClickInsideDrawer = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
106-
(event.nativeEvent as DrawerEvent)._capturedInsideDrawer = true;
133+
const nativeEvent = event.nativeEvent as DrawerEvent;
134+
nativeEvent._capturedInsideDrawer = true;
107135
};
108136

109137
const itemContainer = itemContainerRef?.current;
@@ -124,7 +152,7 @@ const DrawerPaneContentWrapper = ({
124152
visible={isVisible}
125153
resizable
126154
maxResizeWidth={containerWidth}
127-
width={isPercentageWidth ? calculatedWidth : drawerWidth}
155+
width={calculatedWidth}
128156
onResize={handleResizeDrawer}
129157
direction={direction}
130158
className={b('item')}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {isNumeric} from '../../utils/utils';
2+
3+
export const MIN_DRAWER_WIDTH_PERCENTS = 1;
4+
export const MAX_DRAWER_WIDTH_PERCENTS = 100;
5+
export const MIN_DRAWER_WIDTH_PX = 1;
6+
7+
function clamp(value: number, min: number, max: number) {
8+
return Math.min(max, Math.max(min, value));
9+
}
10+
11+
function getFallbackWidth(args: {
12+
defaultWidth?: number;
13+
isPercentageWidth?: boolean;
14+
defaultPercents: number;
15+
defaultPx: number;
16+
}) {
17+
const {defaultWidth, isPercentageWidth, defaultPercents, defaultPx} = args;
18+
if (defaultWidth !== undefined) {
19+
return defaultWidth;
20+
}
21+
return isPercentageWidth ? defaultPercents : defaultPx;
22+
}
23+
24+
export function normalizeDrawerWidthFromSavedString(args: {
25+
savedWidthString: string | undefined;
26+
defaultWidth?: number;
27+
isPercentageWidth?: boolean;
28+
containerWidth: number;
29+
defaultPercents: number;
30+
defaultPx: number;
31+
}) {
32+
const {
33+
savedWidthString,
34+
defaultWidth,
35+
isPercentageWidth,
36+
containerWidth,
37+
defaultPercents,
38+
defaultPx,
39+
} = args;
40+
41+
const fallback = getFallbackWidth({
42+
defaultWidth,
43+
isPercentageWidth,
44+
defaultPercents,
45+
defaultPx,
46+
});
47+
48+
if (!isNumeric(savedWidthString)) {
49+
return fallback;
50+
}
51+
52+
const raw = Number(savedWidthString);
53+
if (!Number.isFinite(raw)) {
54+
return fallback;
55+
}
56+
57+
if (isPercentageWidth) {
58+
return clamp(raw, MIN_DRAWER_WIDTH_PERCENTS, MAX_DRAWER_WIDTH_PERCENTS);
59+
}
60+
61+
if (raw < MIN_DRAWER_WIDTH_PX) {
62+
return fallback;
63+
}
64+
65+
if (containerWidth > 0) {
66+
return Math.min(raw, containerWidth);
67+
}
68+
69+
return raw;
70+
}
71+
72+
export function normalizeDrawerWidthFromResize(args: {
73+
resizedWidthPx: number;
74+
isPercentageWidth?: boolean;
75+
containerWidth: number;
76+
}) {
77+
const {resizedWidthPx, isPercentageWidth, containerWidth} = args;
78+
79+
if (isPercentageWidth && containerWidth > 0) {
80+
const percentageWidthRaw = Math.round((resizedWidthPx / containerWidth) * 100);
81+
const percentageWidth = clamp(
82+
percentageWidthRaw,
83+
MIN_DRAWER_WIDTH_PERCENTS,
84+
MAX_DRAWER_WIDTH_PERCENTS,
85+
);
86+
87+
return {
88+
drawerWidth: percentageWidth,
89+
savedWidthString: percentageWidth.toString(),
90+
} as const;
91+
}
92+
93+
const cappedWidth =
94+
containerWidth > 0 ? Math.min(resizedWidthPx, containerWidth) : resizedWidthPx;
95+
const safeWidth = Math.max(MIN_DRAWER_WIDTH_PX, cappedWidth);
96+
97+
return {drawerWidth: safeWidth, savedWidthString: safeWidth.toString()} as const;
98+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
MAX_DRAWER_WIDTH_PERCENTS,
3+
MIN_DRAWER_WIDTH_PERCENTS,
4+
MIN_DRAWER_WIDTH_PX,
5+
normalizeDrawerWidthFromResize,
6+
normalizeDrawerWidthFromSavedString,
7+
} from '../DrawerWidthUtils';
8+
9+
describe('DrawerWidthUtils', () => {
10+
describe('normalizeDrawerWidthFromSavedString', () => {
11+
it('falls back when savedWidthString is missing/invalid', () => {
12+
expect(
13+
normalizeDrawerWidthFromSavedString({
14+
savedWidthString: undefined,
15+
defaultWidth: 123,
16+
isPercentageWidth: false,
17+
containerWidth: 0,
18+
defaultPercents: 60,
19+
defaultPx: 600,
20+
}),
21+
).toBe(123);
22+
23+
expect(
24+
normalizeDrawerWidthFromSavedString({
25+
savedWidthString: 'abc',
26+
defaultWidth: undefined,
27+
isPercentageWidth: true,
28+
containerWidth: 0,
29+
defaultPercents: 60,
30+
defaultPx: 600,
31+
}),
32+
).toBe(60);
33+
});
34+
35+
it('clamps percentage width to [1..100]', () => {
36+
expect(
37+
normalizeDrawerWidthFromSavedString({
38+
savedWidthString: '-10',
39+
defaultWidth: undefined,
40+
isPercentageWidth: true,
41+
containerWidth: 0,
42+
defaultPercents: 60,
43+
defaultPx: 600,
44+
}),
45+
).toBe(MIN_DRAWER_WIDTH_PERCENTS);
46+
47+
expect(
48+
normalizeDrawerWidthFromSavedString({
49+
savedWidthString: '1000',
50+
defaultWidth: undefined,
51+
isPercentageWidth: true,
52+
containerWidth: 0,
53+
defaultPercents: 60,
54+
defaultPx: 600,
55+
}),
56+
).toBe(MAX_DRAWER_WIDTH_PERCENTS);
57+
});
58+
59+
it('enforces px width >= 1 and caps to containerWidth when provided', () => {
60+
expect(
61+
normalizeDrawerWidthFromSavedString({
62+
savedWidthString: '0',
63+
defaultWidth: 200,
64+
isPercentageWidth: false,
65+
containerWidth: 0,
66+
defaultPercents: 60,
67+
defaultPx: 600,
68+
}),
69+
).toBe(200);
70+
71+
expect(
72+
normalizeDrawerWidthFromSavedString({
73+
savedWidthString: '500',
74+
defaultWidth: undefined,
75+
isPercentageWidth: false,
76+
containerWidth: 300,
77+
defaultPercents: 60,
78+
defaultPx: 600,
79+
}),
80+
).toBe(300);
81+
});
82+
});
83+
84+
describe('normalizeDrawerWidthFromResize', () => {
85+
it('returns percent width when isPercentageWidth and containerWidth > 0', () => {
86+
const normalized = normalizeDrawerWidthFromResize({
87+
resizedWidthPx: 240,
88+
isPercentageWidth: true,
89+
containerWidth: 400,
90+
});
91+
92+
expect(normalized.drawerWidth).toBe(60);
93+
expect(normalized.savedWidthString).toBe('60');
94+
});
95+
96+
it('clamps percent width to [1..100]', () => {
97+
const normalized = normalizeDrawerWidthFromResize({
98+
resizedWidthPx: 10_000,
99+
isPercentageWidth: true,
100+
containerWidth: 100,
101+
});
102+
103+
expect(normalized.drawerWidth).toBe(MAX_DRAWER_WIDTH_PERCENTS);
104+
expect(normalized.savedWidthString).toBe(String(MAX_DRAWER_WIDTH_PERCENTS));
105+
});
106+
107+
it('caps px width to containerWidth (when provided) and enforces >= 1px', () => {
108+
const normalized = normalizeDrawerWidthFromResize({
109+
resizedWidthPx: 0,
110+
isPercentageWidth: false,
111+
containerWidth: 0,
112+
});
113+
expect(normalized.drawerWidth).toBe(MIN_DRAWER_WIDTH_PX);
114+
expect(normalized.savedWidthString).toBe(String(MIN_DRAWER_WIDTH_PX));
115+
116+
const capped = normalizeDrawerWidthFromResize({
117+
resizedWidthPx: 500,
118+
isPercentageWidth: false,
119+
containerWidth: 300,
120+
});
121+
expect(capped.drawerWidth).toBe(300);
122+
expect(capped.savedWidthString).toBe('300');
123+
});
124+
});
125+
});

0 commit comments

Comments
 (0)