diff --git a/packages/vkui/src/components/Alert/Alert.module.css b/packages/vkui/src/components/Alert/Alert.module.css index 8f6f5006aa..f8a417df2c 100644 --- a/packages/vkui/src/components/Alert/Alert.module.css +++ b/packages/vkui/src/components/Alert/Alert.module.css @@ -34,7 +34,7 @@ .Alert__content { position: relative; - padding: 24px 24px 20px; + padding: 24px 24px 16px; } .Alert__action { @@ -46,7 +46,7 @@ display: flex; max-width: 100%; position: relative; - padding: 0 16px 16px; + padding: 0 12px 12px; } .Alert__header { @@ -57,30 +57,34 @@ color: var(--vkui--color_text_secondary); } -.Alert--h .Alert__actions { +.Alert__actions--direction-horizontal { justify-content: flex-end; } -.Alert--h .Alert__button { - margin-left: 8px; -} - -.Alert--v .Alert__actions { +.Alert__actions--direction-vertical { flex-direction: column; align-items: flex-end; } -.Alert--v .Alert__button { - margin-top: 4px; - margin-bottom: 4px; +/* stylelint-disable @project-tools/stylelint-atomic */ +.Alert__actions > * { + margin: 4px; +} +/* stylelint-enable @project-tools/stylelint-atomic */ + +.Alert__actions--align-left { + justify-content: flex-start; + align-items: flex-start; } -.Alert--v .Alert__button:first-child { - margin-top: 0; +.Alert__actions--align-center { + justify-content: center; + align-items: center; } -.Alert--v .Alert__button:last-child { - margin-bottom: 0; +.Alert__actions--align-right { + justify-content: flex-end; + align-items: flex-end; } /** @@ -128,7 +132,7 @@ padding: initial; } -.Alert--ios.Alert--v .Alert__actions { +.Alert--ios .Alert__actions--direction-vertical { flex-direction: column; align-items: initial; } @@ -157,7 +161,7 @@ background: var(--vkui--color_separator_primary_alpha); } -.Alert--ios.Alert--h .Alert__action::after { +.Alert--ios .Alert__actions--direction-horizontal .Alert__action::after { top: 0; right: 0; width: 1px; @@ -165,25 +169,25 @@ transform-origin: right center; } -.Alert--ios.Alert--h .Alert__action:last-child::after { +.Alert--ios .Alert__actions--direction-horizontal .Alert__action:last-child::after { content: none; } -.Alert--ios.Alert--h .Alert__action { +.Alert--ios .Alert__actions--direction-horizontal .Alert__action { flex-grow: 1; flex-shrink: 1; flex-basis: 0; } -.Alert--ios.Alert--h .Alert__action:first-child { +.Alert--ios .Alert__actions--direction-horizontal .Alert__action:first-child { border-bottom-left-radius: var(--vkui--size_border_radius_paper--regular); } -.Alert--ios.Alert--h .Alert__action:last-child { +.Alert--ios .Alert__actions--direction-horizontal .Alert__action:last-child { border-bottom-right-radius: var(--vkui--size_border_radius_paper--regular); } -.Alert--ios.Alert--v .Alert__action::after { +.Alert--ios .Alert__actions--direction-vertical .Alert__action::after { left: 0; bottom: 0; width: 100%; @@ -191,31 +195,31 @@ transform-origin: center bottom; } -.Alert--ios.Alert--v .Alert__action:last-child::after { +.Alert--ios .Alert__actions--direction-vertical .Alert__action:last-child::after { content: none; } -.Alert--ios.Alert--v .Alert__action:last-child { +.Alert--ios .Alert__actions--direction-vertical .Alert__action:last-child { border-radius: 0 0 12px 12px; } @media (min-resolution: 2dppx) { .Alert--ios .Alert__content::after, - .Alert--ios.Alert--v .Alert__action::after { + .Alert--ios .Alert__actions--direction-vertical .Alert__action::after { transform: scaleY(0.5); } - .Alert--ios.Alert--h .Alert__action::after { + .Alert--ios .Alert__actions--direction-horizontal .Alert__action::after { transform: scaleX(0.5); } } @media (min-resolution: 3dppx) { .Alert--ios .Alert__content::after, - .Alert--ios.Alert--v .Alert__action::after { + .Alert--ios .Alert__actions--direction-vertical .Alert__action::after { transform: scaleY(0.33); } - .Alert--ios.Alert--h .Alert__action::after { + .Alert--ios .Alert__actions--direction-horizontal .Alert__action::after { transform: scaleX(0.33); } } @@ -238,11 +242,11 @@ } .Alert--vkcom .Alert__content { - padding: 24px; + padding-bottom: 20px; } .Alert--vkcom .Alert__actions { - padding: 0 24px 16px; + padding: 0 20px 12px; } .Alert--vkcom .Alert__button { diff --git a/packages/vkui/src/components/Alert/Alert.tsx b/packages/vkui/src/components/Alert/Alert.tsx index 6e77e39a09..4537100d8a 100644 --- a/packages/vkui/src/components/Alert/Alert.tsx +++ b/packages/vkui/src/components/Alert/Alert.tsx @@ -6,31 +6,33 @@ import { usePlatform } from '../../hooks/usePlatform'; import { useWaitTransitionFinish } from '../../hooks/useWaitTransitionFinish'; import { Platform } from '../../lib/platform'; import { stopPropagation } from '../../lib/utils'; -import { AnchorHTMLAttributesOnly, HasChildren } from '../../types'; +import { AlignType, AnchorHTMLAttributesOnly } from '../../types'; import { useScrollLock } from '../AppRoot/ScrollContext'; -import { Button, ButtonProps } from '../Button/Button'; +import { ButtonProps } from '../Button/Button'; import { FocusTrap } from '../FocusTrap/FocusTrap'; import { ModalDismissButton } from '../ModalDismissButton/ModalDismissButton'; import { PopoutWrapper } from '../PopoutWrapper/PopoutWrapper'; -import { Tappable } from '../Tappable/Tappable'; -import { Caption } from '../Typography/Caption/Caption'; -import { Footnote } from '../Typography/Footnote/Footnote'; -import { Text } from '../Typography/Text/Text'; -import { Title } from '../Typography/Title/Title'; +import { AlertActionProps } from './AlertAction'; +import { AlertActions } from './AlertActions'; +import { AlertHeader, AlertText } from './AlertTypography'; import styles from './Alert.module.css'; +type AlertActionMode = 'cancel' | 'destructive' | 'default'; + export interface AlertActionInterface extends Pick, AnchorHTMLAttributesOnly { title: string; action?: VoidFunction; autoClose?: boolean; - mode: 'cancel' | 'destructive' | 'default'; + mode: AlertActionMode; } export interface AlertProps extends React.HTMLAttributes { actionsLayout?: 'vertical' | 'horizontal'; + actionsAlign?: AlignType; actions?: AlertActionInterface[]; + renderAction?: (props: AlertActionProps) => React.ReactNode; header?: React.ReactNode; text?: React.ReactNode; onClose: VoidFunction; @@ -41,89 +43,6 @@ export interface AlertProps extends React.HTMLAttributes { dismissLabel?: string; } -type ItemClickHandler = (item: AlertActionInterface) => void; - -interface AlertTypography extends HasChildren { - id: string; -} - -const AlertHeader = (props: AlertTypography) => { - const platform = usePlatform(); - - switch (platform) { - case Platform.IOS: - return ; - default: - return <Title className={styles['Alert__header']} weight="2" level="2" {...props} />; - } -}; - -const AlertText = (props: AlertTypography) => { - const platform = usePlatform(); - - switch (platform) { - case Platform.VKCOM: - return <Footnote className={styles['Alert__text']} {...props} />; - case Platform.IOS: - return <Caption className={styles['Alert__text']} {...props} />; - default: - return <Text Component="span" className={styles['Alert__text']} weight="3" {...props} />; - } -}; - -interface AlertActionProps { - action: AlertActionInterface; - onItemClick: ItemClickHandler; -} - -const AlertAction = ({ action, onItemClick, ...restProps }: AlertActionProps) => { - const platform = usePlatform(); - const handleItemClick = React.useCallback(() => onItemClick(action), [onItemClick, action]); - - if (platform === Platform.IOS) { - const { title, action: actionProp, autoClose, mode, ...restActionProps } = action; - - return ( - <Tappable - Component={restActionProps.href ? 'a' : 'button'} - className={classNames( - styles['Alert__action'], - mode === 'destructive' && styles['Alert__action--mode-destructive'], - mode === 'cancel' && styles['Alert__action--mode-cancel'], - )} - onClick={handleItemClick} - {...restActionProps} - {...restProps} - > - {title} - </Tappable> - ); - } - - let mode: ButtonProps['mode'] = 'tertiary'; - - if (platform === Platform.VKCOM) { - mode = action.mode === 'cancel' ? 'secondary' : 'primary'; - } - - return ( - <Button - className={classNames( - styles['Alert__button'], - action.mode === 'cancel' && styles['Alert__button--mode-cancel'], - )} - mode={mode} - size="m" - onClick={handleItemClick} - Component={action.Component} - href={action.href} - target={action.target} - > - {action.title} - </Button> - ); -}; - /** * @see https://vkcom.github.io/VKUI/#/Alert */ @@ -137,6 +56,8 @@ export const Alert = ({ header, onClose, dismissLabel = 'Закрыть предупреждение', + renderAction, + actionsAlign, ...restProps }: AlertProps) => { const generatedId = useId(); @@ -152,9 +73,6 @@ export const Alert = ({ const elementRef = React.useRef<HTMLDivElement>(null); - const resolvedActionsLayout: AlertProps['actionsLayout'] = - platform === Platform.VKCOM ? 'horizontal' : actionsLayout; - const timeout = platform === Platform.IOS ? 300 : 200; const close = React.useCallback(() => { @@ -170,7 +88,7 @@ export const Alert = ({ ); }, [elementRef, waitTransitionFinish, onClose, timeout]); - const onItemClick: ItemClickHandler = React.useCallback( + const onItemClick = React.useCallback( (item: AlertActionInterface) => { const { action, autoClose } = item; @@ -207,7 +125,6 @@ export const Alert = ({ styles['Alert'], platform === Platform.IOS && styles['Alert--ios'], platform === Platform.VKCOM && styles['Alert--vkcom'], - resolvedActionsLayout === 'vertical' ? styles['Alert--v'] : styles['Alert--h'], closing && styles['Alert--closing'], isDesktop && styles['Alert--desktop'], )} @@ -221,11 +138,13 @@ export const Alert = ({ {hasReactNode(text) && <AlertText id={textId}>{text}</AlertText>} {children} </div> - <div className={styles['Alert__actions']}> - {actions.map((action, i) => ( - <AlertAction key={i} action={action} onItemClick={onItemClick} /> - ))} - </div> + <AlertActions + actions={actions} + actionsAlign={actionsAlign} + actionsLayout={actionsLayout} + renderAction={renderAction} + onItemClick={onItemClick} + /> {isDesktop && <ModalDismissButton onClick={close} aria-label={dismissLabel} />} </FocusTrap> </PopoutWrapper> diff --git a/packages/vkui/src/components/Alert/AlertAction.tsx b/packages/vkui/src/components/Alert/AlertAction.tsx new file mode 100644 index 0000000000..798d58829a --- /dev/null +++ b/packages/vkui/src/components/Alert/AlertAction.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { classNames } from '@vkontakte/vkjs'; +import { usePlatform } from '../../hooks/usePlatform'; +import { Platform } from '../../lib/platform'; +import { AnchorHTMLAttributesOnly } from '../../types'; +import { Button, ButtonProps } from '../Button/Button'; +import { Tappable } from '../Tappable/Tappable'; +import { AlertActionInterface } from './Alert'; +import styles from './Alert.module.css'; + +export interface AlertActionProps + extends Pick<AlertActionInterface, 'Component' | 'mode'>, + AnchorHTMLAttributesOnly { + children: string; + onClick: React.MouseEventHandler<HTMLElement>; +} + +const AlertActionIos = ({ mode, ...restProps }: AlertActionProps) => { + return ( + <Tappable + Component={restProps.href ? 'a' : 'button'} + className={classNames( + styles['Alert__action'], + mode === 'destructive' && styles['Alert__action--mode-destructive'], + mode === 'cancel' && styles['Alert__action--mode-cancel'], + )} + {...restProps} + /> + ); +}; + +const AlertActionBase = ({ mode, ...restProps }: AlertActionProps) => { + const platform = usePlatform(); + + let buttonMode: ButtonProps['mode'] = 'tertiary'; + + if (platform === Platform.VKCOM) { + buttonMode = mode === 'cancel' ? 'secondary' : 'primary'; + } + + return ( + <Button + className={classNames( + styles['Alert__button'], + mode === 'cancel' && styles['Alert__button--mode-cancel'], + )} + mode={buttonMode} + size="m" + {...restProps} + /> + ); +}; + +export const AlertAction = (props: AlertActionProps) => { + const platform = usePlatform(); + + if (platform === Platform.IOS) { + return <AlertActionIos {...props} />; + } + + return <AlertActionBase {...props} />; +}; diff --git a/packages/vkui/src/components/Alert/AlertActions.tsx b/packages/vkui/src/components/Alert/AlertActions.tsx new file mode 100644 index 0000000000..f33ecfd5d6 --- /dev/null +++ b/packages/vkui/src/components/Alert/AlertActions.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { classNames } from '@vkontakte/vkjs'; +import { usePlatform } from '../../hooks/usePlatform'; +import { Platform } from '../../lib/platform'; +import { AlertActionInterface, AlertProps } from './Alert'; +import { AlertAction } from './AlertAction'; +import styles from './Alert.module.css'; + +const alignStyles = { + left: styles['Alert__actions--align-left'], + center: styles['Alert__actions--align-center'], + right: styles['Alert__actions--align-right'], +}; + +const directionStyles = { + vertical: styles['Alert__actions--direction-vertical'], + horizontal: styles['Alert__actions--direction-horizontal'], +}; + +type ItemClickHandler = (item: AlertActionInterface) => void; +interface AlertActionsProps + extends Pick<AlertProps, 'actions' | 'actionsAlign' | 'renderAction' | 'actionsLayout'> { + onItemClick: ItemClickHandler; +} +export const AlertActions = ({ + actions = [], + renderAction = (props) => <AlertAction {...props} />, + onItemClick, + actionsAlign, + actionsLayout, +}: AlertActionsProps) => { + const platform = usePlatform(); + + const direction: AlertProps['actionsLayout'] = + platform === Platform.VKCOM ? 'horizontal' : actionsLayout; + + return ( + <div + className={classNames( + styles['Alert__actions'], + actionsAlign && alignStyles[actionsAlign], + direction && directionStyles[direction], + )} + > + {actions.map((action, i) => { + // Убираем + const { title: children, action: _, autoClose, ...restProps } = action; + + return ( + <React.Fragment key={i}> + {renderAction({ + children, + onClick: () => onItemClick(action), + ...restProps, + })} + </React.Fragment> + ); + })} + </div> + ); +}; diff --git a/packages/vkui/src/components/Alert/AlertTypography.tsx b/packages/vkui/src/components/Alert/AlertTypography.tsx new file mode 100644 index 0000000000..93bb2cd15e --- /dev/null +++ b/packages/vkui/src/components/Alert/AlertTypography.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { usePlatform } from '../../hooks/usePlatform'; +import { Platform } from '../../lib/platform'; +import { HasChildren } from '../../types'; +import { Caption } from '../Typography/Caption/Caption'; +import { Footnote } from '../Typography/Footnote/Footnote'; +import { Text } from '../Typography/Text/Text'; +import { Title } from '../Typography/Title/Title'; +import styles from './Alert.module.css'; + +interface AlertTypography extends HasChildren { + id: string; +} +export const AlertHeader = (props: AlertTypography) => { + const platform = usePlatform(); + + switch (platform) { + case Platform.IOS: + return <Title className={styles['Alert__header']} weight="1" level="3" {...props} />; + default: + return <Title className={styles['Alert__header']} weight="2" level="2" {...props} />; + } +}; +export const AlertText = (props: AlertTypography) => { + const platform = usePlatform(); + + switch (platform) { + case Platform.VKCOM: + return <Footnote className={styles['Alert__text']} {...props} />; + case Platform.IOS: + return <Caption className={styles['Alert__text']} {...props} />; + default: + return <Text Component="span" className={styles['Alert__text']} weight="3" {...props} />; + } +}; diff --git a/packages/vkui/src/components/Alert/Readme.md b/packages/vkui/src/components/Alert/Readme.md index 4dcfc307ca..12871c0c09 100644 --- a/packages/vkui/src/components/Alert/Readme.md +++ b/packages/vkui/src/components/Alert/Readme.md @@ -109,3 +109,65 @@ const Example = () => { <Example />; ``` + +## renderAction + +```jsx { "props": { "layout": false, "adaptivity": true } } +const renderAction = ({ mode, ...restProps }) => { + return <Button mode={mode === 'cancel' ? 'secondary' : 'primary'} size="m" {...restProps} />; +}; + +const Example = () => { + const [popout, setPopout] = React.useState(null); + + const closePopout = () => { + setPopout(null); + }; + + const openAction = () => { + setPopout( + <Alert + actions={[ + { + title: 'Лишить права', + mode: 'destructive', + autoClose: true, + }, + { + title: 'Отмена', + autoClose: true, + mode: 'cancel', + }, + ]} + actionsAlign="left" + actionsLayout="horizontal" + renderAction={renderAction} + onClose={closePopout} + header="Подтвердите действие" + text="Вы уверены, что хотите лишить пользователя права на модерацию контента?" + />, + ); + }; + + React.useEffect(() => { + openAction(); + }, []); + + return ( + <SplitLayout popout={popout}> + <SplitCol> + <View activePanel="alert"> + <Panel id="alert"> + <PanelHeader>Alert</PanelHeader> + <Group> + <CellButton onClick={openAction}>Лишить права</CellButton> + </Group> + </Panel> + </View> + </SplitCol> + </SplitLayout> + ); +}; + +<Example />; +``` diff --git a/packages/vkui/src/components/Alert/__image_snapshots__/alert-mobile-android-chromium-dark-1-snap.png b/packages/vkui/src/components/Alert/__image_snapshots__/alert-mobile-android-chromium-dark-1-snap.png index f484b7d017..0f6ad13b8f 100644 --- a/packages/vkui/src/components/Alert/__image_snapshots__/alert-mobile-android-chromium-dark-1-snap.png +++ b/packages/vkui/src/components/Alert/__image_snapshots__/alert-mobile-android-chromium-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:560a5e7b09e09a9e3b2de3ca30d50f3fd5bbe5251e4319035fa45c33fc0d308c -size 75391 +oid sha256:e3aa5e602338887001320cb18ac274cdcae715c1645f1cdc619e6d5535177e0f +size 50499