Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: ModalRoot/ModalPage/ModalCard #6759

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion packages/vkui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"test:e2e-update": "../../scripts/generate_env_docker.sh -u && docker compose --env-file=./.env.docker up --abort-on-container-exit",
"test:e2e:ci": "yarn run -T playwright test --config playwright-ct.config.ts",
"test:e2e-update:ci": "yarn run test:e2e:ci --update-snapshots",
"lint:generated-files": "yarn run -T tsc scripts/generateCSSCustomMedias.mjs --checkJs --module ESNext --moduleResolution node --resolveJsonModule --allowSyntheticDefaultImports --noEmit && yarn run generate:css-custom-medias && git diff --exit-code src/styles/customMedias.generated.css",
"lint:generated-files": "yarn run -T tsc scripts/generateCSSCustomMedias.mjs --checkJs --module ESNext --moduleResolution node --resolveJsonModule --allowSyntheticDefaultImports --jsx react-jsx --noEmit && yarn run generate:css-custom-medias && git diff --exit-code src/styles/customMedias.generated.css",
"storybook": "bash -c 'source .env && yarn run -T cross-env SANDBOX=\\.storybook storybook dev -p ${STORYBOOK_DEV_PORT:=6006}'",
"storybook:build": "yarn run -T cross-env SANDBOX=\\.storybook FROM_STORYBOOK=1 storybook build",
"generate:css-custom-medias": "node scripts/generateCSSCustomMedias.mjs"
Expand Down
12 changes: 3 additions & 9 deletions packages/vkui/src/components/Group/Group.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react';
import { classNames, noop } from '@vkontakte/vkjs';
import { ModalContext } from '../../context/ModalContext';
import type { SizeTypeValues } from '../../lib/adaptivity';
import { baselineComponent } from '../../testing/utils';
import { AdaptivityContext } from '../AdaptivityProvider/AdaptivityContext';
Expand All @@ -8,7 +9,6 @@ import {
type AppRootContextInterface,
DEFAULT_APP_ROOT_CONTEXT_VALUE,
} from '../AppRoot/AppRootContext';
import { ModalRootContext } from '../ModalRoot/ModalRootContext';
import { Group, type GroupProps } from './Group';
import styles from './Group.module.css';

Expand Down Expand Up @@ -75,17 +75,11 @@ describe('Group', () => {
}}
>
<AdaptivityContext.Provider value={{ sizeX }}>
<ModalRootContext.Provider
value={{
isInsideModal,
updateModalHeight: noop,
registerModal: noop,
}}
>
<ModalContext.Provider value={isInsideModal ? 'test' : null}>
<Group mode={mode} data-testid="group">
<div />
</Group>
</ModalRootContext.Provider>
</ModalContext.Provider>
</AdaptivityContext.Provider>
</AppRootContext.Provider>,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/vkui/src/components/Group/GroupContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useModalContext } from '../../context/ModalContext';
import { useAdaptivity } from '../../hooks/useAdaptivity';
import type { SizeTypeValues } from '../../lib/adaptivity';
import { warnOnce } from '../../lib/warnOnce';
import type { HasComponent, HTMLAttributesWithRootRef } from '../../types';
import { AppRootContext } from '../AppRoot/AppRootContext';
import { ModalRootContext } from '../ModalRoot/ModalRootContext';
import { RootComponent } from '../RootComponent/RootComponent';
import styles from './Group.module.css';

Expand Down Expand Up @@ -90,7 +90,7 @@ export const GroupContainer: React.FC<GroupContainerProps> = ({
tabIndex: tabIndexProp,
...restProps
}: GroupContainerProps) => {
const { isInsideModal } = React.useContext(ModalRootContext);
const isInsideModal = useModalContext().id !== null;
const { sizeX = 'none' } = useAdaptivity();

const mode = useGroupMode(modeProps, sizeX, isInsideModal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
import { Button } from '../Button/Button';
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
import { Image } from '../Image/Image';
import { ModalRoot } from '../ModalRoot/ModalRootAdaptive';
import { Spacing } from '../Spacing/Spacing';
import { Textarea } from '../Textarea/Textarea';
import { UsersStack } from '../UsersStack/UsersStack';
import { ModalCard, type ModalCardProps } from './ModalCard';
import { ModalCard } from './ModalCard';
import type { ModalCardProps } from './types';

const AppWrapper = ({ children, ...restProps }: AppWrapperProps) => (
<AppDefaultWrapper scroll="contain" {...restProps}>
Expand Down Expand Up @@ -127,15 +127,14 @@ export const ModalCardPlayground = (props: ComponentPlaygroundProps) => {
AppWrapper={AppWrapper}
>
{(props: ModalCardProps) => (
<div style={{ height: 500, transform: 'translateZ(0)' }}>
<ModalRoot
activeModal={props.nav}
<div style={{ height: 500, overflow: 'hidden', transform: 'translateZ(0)' }}>
<ModalCard
open
// Note: с включенным фокусом ломаются скриншоты на движке Webkit из-за фокуса сразу
// на несколько окон
noFocusToDialog
>
<ModalCard {...props} />
</ModalRoot>
{...props}
/>
</div>
)}
</ComponentPlayground>
Expand Down
105 changes: 61 additions & 44 deletions packages/vkui/src/components/ModalCard/ModalCard.module.css
Original file line number Diff line number Diff line change
@@ -1,65 +1,82 @@
.host {
box-sizing: border-box;
position: absolute;
inset-block-start: 0;
padding: 8px;
inset-inline-start: 0;
padding: var(--vkui--spacing_size_m);
margin-inline: auto;
inline-size: 100%;
block-size: 100%;
display: flex;
align-items: flex-end;
box-sizing: border-box;
}

.host:focus {
outline: none;
}

.in {
inline-size: 100%;
margin-inline: auto;
transform: translateY(calc(100% + 16px));
transition: transform 340ms var(--vkui--animation_easing_platform);
.hostMaxWidthS {
max-inline-size: calc(400px + var(--vkui--spacing_size_2xl));
}

/**
* iOS
*/
.hostMaxWidthM {
max-inline-size: calc(414px + var(--vkui--spacing_size_2xl));
}

.ios .in {
max-inline-size: 414px;
.hostMaxWidthL {
max-inline-size: calc(440px + var(--vkui--spacing_size_2xl));
}

/**
* Android + vkcom
*/
/* Mobile */
@media (--viewWidth-smallTabletMinus) {
.host {
--vkui_internal_ModalCard--translateY: 100%;
--vkui_internal_ModalCard--safeAreaInsetBottom: var(--vkui_internal--safe_area_inset_bottom);

.android .in {
max-inline-size: 440px;
}
position: absolute;
inset-inline: 0;
inset-block-end: 0;
margin-block-end: var(--vkui_internal_ModalCard--safeAreaInsetBottom);
transform: translate3d(0, calc(100% - var(--vkui_internal_ModalCard--translateY)), 0);
transition: transform var(--vkui--animation_duration_l) var(--vkui--animation_easing_platform);
}

.vkcom .in {
max-inline-size: 400px;
}
.hostStateEnter {
transform: translate3d(0, 100%, 0);
transition-property: none;
}

/**
* Desktop
*/
.hostStateEntering {
transition-property: transform;
transition-delay: 0.2s;
}

.desktop {
align-items: center;
}
.hostStateExiting {
transform: translate3d(0, 100%, 0);
transition-property: transform;
}

.desktop .in {
transform: unset;
opacity: 0;
transition: opacity 340ms var(--vkui--animation_easing_platform);
.hostStateExited {
transform: translate3d(0, 100%, 0);
transition-property: transform;
}
}
/* Desktop */
@media (--viewWidth-smallTabletPlus) {
.host {
margin-block: auto;
opacity: 1;
transition: opacity 340ms var(--vkui--animation_easing_platform);
}

.hostStateEnter {
opacity: 0;
transition-property: none;
}

.hostStateEntering {
opacity: 1;
}

.hostStateExiting {
opacity: 0;
}

/**
* CMP:
* ModalRoot
*/
/* stylelint-disable-next-line selector-pseudo-class-disallowed-list */
:global(.vkuiInternalModalRoot--touched) .in {
transition: none;
.hostStateExited {
opacity: 0;
}
}
75 changes: 42 additions & 33 deletions packages/vkui/src/components/ModalCard/ModalCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Icon56MoneyTransferOutline, Icon56NotificationOutline } from '@vkontakte/icons';
import { ModalWrapper } from '../../storybook/ModalWrapper';
import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants';
import { getAvatarUrl } from '../../testing/mock';
import { Avatar } from '../Avatar/Avatar';
Expand All @@ -11,7 +10,8 @@ import { Image } from '../Image/Image';
import { Spacing } from '../Spacing/Spacing';
import { Textarea } from '../Textarea/Textarea';
import { UsersStack } from '../UsersStack/UsersStack';
import { ModalCard, type ModalCardProps } from './ModalCard';
import { ModalCard } from './ModalCard';
import type { ModalCardProps } from './types';

const story: Meta<ModalCardProps> = {
title: 'Modals/ModalCard',
Expand All @@ -23,17 +23,14 @@ export default story;

type Story = StoryObj<ModalCardProps>;

const MODAL_CARD_MONEY_SEND = 'money-send';
const MODAL_CARD_APP_TO_MENU = 'app-to-menu';
const MODAL_CARD_ABOUT = 'say-about';
const MODAL_CARD_NOTIFICATIONS = 'notifications';
const MODAL_CARD_CHAT_INVITE = 'chat-invite';

export const SimpleCard: Story = {
render: () => (
<ModalWrapper modalId={MODAL_CARD_MONEY_SEND}>
render: function Render() {
const [open, setOpen] = React.useState(true);
const handleClose = () => setOpen(false);
return (
<ModalCard
id={MODAL_CARD_MONEY_SEND}
open={open}
onClose={handleClose}
icon={<Icon56MoneyTransferOutline />}
title="Отправляйте деньги друзьям, используя банковскую карту"
description="Номер карты получателя не нужен — он сам решит, куда зачислить средства."
Expand All @@ -43,15 +40,18 @@ export const SimpleCard: Story = {
</Button>
}
/>
</ModalWrapper>
),
);
},
};

export const CardWithAvatar: Story = {
render: () => (
<ModalWrapper modalId={MODAL_CARD_APP_TO_MENU}>
render: function Render() {
const [open, setOpen] = React.useState(true);
const handleClose = () => setOpen(false);
return (
<ModalCard
id={MODAL_CARD_APP_TO_MENU}
open={open}
onClose={handleClose}
icon={<Image borderRadius="l" src={getAvatarUrl('app_zagadki', 200)} size={72} />}
title="Добавить игру «Загадки детства» в меню?"
description="Игра появится под списком разделов на экране меню и будет всегда под рукой."
Expand All @@ -61,15 +61,18 @@ export const CardWithAvatar: Story = {
</Button>
}
/>
</ModalWrapper>
),
);
},
};

export const CardWithTextArea: Story = {
render: () => (
<ModalWrapper modalId={MODAL_CARD_ABOUT}>
render: function Render() {
const [open, setOpen] = React.useState(true);
const handleClose = () => setOpen(false);
return (
<ModalCard
id={MODAL_CARD_ABOUT}
open={open}
onClose={handleClose}
title="Расскажите о себе"
actions={
<Button size="l" mode="primary" stretched>
Expand All @@ -79,15 +82,18 @@ export const CardWithTextArea: Story = {
>
<Textarea defaultValue="В Грузии" />
</ModalCard>
</ModalWrapper>
),
);
},
};

export const CardWithMultipleButtons: Story = {
render: () => (
<ModalWrapper modalId={MODAL_CARD_NOTIFICATIONS}>
render: function Render() {
const [open, setOpen] = React.useState(true);
const handleClose = () => setOpen(false);
return (
<ModalCard
id={MODAL_CARD_NOTIFICATIONS}
open={open}
onClose={handleClose}
icon={<Icon56NotificationOutline />}
title="Приложение запрашивает разрешение на отправку Вам уведомлений"
actions={
Expand All @@ -101,15 +107,18 @@ export const CardWithMultipleButtons: Story = {
</ButtonGroup>
}
/>
</ModalWrapper>
),
);
},
};

export const CardWithComplexContent: Story = {
render: () => (
<ModalWrapper modalId={MODAL_CARD_CHAT_INVITE}>
render: function Render() {
const [open, setOpen] = React.useState(true);
const handleClose = () => setOpen(false);
return (
<ModalCard
id={MODAL_CARD_CHAT_INVITE}
open={open}
onClose={handleClose}
icon={<Avatar src={getAvatarUrl('chat_basketball', 200)} size={72} />}
title="Баскетбол на выходных"
titleComponent="h2"
Expand Down Expand Up @@ -147,6 +156,6 @@ export const CardWithComplexContent: Story = {
<br />и ещё 3 человека
</UsersStack>
</ModalCard>
</ModalWrapper>
),
);
},
};
Loading