Skip to content

Commit d4cad26

Browse files
feat(useSnackbarManager): imperative snackbar API
1 parent dd31fd3 commit d4cad26

6 files changed

Lines changed: 72 additions & 92 deletions

File tree

packages/vkui/docs/icons-overview/IconsOverview.tsx

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
ConfigProvider,
1414
Flex,
1515
Mark,
16+
snackbarManager,
1617
Tooltip,
17-
useSnackbarManager,
1818
} from '../../src';
1919
import { Keys, pressedKey } from '../../src/lib/accessibility';
2020
import { OverviewLayout } from '../common/components/OverviewLayout';
@@ -28,6 +28,8 @@ const SIZES_OPTIONS: ChipOption[] = ICON_SIZES.map((size) => ({
2828
label: size,
2929
}));
3030

31+
snackbarManager.setLimit(1);
32+
3133
const filterConfig = (config: ConfigData[], query: string, sizes: string[]) => {
3234
if (!query && sizes.length === SIZES_OPTIONS.length) {
3335
return config;
@@ -52,9 +54,6 @@ const filterConfig = (config: ConfigData[], query: string, sizes: string[]) => {
5254
};
5355

5456
const IconsOverview = () => {
55-
const [snackbarApi, contextHolder] = useSnackbarManager({
56-
limit: 1,
57-
});
5857
const [selectedSizes, setSelectedSizes] = useState<ChipOption[]>(SIZES_OPTIONS);
5958
const rootRef = useRef<HTMLDivElement | null>(null);
6059

@@ -68,32 +67,29 @@ const IconsOverview = () => {
6867
[selectedSizes],
6968
);
7069

71-
const onIconClick = useCallback(
72-
(iconName: string) => {
73-
const iconCode = `<${iconName} />`;
70+
const onIconClick = useCallback((iconName: string) => {
71+
const iconCode = `<${iconName} />`;
7472

75-
navigator.clipboard
76-
.writeText(iconCode)
77-
.then(() => {
78-
snackbarApi.open({
79-
before: (
80-
<Avatar size={24} style={{ background: 'var(--vkui--color_background_accent)' }}>
81-
<Icon16Done fill="#fff" width={14} height={14} />
82-
</Avatar>
83-
),
84-
children: (
85-
<>
86-
<Mark>{iconCode}</Mark> скопировано!
87-
</>
88-
),
89-
style: { maxInlineSize: 'unset', inlineSize: 'fit-content' },
90-
placement: 'top-end',
91-
});
92-
})
93-
.catch(noop);
94-
},
95-
[snackbarApi],
96-
);
73+
navigator.clipboard
74+
.writeText(iconCode)
75+
.then(() => {
76+
snackbarManager.open({
77+
before: (
78+
<Avatar size={24} style={{ background: 'var(--vkui--color_background_accent)' }}>
79+
<Icon16Done fill="#fff" width={14} height={14} />
80+
</Avatar>
81+
),
82+
children: (
83+
<>
84+
<Mark>{iconCode}</Mark> скопировано!
85+
</>
86+
),
87+
style: { maxInlineSize: 'unset', inlineSize: 'fit-content' },
88+
placement: 'top-end',
89+
});
90+
})
91+
.catch(noop);
92+
}, []);
9793

9894
const onKeyDown = useCallback(
9995
(e: KeyboardEvent<HTMLElement>, iconName: string) => {
@@ -157,7 +153,6 @@ const IconsOverview = () => {
157153
</Flex>
158154
}
159155
/>
160-
{contextHolder}
161156
<ColorPickerControl containerRef={rootRef} />
162157
</div>
163158
);

packages/vkui/src/hooks/useSnackbarManager/components/SnackbarManagerHolder.tsx

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import * as React from 'react';
44
import { useIsomorphicLayoutEffect } from '../../../lib/useIsomorphicLayoutEffect';
5-
import { useIsDesktop } from '../helpers/useIsDesktop';
65
import {
76
getSnackbarManagerInternals,
87
snackbarManager,
@@ -28,12 +27,6 @@ export const SnackbarManagerHolder: React.FC<SnackbarManagerNS.HolderProps> = ({
2827
};
2928
}, [internals]);
3029

31-
const isDesktop = useIsDesktop();
32-
33-
useIsomorphicLayoutEffect(() => {
34-
internals.setIsDesktop(isDesktop);
35-
}, [internals, isDesktop]);
36-
3730
useIsomorphicLayoutEffect(() => {
3831
if (limit !== undefined) {
3932
manager.setLimit(limit);
@@ -64,7 +57,7 @@ export const SnackbarManagerHolder: React.FC<SnackbarManagerNS.HolderProps> = ({
6457
}
6558
}, [manager, zIndex]);
6659

67-
const config = React.useSyncExternalStore<SnackbarManagerConfig>(
60+
const configStore = React.useSyncExternalStore<SnackbarManagerConfig>(
6861
internals.subscribeConfig,
6962
internals.getConfig,
7063
internals.getConfig,
@@ -73,10 +66,10 @@ export const SnackbarManagerHolder: React.FC<SnackbarManagerNS.HolderProps> = ({
7366
return (
7467
<SnackbarHolder
7568
store={internals.store}
76-
limit={config.limit}
77-
offsetYStart={config.offsetYStart}
78-
offsetYEnd={config.offsetYEnd}
79-
zIndex={config.zIndex}
69+
limit={configStore.limit}
70+
offsetYStart={configStore.offsetYStart}
71+
offsetYEnd={configStore.offsetYEnd}
72+
zIndex={configStore.zIndex}
8073
/>
8174
);
8275
};

packages/vkui/src/hooks/useSnackbarManager/helpers/useIsDesktop.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
11
import * as React from 'react';
2+
import { MEDIA_QUERIES } from '../../../lib/adaptivity';
23
import { useDOM } from '../../../lib/dom';
34
import { matchMediaListAddListener, matchMediaListRemoveListener } from '../../../lib/matchMedia';
45
import { useMediaQueries } from '../../useMediaQueries';
56

7+
/**
8+
* Определяет desktop-режим по окну без хуков.
9+
* Desktop: (smallTabletPlus and pointer: fine) or (smallTabletPlus and mediumHeight)
10+
* Совпадает с логикой useIsDesktop().
11+
*/
12+
export function getIsDesktop(window: Window | null | undefined): boolean {
13+
if (!window) {
14+
return false;
15+
}
16+
// eslint-disable-next-line no-restricted-properties
17+
const smallTabletPlus = window.matchMedia(MEDIA_QUERIES.SMALL_TABLET_PLUS).matches;
18+
// eslint-disable-next-line no-restricted-properties
19+
const mediumHeight = window.matchMedia(MEDIA_QUERIES.MEDIUM_HEIGHT).matches;
20+
// eslint-disable-next-line no-restricted-properties
21+
const pointerFine = window.matchMedia('(pointer: fine)').matches;
22+
return smallTabletPlus && (pointerFine || mediumHeight);
23+
}
24+
625
/**
726
* Хук для определения desktop режима.
827
* Desktop: (smallTabletPlus and pointer: fine) or (smallTabletPlus and mediumHeight)

packages/vkui/src/hooks/useSnackbarManager/snackbarManager.ts

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getDOM } from '../../lib/dom';
44
import { DEFAULT_LIMIT, DEFAULT_QUEUE_STRATEGY } from './constants';
55
import { createSnackbarActions } from './helpers/createSnackbarActions';
66
import { createSnackbarStore, type SnackbarStore } from './helpers/createSnackbarStore';
7+
import { getIsDesktop } from './helpers/useIsDesktop';
78
import type { SnackbarApi, SnackbarManagerNS } from './types';
89

910
export type SnackbarManagerConfig = {
@@ -18,7 +19,6 @@ type SnackbarManagerInternals = {
1819
store: SnackbarStore;
1920
getConfig: () => SnackbarManagerConfig;
2021
subscribeConfig: (listener: () => void) => () => void;
21-
setIsDesktop: (isDesktop: boolean) => void;
2222
registerHolder: () => void;
2323
unregisterHolder: () => void;
2424
setMountCallback: (callback: (() => void) | null) => void;
@@ -44,7 +44,7 @@ export function createSnackbarManager(
4444
): SnackbarManagerNS.Instance {
4545
const store = createSnackbarStore();
4646

47-
const { document } = getDOM();
47+
const { document, window } = getDOM();
4848

4949
let config: SnackbarManagerConfig = {
5050
limit: options.limit ?? DEFAULT_LIMIT,
@@ -54,8 +54,6 @@ export function createSnackbarManager(
5454
zIndex: options.zIndex,
5555
};
5656

57-
let isDesktop = false;
58-
5957
const configListeners = new Set<() => void>();
6058

6159
const notifyConfig = () => {
@@ -65,7 +63,7 @@ export function createSnackbarManager(
6563
const actions = createSnackbarActions(store, {
6664
getLimit: () => config.limit,
6765
getQueueStrategy: () => config.queueStrategy,
68-
getIsDesktop: () => isDesktop,
66+
getIsDesktop: () => getIsDesktop(window),
6967
});
7068

7169
let holderCount = 0;
@@ -85,6 +83,11 @@ export function createSnackbarManager(
8583
}
8684
};
8785

86+
const updateConfig = (newConfig: SnackbarManagerConfig) => {
87+
config = newConfig;
88+
notifyConfig();
89+
};
90+
8891
const instance: SnackbarManagerNS.Instance = {
8992
open: (config) => {
9093
ensureHolderMounted();
@@ -97,26 +100,11 @@ export function createSnackbarManager(
97100
update: actions.update,
98101
close: actions.close,
99102
closeAll: actions.closeAll,
100-
setLimit: (count) => {
101-
config = { ...config, limit: count };
102-
notifyConfig();
103-
},
104-
setQueueStrategy: (strategy) => {
105-
config = { ...config, queueStrategy: strategy };
106-
notifyConfig();
107-
},
108-
setOffsetYStart: (offset) => {
109-
config = { ...config, offsetYStart: offset };
110-
notifyConfig();
111-
},
112-
setOffsetYEnd: (offset) => {
113-
config = { ...config, offsetYEnd: offset };
114-
notifyConfig();
115-
},
116-
setZIndex: (z) => {
117-
config = { ...config, zIndex: z };
118-
notifyConfig();
119-
},
103+
setLimit: (count) => updateConfig({ ...config, limit: count }),
104+
setQueueStrategy: (strategy) => updateConfig({ ...config, queueStrategy: strategy }),
105+
setOffsetYStart: (offset) => updateConfig({ ...config, offsetYStart: offset }),
106+
setOffsetYEnd: (offset) => updateConfig({ ...config, offsetYEnd: offset }),
107+
setZIndex: (z) => updateConfig({ ...config, zIndex: z }),
120108
};
121109

122110
internalsMap.set(instance, {
@@ -126,9 +114,6 @@ export function createSnackbarManager(
126114
configListeners.add(listener);
127115
return () => configListeners.delete(listener);
128116
},
129-
setIsDesktop: (value) => {
130-
isDesktop = value;
131-
},
132117
registerHolder: () => {
133118
holderCount += 1;
134119
},

website/components/mdx/Playground/PlaygroundToolbar/PlatformPicker/PlatformPicker.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { Icon28ErrorCircleOutline } from '@vkontakte/icons';
3-
import { type PlatformType, SegmentedControl, Snackbar } from '@vkontakte/vkui';
3+
import { type PlatformType, SegmentedControl, snackbarManager } from '@vkontakte/vkui';
44
import { PlaygroundStoreContext, usePlaygroundStore } from '@/providers/playgroundStoreProvider';
55
import { DEFAULT_THEME_FOR_PLATFORM, DEFAULT_THEME_NAMES } from '../../vkuiThemes/constants';
66
import { getDefaultByThemesPresets, loadTheme } from '../../vkuiThemes/helpers';
@@ -10,7 +10,6 @@ export function PlatformPicker({ className }: { className?: string }) {
1010
const playgroundLoading = usePlaygroundStore((store) => store.playgroundLoading);
1111
const platform = usePlaygroundStore((store) => store.platform);
1212
const store = React.useContext(PlaygroundStoreContext);
13-
const [snackbar, setSnackbar] = React.useState<React.ReactElement | null>(null);
1413

1514
const handlePlatformChange = async (newPlatform: PlatformType) => {
1615
if (store) {
@@ -32,14 +31,10 @@ export function PlatformPicker({ className }: { className?: string }) {
3231
} catch (error) {
3332
// eslint-disable-next-line no-console
3433
console.warn(error);
35-
setSnackbar(
36-
<Snackbar
37-
onClosed={() => setSnackbar(null)}
38-
before={<Icon28ErrorCircleOutline fill="var(--vkui--color_icon_negative)" />}
39-
>
40-
{`Не удалось загрузить токены для темы ${newThemeName}`}
41-
</Snackbar>,
42-
);
34+
snackbarManager.open({
35+
before: <Icon28ErrorCircleOutline fill="var(--vkui--color_icon_negative)" />,
36+
children: `Не удалось загрузить токены для темы ${newThemeName}`,
37+
});
4338
} finally {
4439
updatePlaygroundLoading(false);
4540
}
@@ -51,7 +46,6 @@ export function PlatformPicker({ className }: { className?: string }) {
5146

5247
return (
5348
<>
54-
{snackbar}
5549
<SegmentedControl
5650
size="m"
5751
value={platform}

website/components/mdx/Playground/PlaygroundToolbar/ThemePicker/ThemesModal.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
ModalPageHeader,
1010
PanelSpinner,
1111
SimpleCell,
12-
Snackbar,
12+
snackbarManager,
1313
VisuallyHidden,
1414
} from '@vkontakte/vkui';
1515
import { Callout, Code } from '@vkontakte/vkui-docs-theme';
@@ -40,7 +40,6 @@ export function ThemesModal({ open, setOpen }: ThemesModalProps) {
4040
function ThemesModalInner({ setOpen }: Pick<ThemesModalProps, 'setOpen'>) {
4141
const store = React.useContext(PlaygroundStoreContext);
4242
const { themeNames, isLoading, error } = useLoadThemeNames();
43-
const [snackbar, setSnackbar] = React.useState<React.ReactElement | null>(null);
4443

4544
const handleThemeSelect = async (
4645
themeName: ThemeDefinitionProps['themeName'],
@@ -71,14 +70,10 @@ function ThemesModalInner({ setOpen }: Pick<ThemesModalProps, 'setOpen'>) {
7170
} catch (error) {
7271
// eslint-disable-next-line no-console
7372
console.warn(error);
74-
setSnackbar(
75-
<Snackbar
76-
onClosed={() => setSnackbar(null)}
77-
before={<Icon28ErrorCircleOutline fill="var(--vkui--color_icon_negative)" />}
78-
>
79-
{`Не удалось загрузить токены для темы ${themeName}`}
80-
</Snackbar>,
81-
);
73+
snackbarManager.open({
74+
before: <Icon28ErrorCircleOutline fill="var(--vkui--color_icon_negative)" />,
75+
children: `Не удалось загрузить токены для темы ${themeName}`,
76+
});
8277
} finally {
8378
updatePlaygroundLoading(false);
8479
}
@@ -87,7 +82,6 @@ function ThemesModalInner({ setOpen }: Pick<ThemesModalProps, 'setOpen'>) {
8782

8883
return (
8984
<>
90-
{snackbar}
9185
<Div>
9286
Ниже представлены все темы из{' '}
9387
<Link href="https://github.com/VKCOM/vkui-tokens" target="_blank" rel="noreferrer">

0 commit comments

Comments
 (0)