feat(ux): improve pull-down interaction for mobile modal (#440)
* Adjusts overlay background opacity dynamically based on drag position * Ensures smooth transition as the modal is pulled down * Enhances UX for mobile users by making dismissal more intuitive
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { TbLayoutSidebar, TbLayoutSidebarFilled } from 'react-icons/tb';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Button from '@/components/Button';
|
||||
@@ -12,6 +13,7 @@ interface SidebarTogglerProps {
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { sideBarBookKey, isSideBarVisible, setSideBarBookKey, toggleSideBar } = useSidebarStore();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleSideBar();
|
||||
@@ -19,6 +21,7 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
setSideBarBookKey(bookKey);
|
||||
if (!isSideBarVisible) toggleSideBar();
|
||||
}
|
||||
setHoveredBookKey('');
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -9,11 +9,11 @@ import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
@@ -81,12 +81,6 @@ const Notebook: React.FC = ({}) => {
|
||||
setNotebookEditAnnotation(null);
|
||||
};
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = 1 - e.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_NOTEBOOK_WIDTH, Math.min(MAX_NOTEBOOK_WIDTH, widthFraction));
|
||||
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
|
||||
const handleSaveNote = (selection: TextSelection, note: string) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
@@ -132,7 +126,13 @@ const Notebook: React.FC = ({}) => {
|
||||
setNotebookEditAnnotation(null);
|
||||
};
|
||||
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
const onDragMove = (data: { clientX: number }) => {
|
||||
const widthFraction = 1 - data.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_NOTEBOOK_WIDTH, Math.min(MAX_NOTEBOOK_WIDTH, widthFraction));
|
||||
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
|
||||
const { handleDragStart } = useDrag(onDragMove);
|
||||
|
||||
if (!sideBarBookKey) return null;
|
||||
|
||||
@@ -175,7 +175,7 @@ const Notebook: React.FC = ({}) => {
|
||||
`}</style>
|
||||
<div
|
||||
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleDragStart}
|
||||
/>
|
||||
<NotebookHeader
|
||||
isPinned={isNotebookPinned}
|
||||
|
||||
@@ -64,6 +64,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
/>
|
||||
}
|
||||
label={book.title}
|
||||
labelClass='max-w-36'
|
||||
onClick={() => handleParallelView(book.hash)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -9,11 +9,11 @@ import { BookSearchResult } from '@/types/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import SidebarHeader from './Header';
|
||||
import SidebarContent from './Content';
|
||||
import BookCard from './BookCard';
|
||||
import useSidebar from '../../hooks/useSidebar';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import SearchBar from './SearchBar';
|
||||
import SearchResults from './SearchResults';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
@@ -33,6 +33,7 @@ const SideBar: React.FC<{
|
||||
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const isMobile = window.innerWidth < 640;
|
||||
const {
|
||||
sideBarWidth,
|
||||
isSideBarPinned,
|
||||
@@ -42,7 +43,7 @@ const SideBar: React.FC<{
|
||||
handleSideBarTogglePin,
|
||||
} = useSidebar(
|
||||
settings.globalReadSettings.sideBarWidth,
|
||||
window.innerWidth >= 640 ? settings.globalReadSettings.isSideBarPinned : false,
|
||||
isMobile ? false : settings.globalReadSettings.isSideBarPinned,
|
||||
);
|
||||
|
||||
const onSearchEvent = async (event: CustomEvent) => {
|
||||
@@ -79,12 +80,52 @@ const SideBar: React.FC<{
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = e.clientX / window.innerWidth;
|
||||
const handleVerticalDragMove = (data: { clientY: number }) => {
|
||||
if (!isMobile) return;
|
||||
|
||||
const heightFraction = data.clientY / window.innerHeight;
|
||||
const newTop = Math.max(0.0, Math.min(1, heightFraction));
|
||||
|
||||
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
|
||||
if (sidebar && overlay) {
|
||||
sidebar.style.top = `${newTop * 100}%`;
|
||||
overlay.style.opacity = `${1 - heightFraction}`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerticalDragEnd = (velocity: number) => {
|
||||
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
|
||||
if (!sidebar || !overlay) return;
|
||||
|
||||
if (velocity > 0.5) {
|
||||
sidebar.style.transition = `top ${0.15 / velocity}s ease-out`;
|
||||
sidebar.style.top = '100%';
|
||||
overlay.style.transition = `opacity ${0.15 / velocity}s ease-out`;
|
||||
overlay.style.opacity = '0';
|
||||
setTimeout(() => setSideBarVisible(false), 300);
|
||||
} else {
|
||||
sidebar.style.transition = 'top 0.3s ease-out';
|
||||
sidebar.style.top = '0%';
|
||||
overlay.style.transition = 'opacity 0.3s ease-out';
|
||||
overlay.style.opacity = '0.8';
|
||||
}
|
||||
};
|
||||
|
||||
const handleHorizontalDragMove = (data: { clientX: number }) => {
|
||||
const widthFraction = data.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, widthFraction));
|
||||
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
|
||||
const { handleDragStart: handleVerticalDragStart } = useDrag(
|
||||
handleVerticalDragMove,
|
||||
handleVerticalDragEnd,
|
||||
);
|
||||
const { handleDragStart: handleHorizontalDragStart } = useDrag(handleHorizontalDragMove);
|
||||
|
||||
const handleClickOverlay = () => {
|
||||
setSideBarVisible(false);
|
||||
@@ -135,10 +176,27 @@ const SideBar: React.FC<{
|
||||
.sidebar-container {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
.sidebar-container.open {
|
||||
top: 0%;
|
||||
}
|
||||
.overlay {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div className='flex-shrink-0'>
|
||||
{isMobile && (
|
||||
<div
|
||||
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
|
||||
onMouseDown={handleVerticalDragStart}
|
||||
onTouchStart={handleVerticalDragStart}
|
||||
>
|
||||
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
<SidebarHeader
|
||||
isPinned={isSideBarPinned}
|
||||
isSearchBarVisible={isSearchBarVisible}
|
||||
@@ -174,11 +232,14 @@ const SideBar: React.FC<{
|
||||
)}
|
||||
<div
|
||||
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleHorizontalDragStart}
|
||||
></div>
|
||||
</div>
|
||||
{!isSideBarPinned && (
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/20' onClick={handleClickOverlay} />
|
||||
<div
|
||||
className='overlay fixed inset-0 z-10 bg-black/50 sm:bg-black/20'
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
|
||||
Reference in New Issue
Block a user