Skip to content

Commit

Permalink
feat(structure): expand elements on menu click
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Sep 3, 2024
1 parent 7756ae7 commit 1814631
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import {Menu} from '@sanity/ui'
import {Menu, usePortal} from '@sanity/ui'
import {type MouseEvent, useCallback} from 'react'
import {ContextMenuButton, useTranslation} from 'sanity'

import {MenuButton, MenuItem} from '../../../../ui-components'
import {structureLocaleNamespace} from '../../../i18n'
import {TIMELINE_LIST_WRAPPER_ID} from './timeline'
import {TIMELINE_MENU_PORTAL} from './timelineMenu'

/**
* This is a hack to force the scrollbar to not appear when the list is expanding,
* if we don't do this the scrollbar will appear for a brief moment when the list is expanding and then disappear
* when the list is fully expanded.
*/
function hideScrollbarOnExpand(isExpanded: boolean) {
// Do nothing if the list is already expanded
if (isExpanded) return

const listWrapper = document.getElementById(TIMELINE_LIST_WRAPPER_ID)

if (listWrapper) {
const firstChildren = listWrapper.children[0] as HTMLElement
const hasScrollbar = firstChildren.scrollHeight > firstChildren.clientHeight
if (!hasScrollbar) {
//
const currentStyle = getComputedStyle(firstChildren).overflowY
// Add overflow hidden to the listWrapper to avoid the scrollbar to appear when expanding
firstChildren.style.overflowY = 'hidden'
setTimeout(() => {
// Reset the overflow style after the list is expanded
firstChildren.style.overflowY = currentStyle
}, 0)
}
}
}

export function ExpandableTimelineItemMenu({
chunkId,
Expand All @@ -15,14 +44,17 @@ export function ExpandableTimelineItemMenu({
onExpand: () => void
}) {
const {t} = useTranslation(structureLocaleNamespace)
const portalContext = usePortal()

const handleExpandClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
// TODO: Avoid the click event to propagate to the parent button and closing the popover
e.stopPropagation()
hideScrollbarOnExpand(isExpanded)
onExpand()
},
[onExpand],
[onExpand, isExpanded],
)

return (
<MenuButton
id={`timeline-item-menu-button-${chunkId}`}
Expand All @@ -45,6 +77,12 @@ export function ExpandableTimelineItemMenu({
/>
</Menu>
}
popover={{
// when used inside the timeline menu we want to keep the element inside the popover, to avoid closing the popover when clicking expand.
portal: portalContext.elements?.[TIMELINE_MENU_PORTAL] ? TIMELINE_MENU_PORTAL : true,
placement: 'bottom-end',
fallbackPlacements: ['left', 'left-end', 'left-start'],
}}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ interface TimelineProps {
listMaxHeight?: string
}

export const TIMELINE_LIST_WRAPPER_ID = 'timeline-list-wrapper'

export const Timeline = ({
chunks,
hasMoreChunks,
lastChunk: selectedChunk,
onLoadMore,
onSelect,
listMaxHeight = 'calc(100vh - 198px)',
listMaxHeight = 'calc(100vh - 280px)',
}: TimelineProps) => {
const [mounted, setMounted] = useState(false)
const {t} = useTranslation('studio')
Expand Down Expand Up @@ -153,7 +155,7 @@ export const Timeline = ({
)}

{filteredChunks.length > 0 && (
<ListWrapper direction="column" $maxHeight={listMaxHeight}>
<ListWrapper direction="column" $maxHeight={listMaxHeight} id={TIMELINE_LIST_WRAPPER_ID}>
<CommandList
activeItemDataAttr="data-hovered"
ariaLabel={t('timeline.list.aria-label')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {ChevronDownIcon} from '@sanity/icons'
import {
Flex,
type Placement,
PortalProvider,
Text,
useClickOutsideEvent,
useGlobalKeyDown,
useToast,
} from '@sanity/ui'
import {useCallback, useMemo, useRef, useState} from 'react'
import {useCallback, useMemo, useState} from 'react'
import {type Chunk, useTimelineSelector, useTranslation} from 'sanity'
import {styled} from 'styled-components'

Expand All @@ -28,11 +29,14 @@ const Root = styled(Popover)`
overflow: clip;
`

export const TIMELINE_MENU_PORTAL = 'timeline-menu'

export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) {
const {setTimelineRange, setTimelineMode, timelineError, ready, timelineStore} = useDocumentPane()
const [open, setOpen] = useState(false)
const [button, setButton] = useState<HTMLButtonElement | null>(null)
const popoverRef = useRef<HTMLDivElement | null>(null)
const [popoverRef, setPopoverRef] = useState<HTMLElement | null>(null)

const toast = useToast()

const chunks = useTimelineSelector(timelineStore, (state) => state.chunks)
Expand Down Expand Up @@ -64,7 +68,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) {
)

useGlobalKeyDown(handleGlobalKeyDown)
useClickOutsideEvent(open && handleClose, () => [button, popoverRef.current])
useClickOutsideEvent(open && handleClose, () => [button, popoverRef])

const selectRev = useCallback(
(revChunk: Chunk) => {
Expand Down Expand Up @@ -169,35 +173,38 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) {
const buttonLabel = mode === 'rev' ? revLabel : sinceLabel

return (
<Root
data-testid="timeline-menu"
constrainSize
content={open && content}
data-ui="versionMenu"
open={open}
placement={placement}
portal
ref={popoverRef}
>
<Button
data-testid={open ? 'timeline-menu-close-button' : 'timeline-menu-open-button'}
disabled={!ready}
mode="ghost"
onClick={open ? handleClose : handleOpen}
ref={setButton}
selected={open}
width="fill"
tooltipProps={null}
<PortalProvider __unstable_elements={{[TIMELINE_MENU_PORTAL]: popoverRef}}>
<Root
data-testid="timeline-menu"
constrainSize
content={open && content}
data-ui="versionMenu"
open={open}
placement={placement}
matchReferenceWidth
portal
ref={setPopoverRef}
>
<Flex align={'center'} justify={'space-between'}>
<Text size={1} weight="medium">
{ready ? buttonLabel : t('timeline.loading-history')}
</Text>
<Text size={1}>
<ChevronDownIcon />
</Text>
</Flex>
</Button>
</Root>
<Button
data-testid={open ? 'timeline-menu-close-button' : 'timeline-menu-open-button'}
disabled={!ready}
mode="ghost"
onClick={open ? handleClose : handleOpen}
ref={setButton}
selected={open}
width="fill"
tooltipProps={null}
>
<Flex align={'center'} justify={'space-between'}>
<Text size={1} weight="medium">
{ready ? buttonLabel : t('timeline.loading-history')}
</Text>
<Text size={1}>
<ChevronDownIcon />
</Text>
</Flex>
</Button>
</Root>
</PortalProvider>
)
}

0 comments on commit 1814631

Please sign in to comment.