Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 25 additions & 30 deletions packages/vkui/docs/icons-overview/IconsOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
ConfigProvider,
Flex,
Mark,
snackbarManager,
Tooltip,
useSnackbarManager,
} from '../../src';
import { Keys, pressedKey } from '../../src/lib/accessibility';
import { OverviewLayout } from '../common/components/OverviewLayout';
Expand All @@ -28,6 +28,8 @@ const SIZES_OPTIONS: ChipOption[] = ICON_SIZES.map((size) => ({
label: size,
}));

snackbarManager.setLimit(1);

const filterConfig = (config: ConfigData[], query: string, sizes: string[]) => {
if (!query && sizes.length === SIZES_OPTIONS.length) {
return config;
Expand All @@ -52,9 +54,6 @@ const filterConfig = (config: ConfigData[], query: string, sizes: string[]) => {
};

const IconsOverview = () => {
const [snackbarApi, contextHolder] = useSnackbarManager({
limit: 1,
});
const [selectedSizes, setSelectedSizes] = useState<ChipOption[]>(SIZES_OPTIONS);
const rootRef = useRef<HTMLDivElement | null>(null);

Expand All @@ -68,32 +67,29 @@ const IconsOverview = () => {
[selectedSizes],
);

const onIconClick = useCallback(
(iconName: string) => {
const iconCode = `<${iconName} />`;
const onIconClick = useCallback((iconName: string) => {
const iconCode = `<${iconName} />`;

navigator.clipboard
.writeText(iconCode)
.then(() => {
snackbarApi.open({
before: (
<Avatar size={24} style={{ background: 'var(--vkui--color_background_accent)' }}>
<Icon16Done fill="#fff" width={14} height={14} />
</Avatar>
),
children: (
<>
<Mark>{iconCode}</Mark> скопировано!
</>
),
style: { maxInlineSize: 'unset', inlineSize: 'fit-content' },
placement: 'top-end',
});
})
.catch(noop);
},
[snackbarApi],
);
navigator.clipboard
.writeText(iconCode)
.then(() => {
snackbarManager.open({
before: (
<Avatar size={24} style={{ background: 'var(--vkui--color_background_accent)' }}>
<Icon16Done fill="#fff" width={14} height={14} />
</Avatar>
),
children: (
<>
<Mark>{iconCode}</Mark> скопировано!
</>
),
style: { maxInlineSize: 'unset', inlineSize: 'fit-content' },
placement: 'top-end',
});
})
.catch(noop);
}, []);

const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLElement>, iconName: string) => {
Expand Down Expand Up @@ -157,7 +153,6 @@ const IconsOverview = () => {
</Flex>
}
/>
{contextHolder}
<ColorPickerControl containerRef={rootRef} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client';

import * as React from 'react';
import { useIsomorphicLayoutEffect } from '../../../lib/useIsomorphicLayoutEffect';
import {
getSnackbarManagerInternals,
snackbarManager,
type SnackbarManagerConfig,
} from '../snackbarManager';
import type { SnackbarManagerNS } from '../types';
import { SnackbarHolder } from './SnackbarHolder';

export const SnackbarManagerHolder: React.FC<SnackbarManagerNS.HolderProps> = ({
manager = snackbarManager,
limit,
queueStrategy,
offsetYStart,
offsetYEnd,
zIndex,
}) => {
const internals = getSnackbarManagerInternals(manager);

React.useEffect(() => {
internals.registerHolder();
return () => {
internals.unregisterHolder();
};
}, [internals]);

useIsomorphicLayoutEffect(() => {
if (limit !== undefined) {
manager.setLimit(limit);
}
}, [manager, limit]);

useIsomorphicLayoutEffect(() => {
if (queueStrategy !== undefined) {
manager.setQueueStrategy(queueStrategy);
}
}, [manager, queueStrategy]);

useIsomorphicLayoutEffect(() => {
if (offsetYStart !== undefined) {
manager.setOffsetYStart(offsetYStart);
}
}, [manager, offsetYStart]);

useIsomorphicLayoutEffect(() => {
if (offsetYEnd !== undefined) {
manager.setOffsetYEnd(offsetYEnd);
}
}, [manager, offsetYEnd]);

useIsomorphicLayoutEffect(() => {
if (zIndex !== undefined) {
manager.setZIndex(zIndex);
}
}, [manager, zIndex]);

const configStore = React.useSyncExternalStore<SnackbarManagerConfig>(
internals.subscribeConfig,
internals.getConfig,
internals.getConfig,
);

return (
<SnackbarHolder
store={internals.store}
limit={configStore.limit}
offsetYStart={configStore.offsetYStart}
offsetYEnd={configStore.offsetYEnd}
zIndex={configStore.zIndex}
/>
);
};
2 changes: 2 additions & 0 deletions packages/vkui/src/hooks/useSnackbarManager/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DEFAULT_LIMIT = 4;
export const DEFAULT_QUEUE_STRATEGY = 'shift';
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type * as React from 'react';
import { randomUUID } from '../../../lib/randomUUID';
import { SnackbarWrapper } from '../components/SnackbarWrapper';
import {
type CommonOnOpenPayload,
type CustomSnackbar,
type SnackbarApi,
type SnackbarPlacement,
} from '../types';
import type { SnackbarStore } from './createSnackbarStore';

const resolveMobilePlacement = (
placement: SnackbarPlacement,
): Extract<SnackbarPlacement, 'top' | 'bottom-start'> => {
if (placement.startsWith('top')) {
return 'top';
}
return 'bottom-start';
};

const resolveCustomPayload = <AdditionalProps extends object>(
props:
| CustomSnackbar.Payload<AdditionalProps>
| React.ComponentType<CustomSnackbar.Props<AdditionalProps>>,
): CustomSnackbar.Payload<AdditionalProps> => {
if ('component' in props) {
return props;
}
return {
component: props,
};
};

export type SnackbarActionsConfig = {
getLimit: () => number;
getQueueStrategy: () => SnackbarApi.QueueStrategy;
getIsDesktop: () => boolean;
};

export type SnackbarActions = Pick<
SnackbarApi.Api,
'open' | 'openCustom' | 'update' | 'close' | 'closeAll'
>;

export const createSnackbarActions = (
store: SnackbarStore,
config: SnackbarActionsConfig,
): SnackbarActions => {
const update: SnackbarApi.Api['update'] = (id, updateConfig) => {
store.updateSnackbar(id, updateConfig);
};

const close: SnackbarApi.Api['close'] = (id) => {
if (store.showedSnackbars.has(id)) {
store.closeSnackbar(id);
} else {
store.removeSnackbar(id);
}
};

const onOpenSnackbarImpl = (item: CommonOnOpenPayload): SnackbarApi.OpenReturn => {
const placement: SnackbarPlacement = item.snackbarProps?.placement || 'bottom-start';
const resolvedPlacement = config.getIsDesktop() ? placement : resolveMobilePlacement(placement);

const limit = config.getLimit();
const placementSnackbars = store.getSnackbarsByPlacement(resolvedPlacement, limit);

const withOverflow =
config.getQueueStrategy() === 'shift' && placementSnackbars.length >= limit;

let resolvePromise: () => void;
const promise = new Promise<void>((resolve) => {
resolvePromise = resolve;
});

const id = item.id;

if (withOverflow) {
store.closeOverflowedSnackbars(placementSnackbars);
}

store.addSnackbar({
...item,
id,
snackbarProps: {
...item.snackbarProps,
id,
placement: resolvedPlacement,
onClosed: () => {
resolvePromise!();
item.snackbarProps?.onClosed?.();
},
},
});

return {
id,
close: () => close(id),
update: (newProps) => update(id, newProps),
onClose: <R>(resolve?: () => R) => {
return promise.then(resolve);
},
};
};

const openCustom: SnackbarApi.Api['openCustom'] = (openConfig) => {
const resolvedProps = resolveCustomPayload(openConfig);
const id = resolvedProps.id || randomUUID();

return onOpenSnackbarImpl({
id,
component: resolvedProps.component,
snackbarProps: resolvedProps.baseProps,
additionalProps: resolvedProps.additionalProps,
close: () => close(id),
update: (newProps) => update(id, newProps),
});
};

const open: SnackbarApi.Api['open'] = (openConfig) => {
return openCustom({
id: openConfig.id,
component: SnackbarWrapper,
baseProps: openConfig,
});
};

const closeAll: SnackbarApi.Api['closeAll'] = () => {
store.closeAll(store.showedSnackbars);
};

return { open, openCustom, update, close, closeAll };
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import * as React from 'react';
import { MEDIA_QUERIES } from '../../../lib/adaptivity';
import { useDOM } from '../../../lib/dom';
import { useMediaQueries } from '../../useMediaQueries';

/**
* Определяет desktop-режим по окну без хуков.
* Desktop: (smallTabletPlus and pointer: fine) or (smallTabletPlus and mediumHeight)
* Совпадает с логикой useIsDesktop().
*/
export function getIsDesktop(window: Window | null | undefined): boolean {
if (!window) {
return false;
}
// eslint-disable-next-line no-restricted-properties
const smallTabletPlus = window.matchMedia(MEDIA_QUERIES.SMALL_TABLET_PLUS).matches;
// eslint-disable-next-line no-restricted-properties
const mediumHeight = window.matchMedia(MEDIA_QUERIES.MEDIUM_HEIGHT).matches;
// eslint-disable-next-line no-restricted-properties
const pointerFine = window.matchMedia('(pointer: fine)').matches;
return smallTabletPlus && (pointerFine || mediumHeight);
}

/**
* Хук для определения desktop режима.
* Desktop: (smallTabletPlus and pointer: fine) or (smallTabletPlus and mediumHeight)
Expand Down
Loading
Loading