Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make a <Modal> controllable via touch events #113

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading