Skip to content

Commit

Permalink
concept: new ModalPage
Browse files Browse the repository at this point in the history
  • Loading branch information
SevereCloud committed May 18, 2023
1 parent 1cdad2d commit d9253b1
Show file tree
Hide file tree
Showing 6 changed files with 501 additions and 0 deletions.
73 changes: 73 additions & 0 deletions packages/vkui/src/components/ModalPageNew/ModalPageNew.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.ModalPageNew__container {
top: 0;
right: 0;
bottom: 0;
left: 0;
position: fixed;
overflow-x: hidden;
overflow-y: auto;
z-index: var(--vkui--z_index_modal);
-webkit-overflow-scrolling: touch;

/**
* Для удаление скролла в Firefox.
* В версии ниже 64 будет виден скролл, но это не ломает функциональность.
*/
scrollbar-width: none;

/**
* В старых браузерах не работает, но это не ломает функциональность.
*/
scroll-snap-type: y mandatory;
}

/* stylelint-disable-next-line @project-tools/stylelint-atomic */
.ModalPageNew__container > * {
scroll-snap-align: start;
}

.ModalPageNew__container::-webkit-scrollbar {
display: none;
}

.ModalPageNew__contentIn {
background: var(--vkui--color_background_modal);
border-radius: var(--vkui--size_border_radius_paper--regular)
var(--vkui--size_border_radius_paper--regular) 0 0;
margin-left: auto;
margin-right: auto;
max-width: var(--vkui--size_popup_small--regular);
}

.ModalPageNew__mask {
z-index: var(--vkui--z_index_modal);
position: fixed;
background: var(--vkui--color_overlay_primary);
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
}

/**
* В старых браузерах sticky не работает, но это не ломает функциональность.
*/
.ModalPageNew__header {
z-index: 1;
position: sticky;
top: 0;
border-radius: inherit;
background: var(--vkui--color_background_modal);
}

.ModalPageNew__headerFixed {
border-radius: 0;
}

.ModalPageNew__footer {
z-index: 2;
position: sticky;
bottom: 0;
background: var(--vkui--color_background_modal);
}
130 changes: 130 additions & 0 deletions packages/vkui/src/components/ModalPageNew/ModalPageNew.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { withSinglePanel, withVKUILayout } from '../../storybook/VKUIDecorators';
import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants';
import { Avatar } from '../Avatar/Avatar';
import { Button } from '../Button/Button';
import { Card } from '../Card/Card';
import { CardScroll } from '../CardScroll/CardScroll';
import { Div } from '../Div/Div';
import { FormItem } from '../FormItem/FormItem';
import { Group } from '../Group/Group';
import { Header } from '../Header/Header';
import { HorizontalCell } from '../HorizontalCell/HorizontalCell';
import { HorizontalScroll } from '../HorizontalScroll/HorizontalScroll';
import { Input } from '../Input/Input';
import { ModalPageHeader } from '../ModalPageHeader/ModalPageHeader';
import { Panel } from '../Panel/Panel';
import { PanelHeader } from '../PanelHeader/PanelHeader';
import { SimpleCell } from '../SimpleCell/SimpleCell';
import { Textarea } from '../Textarea/Textarea';
import { ModalPageNew, ModalPageNewProps } from './ModalPageNew';

const story: Meta<ModalPageNewProps> = {
title: 'Modals/ModalPageNew',
component: ModalPageNew,
parameters: { ...CanvasFullLayout, ...DisableCartesianParam },
decorators: [withSinglePanel, withVKUILayout],
};

export default story;

type Story = StoryObj<ModalPageNewProps>;

export const Example: Story = {
render: function Render() {
const [modal, setModal] = React.useState(true);

const openModal = () => setModal(true);
const closeModal = () => setModal(false);

return (
<Panel>
<PanelHeader>ANKI</PanelHeader>
<Group>
<FormItem>
<Input />
</FormItem>

<SimpleCell onClick={openModal}>open modal</SimpleCell>
</Group>

<Group>
{Array(50)
.fill(undefined)
.map((_, i) => (
<SimpleCell key={i} expandable>
SimpleCell
</SimpleCell>
))}
</Group>

{modal && (
<ModalPageNew
header={<ModalPageHeader>Заголовок</ModalPageHeader>}
footer={
<Div>
<Button size="l" stretched>
Button
</Button>
</Div>
}
onClosed={closeModal}
>
<Group>
<Header>https://github.com/VKCOM/VKUI/issues/335</Header>
<HorizontalScroll>
<div style={{ display: 'flex' }}>
{Array(50)
.fill(undefined)
.map((_, i) => (
<HorizontalCell key={i} header="title">
<Avatar />
</HorizontalCell>
))}
</div>
</HorizontalScroll>
</Group>

<Group>
<Header>https://github.com/VKCOM/VKUI/issues/338</Header>
<Header>https://github.com/VKCOM/VKUI/issues/599</Header>
<FormItem>
<Input />
</FormItem>
</Group>

<Group>
<Header>https://github.com/VKCOM/VKUI/issues/1071</Header>
<FormItem>
<Textarea />
</FormItem>
</Group>

<Group>
<Header>https://github.com/VKCOM/VKUI/issues/1494</Header>
<CardScroll size="s">
<div style={{ display: 'flex' }}>
{Array(50)
.fill(undefined)
.map((_, i) => (
<Card key={i}>
<div style={{ paddingBottom: '66%' }} />
</Card>
))}
</div>
</CardScroll>
</Group>

<Div>
https://github.com/VKCOM/VKUI/issues/604 https://github.com/VKCOM/VKUI/issues/741
https://github.com/VKCOM/VKUI/issues/876 https://github.com/VKCOM/VKUI/issues/1570
https://github.com/VKCOM/VKUI/issues/2008 https://github.com/VKCOM/VKUI/issues/2029
https://github.com/VKCOM/VKUI/issues/2030 https://github.com/VKCOM/VKUI/issues/2449
</Div>
</ModalPageNew>
)}
</Panel>
);
},
};
186 changes: 186 additions & 0 deletions packages/vkui/src/components/ModalPageNew/ModalPageNew.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useEventListener } from '../../hooks/useEventListener';
import { useGlobalEventListener } from '../../hooks/useGlobalEventListener';
import { useDOM } from '../../lib/dom';
import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect';
import { useScrollLock } from '../AppRoot/ScrollContext';
import styles from './ModalPageNew.module.css';

// Прокрутка элемента на определенный процент
function useFirstOpen(container: React.RefObject<HTMLDivElement>, settlingHeight: number) {
useIsomorphicLayoutEffect(() => {
const el = container.current!;

el.scrollTop = 0;
el.scrollTo({
top: (el.clientHeight * settlingHeight) / 100,
behavior: 'smooth',
});
}, []);
}

// Отступы для модалки
function useIndents(settlingHeight: number) {
const { document, window } = useDOM();

const indent1Ref = React.useRef<HTMLDivElement>(null);
const indent2Ref = React.useRef<HTMLDivElement>(null);

const indentCalculate = () => {
const indent1Height = (document!.documentElement.clientHeight * settlingHeight) / 100;
const indent2Height = document!.documentElement.clientHeight - indent1Height;

indent1Ref.current!.style.height = `${indent1Height}px`;
indent2Ref.current!.style.height = `${indent2Height}px`;
};

useGlobalEventListener(window, 'resize', indentCalculate);

useIsomorphicLayoutEffect(() => {
indentCalculate();
}, [settlingHeight]);

return [indent1Ref, indent2Ref];
}

// Маска для модалки
function useMask(container: React.RefObject<HTMLDivElement>, settlingHeight: number) {
const maskRef = React.useRef<HTMLDivElement>(null);

const scroll = () => {
const el = container.current!;

const indent1 = (el.clientHeight * settlingHeight) / 100;

const opacity = Math.min(el.scrollTop / indent1, 1);

maskRef.current!.style.opacity = `${opacity}`;
};

const scrollListener = useEventListener('scroll', scroll);

useIsomorphicLayoutEffect(() => {
const el = container.current!;
scrollListener.add(el);
}, [settlingHeight]);

return maskRef;
}

function useCheckScroll(container: React.RefObject<HTMLDivElement>, closeCallback: () => void) {
useIsomorphicLayoutEffect(() => {
const el = container.current!;

const scroll = () => {
if (el.scrollTop === 0) {
closeCallback();
}
};

el.addEventListener('scroll', scroll);

return () => el.removeEventListener('scroll', scroll);
}, []);
}

function useFullOpen(container: React.RefObject<HTMLDivElement>) {
const [fullOpen, setFullOpen] = React.useState(false);

useIsomorphicLayoutEffect(() => {
const el = container.current!;

const scroll = () => {
if (el.scrollTop >= el.offsetHeight) {
!fullOpen && setFullOpen(true);
return;
}

fullOpen && setFullOpen(false);
};

el.addEventListener('scroll', scroll);

return () => el.removeEventListener('scroll', scroll);
}, [fullOpen]);

return fullOpen;
}

export interface ModalPageNewProps {
header?: React.ReactNode;
children?: React.ReactNode;
footer?: React.ReactNode;

onClose?: () => void;
onClosed?: () => void;

/**
* Процент, на который изначально будет открыта модальная страница.
* При settlingHeight={100} модальная страница раскрывается на всю высоту.
*/
settlingHeight?: number;
}

export const ModalPageNew = ({
header,
children,
footer,
onClose,
onClosed,
settlingHeight = 75,
...restProp
}: ModalPageNewProps) => {
const containerRef = React.useRef<HTMLDivElement>(null);

const [indent1Ref, indent2Ref] = useIndents(settlingHeight);
const maskRef = useMask(containerRef, settlingHeight);

useFirstOpen(containerRef, settlingHeight);
const fullOpen = useFullOpen(containerRef);
useCheckScroll(containerRef, () => {
console.log('Closed');
onClosed && onClosed();
});

useScrollLock();

const close = () => {
onClose && onClose();

containerRef.current?.scrollTo({
top: 0,
behavior: 'smooth',
});
};

return (
<>
<div className={styles['ModalPageNew__mask']} ref={maskRef} />
<div
className={styles['ModalPageNew__container']}
ref={containerRef}
{...restProp}
onClick={close}
>
<div style={{ height: `${settlingHeight}%` }} ref={indent1Ref} />
<div style={{ height: `${100 - settlingHeight}%` }} ref={indent2Ref} />
<div className={styles['ModalPageNew__contentIn']} onClick={(e) => e.stopPropagation()}>
<div
className={classNames(
styles['ModalPageNew__header'],
fullOpen && styles['ModalPageNew__headerFixed'],
)}
>
{header}
</div>

{children}

{footer && <div className={styles['ModalPageNew__footer']}>{footer}</div>}
</div>
<div />
</div>
</>
);
};
Loading

0 comments on commit d9253b1

Please sign in to comment.