Skip to content

Commit

Permalink
feat(ModalPage, ModalCard): add outside buttons support (#8214)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackySoul authored Jan 30, 2025
1 parent 8df2db2 commit 6e09949
Show file tree
Hide file tree
Showing 33 changed files with 376 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Icon56MoneyTransferOutline } from '@vkontakte/icons';
import { Icon20More, Icon56MoneyTransferOutline } from '@vkontakte/icons';
import {
AppDefaultWrapper,
type AppDefaultWrapperProps,
Expand All @@ -9,6 +9,7 @@ import {
import { Button } from '../Button/Button';
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
import { Image } from '../Image/Image';
import { ModalOutsideButton } from '../ModalOutsideButton/ModalOutsideButton';
import { Spacing } from '../Spacing/Spacing';
import { Textarea } from '../Textarea/Textarea';
import { UsersStack } from '../UsersStack/UsersStack';
Expand Down Expand Up @@ -140,3 +141,41 @@ export const ModalCardPlayground = (props: ComponentPlaygroundProps) => {
</ComponentPlayground>
);
};

export const ModalCardOutsideButtonPlayground = (props: ComponentPlaygroundProps) => {
return (
<ComponentPlayground
{...props}
propSets={[
{
nav: ['1'],
title: ['Расскажите о себе'],
actions: [
<Button key="action" size="l" mode="primary" stretched>
Сохранить
</Button>,
],
dismissButtonMode: ['inside', 'outside', 'none'],
outsideButtons: [
<ModalOutsideButton aria-label="More" key="outside">
<Icon20More />
</ModalOutsideButton>,
],
},
]}
AppWrapper={AppWrapper}
>
{(props: ModalCardProps) => (
<div style={{ height: 300, overflow: 'hidden', transform: 'translateZ(0)' }}>
<ModalCard
open
// Note: с включенным фокусом ломаются скриншоты на движке Webkit из-за фокуса сразу
// на несколько окон
noFocusToDialog
{...props}
/>
</div>
)}
</ComponentPlayground>
);
};
21 changes: 20 additions & 1 deletion packages/vkui/src/components/ModalCard/ModalCard.e2e.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from '@vkui-e2e/test';
import { ViewWidth } from '../../lib/adaptivity';
import { ModalCardPlayground } from './ModalCard.e2e-playground';
import { ModalCardOutsideButtonPlayground, ModalCardPlayground } from './ModalCard.e2e-playground';

test.describe('ModalCard', () => {
test.use({
Expand Down Expand Up @@ -43,3 +43,22 @@ test.describe(() => {
await expectScreenshotClippedToContent();
});
});

test.describe('ModalCard', () => {
test.use({
adaptivityProviderProps: {
viewWidth: ViewWidth.SMALL_TABLET,
sizeY: 'compact',
},
onlyForPlatforms: ['ios', 'android'],
onlyForColorSchemes: ['light'],
});
test('OutsideButton', async ({
mount,
expectScreenshotClippedToContent,
componentPlaygroundProps,
}) => {
await mount(<ModalCardOutsideButtonPlayground {...componentPlaygroundProps} />);
await expectScreenshotClippedToContent();
});
});
7 changes: 7 additions & 0 deletions packages/vkui/src/components/ModalCard/ModalCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { useArgs } from '@storybook/preview-api';
import type { Meta, StoryObj } from '@storybook/react';
import { Icon20More } from '@vkontakte/icons';
import { noop } from '@vkontakte/vkjs';
import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants';
import { getAvatarUrl } from '../../testing/mock';
Expand All @@ -9,6 +10,7 @@ import { Avatar } from '../Avatar/Avatar';
import { Button } from '../Button/Button';
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
import { Image } from '../Image/Image';
import { ModalOutsideButton } from '../ModalOutsideButton/ModalOutsideButton';
import { Spacing } from '../Spacing/Spacing';
import { Textarea } from '../Textarea/Textarea';
import { UsersStack } from '../UsersStack/UsersStack';
Expand Down Expand Up @@ -145,6 +147,11 @@ export const CardWithComplexContent: Story = {
</ButtonGroup>
</React.Fragment>
),
outsideButtons: (
<ModalOutsideButton aria-label="More" onClick={noop}>
<Icon20More />
</ModalOutsideButton>
),
children: (
<>
<Spacing size={20} />
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 41 additions & 9 deletions packages/vkui/src/components/ModalCardBase/ModalCardBase.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
'use client';

import * as React from 'react';
import { Icon20Cancel, Icon24Dismiss } from '@vkontakte/icons';
import { classNames, hasReactNode } from '@vkontakte/vkjs';
import { useAdaptivityWithJSMediaQueries } from '../../hooks/useAdaptivityWithJSMediaQueries';
import { usePlatform } from '../../hooks/usePlatform';
import type { HTMLAttributesWithRootRef } from '../../types';
import { AdaptivityContext } from '../AdaptivityProvider/AdaptivityContext';
import { ModalOutsideButton } from '../ModalOutsideButton/ModalOutsideButton';
import { ModalOutsideButtons } from '../ModalOutsideButtons/ModalOutsideButtons';
import { RootComponent } from '../RootComponent/RootComponent';
import { Spacing } from '../Spacing/Spacing';
import { Tappable } from '../Tappable/Tappable';
import { Subhead } from '../Typography/Subhead/Subhead';
import { Title } from '../Typography/Title/Title';
import { ModalCardBaseCloseButton } from './ModalCardBaseCloseButton';
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden';
import styles from './ModalCardBase.module.css';

export interface ModalCardBaseProps
Expand Down Expand Up @@ -81,6 +85,13 @@ export interface ModalCardBaseProps
* ⚠️ ВНИМАНИЕ: использование этой опции негативно сказывается на пользовательском опыте
*/
preventClose?: boolean;
/**
* Управляющие элементы под кнопкой закрытия.
*
* Доступно только в `compact`-режиме. Рекомендуется размещать иконки размера 20, обернутые в ModalOutsideButton
*
*/
outsideButtons?: React.ReactNode;
}

/**
Expand All @@ -95,11 +106,12 @@ export const ModalCardBase = ({
children,
actions,
onClose,
dismissLabel = 'Скрыть',
dismissLabel = 'Закрыть',
size: sizeProp,
modalDismissButtonTestId,
dismissButtonMode = 'outside',
preventClose,
outsideButtons,
...restProps
}: ModalCardBaseProps): React.ReactNode => {
const platform = usePlatform();
Expand All @@ -113,6 +125,7 @@ export const ModalCardBase = ({

const hasTitle = hasReactNode(title);
const hasDescription = hasReactNode(description);

return (
<RootComponent
{...restProps}
Expand Down Expand Up @@ -146,14 +159,33 @@ export const ModalCardBase = ({

{hasReactNode(actions) && <div className={styles.actions}>{actions}</div>}

{dismissButtonMode !== 'none' && (
<ModalCardBaseCloseButton
testId={modalDismissButtonTestId}
onClose={onClose}
mode={dismissButtonMode}
{isDesktop && (dismissButtonMode === 'outside' || outsideButtons) && (
<ModalOutsideButtons>
{dismissButtonMode === 'outside' && (
<ModalOutsideButton
aria-label={dismissLabel}
data-testid={modalDismissButtonTestId}
onClick={onClose}
>
<Icon20Cancel />
</ModalOutsideButton>
)}
{outsideButtons}
</ModalOutsideButtons>
)}

{(dismissButtonMode === 'inside' ||
(platform === 'ios' && !isDesktop && dismissButtonMode !== 'none')) && (
<Tappable
className={styles.dismiss}
onClick={onClose}
hoverMode="opacity"
activeMode="opacity"
data-testid={modalDismissButtonTestId}
>
{dismissLabel}
</ModalCardBaseCloseButton>
<VisuallyHidden>{dismissLabel}</VisuallyHidden>
{platform === 'ios' ? <Icon24Dismiss /> : <Icon20Cancel />}
</Tappable>
)}
</div>
</RootComponent>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,4 @@
position: absolute;
inset-block-start: 0;
inset-inline-end: -56px;
inline-size: 56px;
block-size: 56px;
padding: 18px;
box-sizing: border-box;
color: var(--vkui--color_icon_contrast);
}

.host::before {
display: block;
content: '';
inset: 14px;
background: var(--vkui--color_overlay_secondary);
border-radius: 50%;
position: absolute;
transition: background-color 0.15s ease-out;
}

/* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- fixes icon misplacement on Safari in some cases */
.host :global(.vkuiIcon) {
transform: translateX(0);
}

.hover::before {
background: var(--vkui--color_overlay_secondary--hover);
}

.active::before {
background: var(--vkui--color_overlay_secondary--active);
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import * as React from 'react';
import { Icon20Cancel } from '@vkontakte/icons';
import { Tappable, type TappableProps } from '../Tappable/Tappable';
import { classNames } from '@vkontakte/vkjs';
import {
ModalOutsideButton,
type ModalOutsideButtonProps,
} from '../ModalOutsideButton/ModalOutsideButton';
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden';
import styles from './ModalDismissButton.module.css';

export type ModalDismissButtonProps = Omit<TappableProps, 'mode' | 'onClose'>;
export interface ModalDismissButtonProps extends Omit<ModalOutsideButtonProps, 'children'> {
children?: React.ReactNode;
}

/**
* @see https://vkcom.github.io/VKUI/#/ModalDismissButton
*/
export const ModalDismissButton = ({
children = 'Закрыть',
className,
...restProps
}: ModalDismissButtonProps): React.ReactNode => {
return (
<Tappable
baseClassName={styles.host}
{...restProps}
activeMode={styles.active}
hoverMode={styles.hover}
>
<ModalOutsideButton className={classNames(styles.host, className)} {...restProps}>
{children && <VisuallyHidden>{children}</VisuallyHidden>}
<Icon20Cancel />
</Tappable>
</ModalOutsideButton>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Icon20More } from '@vkontakte/icons';
import { noop } from '@vkontakte/vkjs';
import { ComponentPlayground, type ComponentPlaygroundProps } from '@vkui-e2e/playground-helpers';
import { ModalOutsideButton, type ModalOutsideButtonProps } from './ModalOutsideButton';

export const ModalOutsideButtonPlayground = (props: ComponentPlaygroundProps) => (
<ComponentPlayground {...props}>
{(props: ModalOutsideButtonProps) => (
<div style={{ width: 60, display: 'flex', flexDirection: 'column' }}>
<ModalOutsideButton aria-label="Больше" {...props}>
<Icon20More />
</ModalOutsideButton>
<ModalOutsideButton aria-label="Больше" {...props} onClick={noop} hovered>
<Icon20More />
</ModalOutsideButton>
<ModalOutsideButton aria-label="Больше" {...props} onClick={noop} activated>
<Icon20More />
</ModalOutsideButton>
</div>
)}
</ComponentPlayground>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test } from '@vkui-e2e/test';
import { ModalOutsideButtonPlayground } from './ModalOutsideButton.e2e-playground';

test('ModalOutsideButton', async ({
mount,
expectScreenshotClippedToContent,
componentPlaygroundProps,
}) => {
await mount(<ModalOutsideButtonPlayground {...componentPlaygroundProps} />);
await expectScreenshotClippedToContent();
});
Loading

0 comments on commit 6e09949

Please sign in to comment.