-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b5aa644
commit 7ab238c
Showing
6 changed files
with
500 additions
and
0 deletions.
There are no files selected for viewing
73 changes: 73 additions & 0 deletions
73
packages/vkui/src/components/ModalPageNew/ModalPageNew.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
130
packages/vkui/src/components/ModalPageNew/ModalPageNew.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}, | ||
}; |
185 changes: 185 additions & 0 deletions
185
packages/vkui/src/components/ModalPageNew/ModalPageNew.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
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> | ||
</> | ||
); | ||
}; |
Oops, something went wrong.