Skip to content

Commit

Permalink
Make a <Modal> controllable via touch events
Browse files Browse the repository at this point in the history
  • Loading branch information
sashachabin committed Aug 26, 2024
1 parent 2ef80cc commit b3215e1
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 75 deletions.
112 changes: 78 additions & 34 deletions client/components/UI/Modal/Modal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,49 @@ html:has(.Modal) {
display: flex;
flex-direction: column;
overflow: visible;
width: calc(100% - 32px);
width: calc(100% - 16px);
max-width: none;
margin: auto;
max-height: none;
margin-top: auto;
margin-bottom: 0;
padding: 0;
border: 0;
background: transparent;
transition: .15s ease-out;
animation: .2s ease-in-out fadeIn;
}

.Modal_Align-top {
margin-top: 16px;
}

.Modal_Align-center {
margin-top: auto;
margin-bottom: auto;
.Modal:not([open]) {
display: none;
}

.Modal::backdrop {
background: rgba(12, 27, 39, .3);
}

.Modal_Position-fullOpen {
height: calc(100% - 90px);
}

.Modal_Position-halfOpen {
height: 50%;
}

.Modal_Position-hidden {
overflow: hidden;
height: 15%;
}

.ModalTitle {
margin: 0;
padding: 24px 48px 0 24px;
padding: 32px 56px 0 24px;
font-size: 24px;
}

.ModalInner {
flex-grow: 1;
overflow: auto;
height: 100%;
border-radius: 8px;
border-radius: 16px 16px 0 0;
background: white;
scrollbar-color: transparent transparent;
}
Expand All @@ -48,31 +58,75 @@ html:has(.Modal) {
}

.ModalContent {
padding: 0px 24px 8px;
padding: 0px 24px;
}

.ModalDragArea {
position: sticky;
top: 0;
left: 0;
z-index: 1;
height: 76px;
margin-bottom: -76px;
touch-action: none;
}

.ModalDragArea::before {
content: '';
position: absolute;
top: 7px;
left: 50%;
display: block;
width: 50px;
height: 5px;
border-radius: 10px;
background: #9baac3;
opacity: 0.5;
transform: translate(-50%, 0);
}

.ModalClose {
position: absolute;
top: 0;
right: 0;
padding: 16px;
z-index: 1;
display: flex;
padding: 20px;
border: 0;
border-radius: 50%;
background: none;
color: rgba(0, 0, 0, 0.5);
}

.ModalClose svg {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
}

@media screen and (min-width: 768px) {
.Modal {
width: calc(100% - 140px);
width: calc(100% - 120px);
height: fit-content;
max-height: calc(100% - 160px);
background: none;
}

.Modal_Align-top {
margin-top: 56px;
margin-top: 80px;
margin-bottom: 0;
}

.Modal_Align-center {
margin-top: auto;
margin-bottom: auto;
}

.ModalDragArea {
display: none;
}

.ModalInner {
border-radius: 24px;
}

.ModalClose {
Expand Down Expand Up @@ -117,18 +171,10 @@ html:has(.Modal) {

@media screen and (min-width: 991px) {
.Modal {
width: calc(100% - 154px);
width: calc(100% - 150px);
max-width: 1360px;
}

.Modal_Align-top {
margin-top: 64px;
}

.ModalInner {
border-radius: 24px;
}

.ModalTitle {
padding: 60px 64px 0;
font-size: 32px;
Expand All @@ -138,9 +184,13 @@ html:has(.Modal) {
padding: 24px 32px;
}

.ModalInner {
border-radius: 28px;
}

.ModalClose {
right: -8px;
padding: 16px;
right: -12px;
}

.ModalClose svg {
Expand All @@ -149,12 +199,6 @@ html:has(.Modal) {
}
}

@media screen and (min-width: 991px) and (min-height: 800px) {
.Modal {
max-height: calc(100vh - 128px);
}
}

@keyframes fadeIn {
from {
opacity: 0;
Expand Down
46 changes: 27 additions & 19 deletions client/components/UI/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useEffect, useRef } from 'react';
import classNames from 'classnames/bind';

import { CardPosition, useSwipeableCard } from 'hooks/useSwipeableCard';
import t from 'utils/typograph';

import { ModalProps } from './Modal.types';

import Close from 'public/icons/close.svg';
Expand All @@ -21,43 +20,52 @@ export function Modal({
const ref = useRef<HTMLDialogElement>(null);
const refInner = useRef<HTMLDivElement>(null);

const mobilePosition = align === 'center'
? CardPosition.HalfOpen
: CardPosition.FullOpen;

const [currentPosition, onDragEnd, onDrag] = useSwipeableCard(mobilePosition);

const close = () => {
ref.current.close();
};

useEffect(() => {
ref.current.showModal();
refInner.current.scrollTo(0, 0);
// Remove focus after open
(document.activeElement as HTMLElement).blur();

const handleClickOutside = (e) => {
if (ref.current === e.target) {
close();
}
const handleClickOutside = (e) => {
if (e.target === ref.current) {
close();
}
};

document.addEventListener('click', handleClickOutside)

return () => {
document.removeEventListener('click', handleClickOutside)
useEffect(() => {
if (refInner.current) {
refInner.current.scrollTo(0, 0);
}
ref.current.showModal();
(document.activeElement as HTMLElement).blur();
}, []);


return (
<dialog
className={cn(styles.Modal, { [`${styles[`Modal_Align-${align}`]}`]: align })}
className={cn(styles.Modal, {
[`${styles[`Modal_Align-${align}`]}`]: align,
[`${styles[`Modal_Position-${currentPosition}`]}`]: currentPosition,
})}
style={{ maxWidth }}
onClick={handleClickOutside}
onClose={onClose}
ref={ref}
>
<div className={cn(styles.ModalInner)} ref={refInner}>
<div
className={cn(styles.ModalDragArea)}
onTouchMoveCapture={onDrag}
onTouchEndCapture={onDragEnd}
/>
{title && <h1 className={cn(styles.ModalTitle)}>{t(title)}</h1>}
<div className={cn(styles.ModalContent)}>{children}</div>
</div>

<button className={cn(styles.ModalClose)} onClick={close} aria-label="Закрыть">
<button className={cn(styles.ModalClose)} onClick={() => close()} aria-label="Закрыть">
<Close />
</button>
</dialog>
Expand Down
8 changes: 4 additions & 4 deletions client/components/UI/Sidepage/Sidepage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
float: right;
margin-bottom: -100%;
top: 0;
padding: 12px;
padding: 0;
border: 0;
background: none;
cursor: pointer;
Expand All @@ -61,14 +61,14 @@
background-color: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(8px);
color: rgba(0, 0, 0, 0.5);
padding: 10px;
padding: 20px;
border-radius: 50%;
transition: .15s ease;
}

.SidepageCloseButtonWrapper svg {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
}

@media screen and (min-width: 768px) {
Expand Down
4 changes: 2 additions & 2 deletions client/components/UI/Sidepage/Sidepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useRef } from 'react';
import classNames from 'classnames/bind';

import { sidebarService } from 'services/sidebar/sidebar';
import { SidepagePosition, useSidepage } from 'hooks/useSidepage/useSidepage';
import { CardPosition, useSwipeableCard } from 'hooks/useSwipeableCard';
import { useDisablePropagation } from 'hooks/useDisablePropagation';

import Close from 'public/icons/close.svg';
Expand All @@ -16,7 +16,7 @@ export function Sidepage({ children }: React.PropsWithChildren) {

useDisablePropagation(ref);

const [currentPosition, onDragEnd, onDrag] = useSidepage(SidepagePosition.HalfOpen);
const [currentPosition, onDragEnd, onDrag] = useSwipeableCard(CardPosition.HalfOpen);

return (
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import React, { useState } from 'react';

export enum SidepagePosition {
export enum CardPosition {
FullOpen = 'fullOpen',
HalfOpen = 'halfOpen',
Hidden = 'hidden',
}

export const MODAL_POSITIONS = [
SidepagePosition.FullOpen,
SidepagePosition.HalfOpen,
SidepagePosition.Hidden,
export const CARD_POSITIONS = [
CardPosition.FullOpen,
CardPosition.HalfOpen,
CardPosition.Hidden,
];
export const CHANGE_POSITION_DELTA = 60;

export function useSidepage(
startPosition = SidepagePosition.FullOpen,
): [SidepagePosition, React.TouchEventHandler, React.TouchEventHandler] {
export function useSwipeableCard(
startPosition = CardPosition.FullOpen,
): [CardPosition, React.TouchEventHandler, React.TouchEventHandler] {
const [currentPositionIndex, setCurrentPositionIndex] = useState(
MODAL_POSITIONS.findIndex((pos) => pos === startPosition),
CARD_POSITIONS.findIndex((pos) => pos === startPosition),
);
const [currentPosition, setCurrentPosition] = useState(startPosition);
let dragStartPosition: null | number = null;
let isDragFinished = false;

function nextPosition(scrollElement?: HTMLElement) {
const newPositionIndex = Math.min(currentPositionIndex + 1, MODAL_POSITIONS.length - 1);
const newPositionIndex = Math.min(currentPositionIndex + 1, CARD_POSITIONS.length - 1);
setCurrentPositionIndex(newPositionIndex);
setCurrentPosition(MODAL_POSITIONS[newPositionIndex]);
setCurrentPosition(CARD_POSITIONS[newPositionIndex]);

if (scrollElement && newPositionIndex === MODAL_POSITIONS.length - 1) {
if (scrollElement && newPositionIndex === CARD_POSITIONS.length - 1) {
scrollElement.scrollTo({
top: 0,
});
Expand All @@ -38,7 +38,7 @@ export function useSidepage(
function prevPosition() {
const newPositionIndex = Math.max(currentPositionIndex - 1, 0);
setCurrentPositionIndex(newPositionIndex);
setCurrentPosition(MODAL_POSITIONS[newPositionIndex]);
setCurrentPosition(CARD_POSITIONS[newPositionIndex]);
}

function onDragEnd(event: React.TouchEvent) {
Expand All @@ -65,8 +65,8 @@ export function useSidepage(
if (currentDragPosition - dragStartPosition > CHANGE_POSITION_DELTA) {
isDragFinished = true;

const SidepageElement = (event.target as HTMLElement).parentElement;
nextPosition(SidepageElement);
const cardElement = (event.target as HTMLElement).parentElement;
nextPosition(cardElement);
}

if (currentDragPosition - dragStartPosition < -CHANGE_POSITION_DELTA) {
Expand Down
2 changes: 1 addition & 1 deletion client/pages/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Head from 'next/head';
import { articlesApi } from 'api/articles/articles';
import { ArticleProps } from 'components/Articles/Article/Article.types';
import { Article } from 'components/Articles/Article/Article';
import { Feedback } from 'components/Common/Feedback/Feedback';
import { Feedback } from 'components/UI/Feedback/Feedback';

function ArticlePage({ title, description, sidebar }: ArticleProps) {
return (
Expand Down

0 comments on commit b3215e1

Please sign in to comment.