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

Replace hand-rolled dropdown on the dashboard and menu-related UI fixes #5037

Merged
Merged
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f6985a6
Try popover
apata Jan 28, 2025
ce86bbe
Pass targetRef instead of target, stop Escape clearing filters
apata Jan 29, 2025
719e59e
Stop Escape clearing filters when popover menus active
apata Jan 29, 2025
9bfc4ec
Attempt get rid of hand-rolled dropdown
apata Jan 29, 2025
d392d3b
Fix issue with comparison calendar
apata Jan 29, 2025
315ce65
Almost works
apata Jan 29, 2025
5ca7af2
Unify styles
apata Jan 29, 2025
9f1cb57
Focus modals on mount
apata Jan 29, 2025
776a19f
Refactor date picker logic
apata Jan 29, 2025
6db18d8
Replace navigate keybinds with straightforward keybinds
apata Jan 29, 2025
688c947
Remove extraneous Calendar component, better props
apata Jan 29, 2025
15efcc3
Attempt optimise menu re-renders
apata Jan 29, 2025
38a7360
Memoise QueryPeriodsMenu, refactor getDatePeriodGroups
apata Jan 30, 2025
e2dae51
Refactor ComparisonMenu to Popover
apata Jan 30, 2025
ec86842
Refactor QueryPeriodMenu to Popover
apata Jan 30, 2025
b8c68f9
Pull calendar out of components
apata Jan 30, 2025
c49465d
Refactor calendar to receive position from JS
apata Jan 30, 2025
868dd40
Simpler calendar API
apata Jan 30, 2025
c526804
Add click outside listener for Calendar, fix FiltersBar measurements
apata Jan 30, 2025
3dbd86a
Give back top bar room
apata Jan 30, 2025
d21f724
Update tests
apata Jan 30, 2025
68afaeb
Apply unified button text style
apata Jan 30, 2025
2633518
Close calendar on keyboard nav to othe menus as well
apata Jan 30, 2025
a6a3807
Kinda works
apata Feb 3, 2025
994c52c
Works even better
apata Feb 3, 2025
ea6fe11
Working well
apata Feb 3, 2025
cb9fbe3
Sometimes menu stays active
apata Feb 3, 2025
382b13f
WIP
apata Feb 3, 2025
2793345
Adapter
apata Feb 3, 2025
5788d62
Works with error
apata Feb 3, 2025
85a7b33
Fixed the error
apata Feb 3, 2025
304758c
Remove handrolled isOpen
apata Feb 3, 2025
cec6f02
Introduce shared adapter
apata Feb 3, 2025
8fdeb07
Share adapter
apata Feb 3, 2025
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
Next Next commit
Try popover
apata committed Jan 29, 2025
commit f6985a60119184434ef4b8bd99781a822ea3060f
29 changes: 29 additions & 0 deletions assets/js/dashboard/components/popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** @format */

import { TransitionClasses } from '@headlessui/react'

const TRANSITION_CONFIG: TransitionClasses = {
enter: 'transition ease-out duration-100',
enterFrom: 'opacity-0 scale-95',
enterTo: 'opacity-100 scale-100',
leave: 'transition ease-in duration-75',
leaveFrom: 'opacity-100 scale-100',
leaveTo: 'opacity-0 scale-95'
}

const transition = {
props: TRANSITION_CONFIG,
classNames: { fullwidth: 'z-10 absolute left-0 right-0' }
}

const panel = {
classNames: {
roundedSheet:
'rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 font-medium text-gray-800 dark:text-gray-200'
}
}

export const popover = {
panel,
transition
}
181 changes: 114 additions & 67 deletions assets/js/dashboard/nav-menu/filter-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
/** @format */

import React, { useMemo, useRef, useState } from 'react'
import {
DropdownLinkGroup,
DropdownMenuWrapper,
DropdownNavigationLink,
DropdownSubtitle,
ToggleDropdownButton
} from '../components/dropdown'
import React, { useMemo, useRef } from 'react'
import {
FILTER_MODAL_TO_FILTER_GROUP,
formatFilterGroup
} from '../util/filters'
import { PlausibleSite, useSiteContext } from '../site-context'
import { filterRoute } from '../router'
import { useOnClickOutside } from '../util/use-on-click-outside'
import { PlusIcon } from '@heroicons/react/20/solid'
import { Popover, Transition } from '@headlessui/react'
import { popover } from '../components/popover'
import classNames from 'classnames'
import { AppNavigationLink } from '../navigation/use-app-navigate'
import { isModifierPressed, isTyping, Keybind } from '../keybinding'

export function getFilterListItems({
@@ -51,77 +47,128 @@ export function getFilterListItems({
}

export const FilterMenu = () => {
const dropdownRef = useRef<HTMLDivElement>(null)
const [opened, setOpened] = useState(false)
const site = useSiteContext()
const columns = useMemo(() => getFilterListItems(site), [site])

useOnClickOutside({
ref: dropdownRef,
active: opened,
handler: () => setOpened(false)
})

const ref = useRef<HTMLDivElement>(null)
return (
<ToggleDropdownButton
ref={dropdownRef}
variant="ghost"
<Popover
className="shrink-0 md:relative"
dropdownContainerProps={{
['aria-controls']: 'filter-menu',
['aria-expanded']: opened
}}
onClick={() => setOpened((opened) => !opened)}
currentOption={
<div className="flex items-center gap-1 ">
<PlusIcon className="block h-4 w-4" />
Add filter
</div>
}
ref={ref}
data-no-clear-filters-on-escape={true}
>
{opened && (
<DropdownMenuWrapper id="filter-menu" className="md:left-auto md:w-80">
{({ close }) => (
<>
<Keybind
keyboardKey="Escape"
shouldIgnoreWhen={[isModifierPressed, isTyping]}
type="keyup"
handler={(event) => {
// ;(event as unknown as Record<string, unknown>).hi = true
event.stopPropagation()
setOpened(false)
event.preventDefault()
// console.log(`Inner ${open}`, event)
// if (open) {
// handler()
// }
// // return true
}}
target={dropdownRef.current}
target={ref.current}
shouldIgnoreWhen={[isModifierPressed, isTyping]}
/>

<DropdownLinkGroup className="flex flex-row">
{columns.map((filterGroups, index) => (
<div key={index} className="flex flex-col w-1/2">
{filterGroups.map(({ title, modals }) => (
<div key={title}>
<DropdownSubtitle className="pb-1">
{title}
</DropdownSubtitle>
{modals
.filter((m) => !!m)
.map((modalKey) => (
<DropdownNavigationLink
className={'text-xs'}
onClick={() => setOpened(false)}
active={false}
key={modalKey}
path={filterRoute.path}
params={{ field: modalKey }}
search={(search) => search}
>
{formatFilterGroup(modalKey)}
</DropdownNavigationLink>
))}
</div>
))}
</div>
))}
</DropdownLinkGroup>
</DropdownMenuWrapper>
<Popover.Button
className={classNames(
'flex items-center gap-1',
'h-9 px-3',
'rounded text-sm leading-tight',
'text-gray-500 hover:text-gray-800 hover:bg-gray-200 dark:hover:text-gray-200 dark:hover:bg-gray-900'
)}
>
<PlusIcon className="block h-4 w-4" />
<span className="truncate block font-medium">Add filter</span>
</Popover.Button>
<Transition
{...popover.transition.props}
className={classNames(
'mt-2',
popover.transition.classNames.fullwidth,
'md:left-auto md:w-80'
)}
>
<Popover.Panel
className={classNames(
popover.panel.classNames.roundedSheet,
'flex'
)}
>
<StopEscapePropagation
// open={open}
target={ref.current}
// handler={close}
/>
{columns.map((filterGroups, index) => (
<div key={index} className="flex flex-col w-1/2">
{filterGroups.map(({ title, modals }) => (
<div key={title}>
<div className="pb-1 px-4 pt-2 text-xs font-bold uppercase text-indigo-500 dark:text-indigo-400">
{title}
</div>
{modals
.filter((m) => !!m)
.map((modalKey) => (
<AppNavigationLink
className={classNames(
'flex',
'px-4 py-2 text-sm leading-tight hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-900 dark:hover:text-gray-100',
'text-xs'
)}
onClick={() => close()}
key={modalKey}
path={filterRoute.path}
params={{ field: modalKey }}
search={(s) => s}
>
{formatFilterGroup(modalKey)}
</AppNavigationLink>
))}
</div>
))}
</div>
))}
</Popover.Panel>
</Transition>
</>
)}
</ToggleDropdownButton>
</Popover>
)
}

const StopEscapePropagation = ({
// open,
// handler,
target
}: {
// open: boolean
// handler: () => void
target: HTMLDivElement | null
}) => {
// useEffect(() => {}, [])
// return null
return (
<Keybind
keyboardKey="Escape"
type="keyup"
handler={(event) => {
// ;(event as unknown as Record<string, unknown>).hi = true
event.stopPropagation()
event.preventDefault()
// console.log(`Inner ${open}`, event)
// if (open) {
// handler()
// }
// // return true
}}
target={target}
shouldIgnoreWhen={[isModifierPressed, isTyping]}
/>
)
}
81 changes: 35 additions & 46 deletions assets/js/dashboard/nav-menu/filters-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/** @format */

import { EllipsisHorizontalIcon } from '@heroicons/react/20/solid'
import { EllipsisHorizontalIcon } from '@heroicons/react/24/solid'
import classNames from 'classnames'
import React, { useRef, useState, useLayoutEffect, useEffect } from 'react'
import { useOnClickOutside } from '../util/use-on-click-outside'
import {
DropdownMenuWrapper,
ToggleDropdownButton
} from '../components/dropdown'
import React, { useRef, useState, useLayoutEffect } from 'react'
import { AppliedFilterPillsList, PILL_X_GAP } from './filter-pills-list'
import { useQueryContext } from '../query-context'
import { AppNavigationLink } from '../navigation/use-app-navigate'
import { BUFFER_FOR_SHADOW_PX } from './filter-pill'
import { Popover, Transition } from '@headlessui/react'
import { popover } from '../components/popover'

const BUFFER_RIGHT_PX = 16 - BUFFER_FOR_SHADOW_PX - PILL_X_GAP
const BUFFER_LEFT_PX = 16 - BUFFER_FOR_SHADOW_PX
@@ -96,24 +93,9 @@ interface FiltersBarProps {
export const FiltersBar = ({ elements }: FiltersBarProps) => {
const containerRef = useRef<HTMLDivElement>(null)
const pillsRef = useRef<HTMLDivElement>(null)
const seeMoreRef = useRef<HTMLDivElement>(null)
const [visibility, setVisibility] = useState<null | VisibilityState>(null)
const { query } = useQueryContext()

const [opened, setOpened] = useState(false)

useEffect(() => {
if (visibility?.visibleCount === query.filters.length) {
setOpened(false)
}
}, [visibility?.visibleCount, query.filters.length])

useOnClickOutside({
ref: seeMoreRef,
active: opened,
handler: () => setOpened(false)
})

useLayoutEffect(() => {
const { topBar, leftSection, rightSection } = elements

@@ -186,27 +168,34 @@ export const FiltersBar = ({ elements }: FiltersBarProps) => {
</div>
{visibility !== null &&
(query.filters.length !== visibility.visibleCount || canClear) && (
<ToggleDropdownButton
style={{
width: SEE_MORE_WIDTH_PX,
marginLeft: SEE_MORE_LEFT_MARGIN_PX,
marginRight: SEE_MORE_RIGHT_MARGIN_PX
}}
className="md:relative"
ref={seeMoreRef}
dropdownContainerProps={{
['title']: opened ? 'Show less' : 'Show more',
['aria-controls']: 'more-filters-menu',
['aria-expanded']: opened
}}
onClick={() => setOpened((opened) => !opened)}
currentOption={<EllipsisHorizontalIcon className="h-full w-full" />}
>
{opened ? (
<DropdownMenuWrapper
id="more-filters-menu"
className="md:right-auto"
innerContainerClassName="flex flex-col p-4 gap-y-2"
<Popover className="md:relative">
<Popover.Button
className={classNames(
'flex items-center justify-center rounded text-sm leading-tight',
'w-full bg-white dark:bg-gray-800 shadow text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-900'
)}
style={{
height: SEE_MORE_WIDTH_PX,
width: SEE_MORE_WIDTH_PX,
marginLeft: SEE_MORE_LEFT_MARGIN_PX,
marginRight: SEE_MORE_RIGHT_MARGIN_PX
}}
>
<EllipsisHorizontalIcon className="block h-5 w-5" />
</Popover.Button>
<Transition
{...popover.transition.props}
className={classNames(
'mt-2',
popover.transition.classNames.fullwidth,
'md:right-auto'
)}
>
<Popover.Panel
className={classNames(
popover.panel.classNames.roundedSheet,
'flex flex-col p-4 gap-y-2'
)}
>
{query.filters.length !== visibility.visibleCount && (
<AppliedFilterPillsList
@@ -219,9 +208,9 @@ export const FiltersBar = ({ elements }: FiltersBarProps) => {
/>
)}
{canClear && <ClearAction />}
</DropdownMenuWrapper>
) : null}
</ToggleDropdownButton>
</Popover.Panel>
</Transition>
</Popover>
)}
</div>
)