#693 open calendar menu on mobile (#842)

Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
lethemanh
2026-04-28 21:09:21 +07:00
committed by GitHub
parent 234b4c3ae8
commit c910f3e3a9
4 changed files with 265 additions and 59 deletions
+92 -59
View File
@@ -10,34 +10,23 @@ import {
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Checkbox, Checkbox,
Divider,
IconButton, IconButton,
ListItem, ListItem
Menu,
MenuItem
} from '@linagora/twake-mui' } from '@linagora/twake-mui'
import AddIcon from '@mui/icons-material/Add' import AddIcon from '@mui/icons-material/Add'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import MoreHorizIcon from '@mui/icons-material/MoreHoriz' import MoreHorizIcon from '@mui/icons-material/MoreHoriz'
import { SetStateAction, useEffect, useMemo, useState } from 'react' import { SetStateAction, useEffect, useMemo, useState, useRef } from 'react'
import { useI18n } from 'twake-i18n' import { useI18n } from 'twake-i18n'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import CalendarPopover from './CalendarModal' import CalendarPopover from './CalendarModal'
import CalendarSearch from './CalendarSearch' import CalendarSearch from './CalendarSearch'
import { DeleteCalendarDialog } from './DeleteCalendarDialog' import { DeleteCalendarDialog } from './DeleteCalendarDialog'
import { OwnerCaption } from './OwnerCaption' import { OwnerCaption } from './OwnerCaption'
import CalendarResources from './CalendarResources' import CalendarResources from './CalendarResources'
import { CalendarSelectorMenu } from './CalendarSelectorMenu'
function CalendarAccordion({ const CalendarAccordion: React.FC<{
title,
calendars,
selectedCalendars,
handleToggle,
showAddButton = false,
onAddClick,
defaultExpanded = false,
setOpen,
hideOwner
}: {
title: string title: string
calendars: string[] calendars: string[]
selectedCalendars: string[] selectedCalendars: string[]
@@ -47,14 +36,24 @@ function CalendarAccordion({
defaultExpanded?: boolean defaultExpanded?: boolean
setOpen: (id: string) => void setOpen: (id: string) => void
hideOwner?: boolean hideOwner?: boolean
}) { }> = ({
title,
calendars,
selectedCalendars,
handleToggle,
showAddButton = false,
onAddClick,
defaultExpanded = false,
setOpen,
hideOwner
}) => {
const allCalendars = useAppSelector(state => state.calendars.list) const allCalendars = useAppSelector(state => state.calendars.list)
const { t } = useI18n() const { t } = useI18n()
const [expended, setExpended] = useState(defaultExpanded) const [expended, setExpended] = useState(defaultExpanded)
useEffect(() => { useEffect(() => {
const handleExpendedChange = () => { const handleExpendedChange = (): void => {
setExpended(defaultExpanded) setExpended(defaultExpanded)
} }
handleExpendedChange() handleExpendedChange()
@@ -127,13 +126,10 @@ function CalendarAccordion({
) )
} }
export default function CalendarSelection({ const CalendarSelection: React.FC<{
selectedCalendars,
setSelectedCalendars
}: {
selectedCalendars: string[] selectedCalendars: string[]
setSelectedCalendars: (value: SetStateAction<string[]>) => void setSelectedCalendars: (value: SetStateAction<string[]>) => void
}) { }> = ({ selectedCalendars, setSelectedCalendars }) => {
const { t } = useI18n() const { t } = useI18n()
const userId = useAppSelector(state => state.user.userData?.openpaasId) ?? '' const userId = useAppSelector(state => state.user.userData?.openpaasId) ?? ''
const calendars = useAppSelector(state => state.calendars.list) const calendars = useAppSelector(state => state.calendars.list)
@@ -158,7 +154,7 @@ export default function CalendarSelection({
extractEventBaseUuid(id) !== userId && calendars?.[id]?.owner?.resource extractEventBaseUuid(id) !== userId && calendars?.[id]?.owner?.resource
) )
const handleCalendarToggle = (name: string) => { const handleCalendarToggle = (name: string): void => {
setSelectedCalendars((prev: string[]) => setSelectedCalendars((prev: string[]) =>
prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name] prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]
) )
@@ -243,7 +239,7 @@ export default function CalendarSelection({
/> />
<CalendarSearch <CalendarSearch
open={Boolean(anchorElCalOthers)} open={Boolean(anchorElCalOthers)}
onClose={(newCalIds?: string[]) => { onClose={(newCalIds?: string[] | Record<string, never>) => {
setAnchorElCalOthers(null) setAnchorElCalOthers(null)
if (newCalIds?.length) { if (newCalIds?.length) {
newCalIds.forEach(id => handleCalendarToggle(id)) newCalIds.forEach(id => handleCalendarToggle(id))
@@ -265,15 +261,7 @@ export default function CalendarSelection({
) )
} }
function CalendarSelector({ const CalendarSelector: React.FC<{
calendars,
id,
isPersonal,
selectedCalendars,
handleCalendarToggle,
setOpen,
hideOwner
}: {
calendars: Record<string, Calendar> calendars: Record<string, Calendar>
id: string id: string
isPersonal: boolean isPersonal: boolean
@@ -281,25 +269,62 @@ function CalendarSelector({
handleCalendarToggle: (name: string) => void handleCalendarToggle: (name: string) => void
setOpen: () => void setOpen: () => void
hideOwner?: boolean hideOwner?: boolean
}) { }> = ({
calendars,
id,
isPersonal,
selectedCalendars,
handleCalendarToggle,
setOpen,
hideOwner
}) => {
const { t } = useI18n() const { t } = useI18n()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const calLink = useAppSelector(state => state.calendars.list[id].link) ?? '' const calLink = useAppSelector(state => state.calendars.list[id].link) ?? ''
const { isTooSmall: isMobile } = useScreenSizeDetection()
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null) const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const open = Boolean(anchorEl) const open = Boolean(anchorEl)
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
setAnchorEl(event.currentTarget) setAnchorEl(event.currentTarget)
} }
const handleClose = () => { const handleClose = (): void => {
setAnchorEl(null) setAnchorEl(null)
} }
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const isLongPressedRef = useRef(false)
const handleTouchStart = (): void => {
if (!isMobile) return
isLongPressedRef.current = false
longPressTimerRef.current = setTimeout(() => {
isLongPressedRef.current = true
setAnchorEl(document.body)
}, 300)
}
const handleTouchEnd = (): void => {
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current)
longPressTimerRef.current = null
}
}
const handleTouchMove = (): void => {
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current)
longPressTimerRef.current = null
}
}
const [userId, calId] = id.split('/') const [userId, calId] = id.split('/')
const isDefault = isPersonal && userId === calId const isDefault = isPersonal && userId === calId
const [deletePopupOpen, setDeletePopupOpen] = useState(false) const [deletePopupOpen, setDeletePopupOpen] = useState(false)
const handleDeleteConfirm = () => { const handleDeleteConfirm = async (): Promise<void> => {
dispatch(removeCalendarAsync({ calId: id, calLink })) await dispatch(removeCalendarAsync({ calId: id, calLink }))
setDeletePopupOpen(false) setDeletePopupOpen(false)
handleClose() handleClose()
} }
@@ -339,6 +364,15 @@ function CalendarSelector({
'& .MoreBtn': { opacity: 1 } '& .MoreBtn': { opacity: 1 }
} }
}} }}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onTouchMove={handleTouchMove}
onClickCapture={e => {
if (isLongPressedRef.current) {
e.stopPropagation()
e.preventDefault()
}
}}
> >
<label <label
style={{ style={{
@@ -381,26 +415,23 @@ function CalendarSelector({
/> />
</div> </div>
</label> </label>
<IconButton className="MoreBtn" onClick={handleClick}> {!isMobile && (
<MoreHorizIcon /> <IconButton className="MoreBtn" onClick={handleClick}>
</IconButton> <MoreHorizIcon />
</ListItem> </IconButton>
<Menu id={id} anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem
onClick={() => {
setOpen()
handleClose()
}}
>
{t('actions.modify')}
</MenuItem>
{!isDefault && <Divider />}
{!isDefault && (
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
{isPersonal ? t('actions.delete') : t('actions.remove')}
</MenuItem>
)} )}
</Menu> </ListItem>
<CalendarSelectorMenu
id={id}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
onModify={setOpen}
onDelete={() => setDeletePopupOpen(true)}
isDefault={isDefault}
isPersonal={isPersonal}
/>
<DeleteCalendarDialog <DeleteCalendarDialog
deletePopupOpen={deletePopupOpen} deletePopupOpen={deletePopupOpen}
@@ -408,8 +439,10 @@ function CalendarSelector({
calendars={calendars} calendars={calendars}
id={id} id={id}
isPersonal={isPersonal} isPersonal={isPersonal}
handleDeleteConfirm={handleDeleteConfirm} handleDeleteConfirm={() => void handleDeleteConfirm()}
/> />
</> </>
) )
} }
export default CalendarSelection
@@ -0,0 +1,53 @@
import React from 'react'
import { Divider, Menu, MenuItem } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
interface CalendarSelectorDesktopMenuProps {
id: string
anchorEl: HTMLElement | null
open: boolean
onClose: () => void
onModify: () => void
onDelete: () => void
isDefault: boolean
isPersonal: boolean
}
export const CalendarSelectorDesktopMenu: React.FC<
CalendarSelectorDesktopMenuProps
> = ({
id,
anchorEl,
open,
onClose,
onModify,
onDelete,
isDefault,
isPersonal
}) => {
const { t } = useI18n()
return (
<Menu id={id} anchorEl={anchorEl} open={open} onClose={onClose}>
<MenuItem
onClick={() => {
onModify()
onClose()
}}
>
{t('actions.modify')}
</MenuItem>
{!isDefault && <Divider />}
{!isDefault && (
<MenuItem
onClick={() => {
onDelete()
onClose()
}}
>
{isPersonal ? t('actions.delete') : t('actions.remove')}
</MenuItem>
)}
</Menu>
)
}
@@ -0,0 +1,66 @@
import React from 'react'
import {
styled,
SwipeableDrawer,
List,
ListItemButton,
ListItemText
} from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
zIndex: theme.zIndex.modal + 100
}))
interface CalendarSelectorMobileMenuProps {
open: boolean
onClose: () => void
onModify: () => void
onDelete: () => void
isDefault: boolean
isPersonal: boolean
}
export const CalendarSelectorMobileMenu: React.FC<
CalendarSelectorMobileMenuProps
> = ({ open, onClose, onModify, onDelete, isDefault, isPersonal }) => {
const { t } = useI18n()
return (
<StyledSwipeableDrawer
anchor="bottom"
open={open}
onClose={onClose}
onOpen={() => {}}
disableAutoFocus
>
<List>
<ListItemButton
onClick={() => {
onModify()
onClose()
}}
>
<ListItemText primary={t('actions.modify')} />
</ListItemButton>
{!isDefault && (
<ListItemButton
onClick={() => {
onDelete()
onClose()
}}
>
<ListItemText
primary={isPersonal ? t('actions.delete') : t('actions.remove')}
slotProps={{
primary: {
color: 'error.main'
}
}}
/>
</ListItemButton>
)}
</List>
</StyledSwipeableDrawer>
)
}
@@ -0,0 +1,54 @@
import React from 'react'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { CalendarSelectorDesktopMenu } from './CalendarSelectorDesktopMenu'
import { CalendarSelectorMobileMenu } from './CalendarSelectorMobileMenu'
interface CalendarSelectorMenuProps {
id: string
anchorEl: HTMLElement | null
open: boolean
onClose: () => void
onModify: () => void
onDelete: () => void
isDefault: boolean
isPersonal: boolean
}
export const CalendarSelectorMenu: React.FC<CalendarSelectorMenuProps> = ({
id,
anchorEl,
open,
onClose,
onModify,
onDelete,
isDefault,
isPersonal
}) => {
const { isTooSmall: isMobile } = useScreenSizeDetection()
if (!isMobile) {
return (
<CalendarSelectorDesktopMenu
id={id}
anchorEl={anchorEl}
open={open}
onClose={onClose}
onModify={onModify}
onDelete={onDelete}
isDefault={isDefault}
isPersonal={isPersonal}
/>
)
}
return (
<CalendarSelectorMobileMenu
open={open}
onClose={onClose}
onModify={onModify}
onDelete={onDelete}
isDefault={isDefault}
isPersonal={isPersonal}
/>
)
}