Add double layer context menu for overflow views

This commit is contained in:
Andrew Kingston 2024-08-16 18:43:48 +01:00
parent 40e7f58131
commit d377186f0d
No known key found for this signature in database
6 changed files with 156 additions and 42 deletions

View File

@ -115,6 +115,7 @@
}
.spectrum-ActionButton--quiet.is-selected {
color: var(--spectrum-global-color-gray-900);
background: var(--spectrum-global-color-gray-300);
}
.noPadding {
padding: 0;

View File

@ -1,14 +1,20 @@
<script>
import { setContext } from "svelte"
import { setContext, getContext } from "svelte"
import Popover from "../Popover/Popover.svelte"
import Menu from "../Menu/Menu.svelte"
export let disabled = false
export let align = "left"
export let portalTarget
export let openOnHover = false
export let animate
export let offset
const actionMenuContext = getContext("actionMenu")
let anchor
let dropdown
let timeout
// This is needed because display: contents is considered "invisible".
// It should only ever be an action button, so should be fine.
@ -16,11 +22,19 @@
anchor = node.firstChild
}
export const show = e => {
cancelHide()
dropdown.show()
}
export const hide = () => {
dropdown.hide()
}
export const show = () => {
dropdown.show()
// Hides this menu and all parent menus
const hideAll = () => {
hide()
actionMenuContext?.hide()
}
const openMenu = event => {
@ -30,12 +44,25 @@
}
}
setContext("actionMenu", { show, hide })
const queueHide = () => {
timeout = setTimeout(hide, 10)
}
const cancelHide = () => {
clearTimeout(timeout)
}
setContext("actionMenu", { show, hide, hideAll })
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div use:getAnchor on:click={openMenu}>
<div
use:getAnchor
on:click={openOnHover ? null : openMenu}
on:mouseenter={openOnHover ? show : null}
on:mouseleave={openOnHover ? queueHide : null}
>
<slot name="control" />
</div>
<Popover
@ -43,9 +70,13 @@
{anchor}
{align}
{portalTarget}
{animate}
{offset}
resizable={false}
on:open
on:close
on:mouseenter={openOnHover ? cancelHide : null}
on:mouseleave={openOnHover ? queueHide : null}
>
<Menu>
<slot />

View File

@ -151,9 +151,9 @@ export default function positionDropdown(element, opts) {
// Determine X strategy
if (align === "right") {
applyXStrategy(Strategies.EndToEnd)
} else if (align === "right-outside") {
} else if (align === "right-outside" || align === "right-context-menu") {
applyXStrategy(Strategies.StartToEnd)
} else if (align === "left-outside") {
} else if (align === "left-outside" || align === "left-context-menu") {
applyXStrategy(Strategies.EndToStart)
} else if (align === "center") {
applyXStrategy(Strategies.MidPoint)
@ -164,6 +164,12 @@ export default function positionDropdown(element, opts) {
// Determine Y strategy
if (align === "right-outside" || align === "left-outside") {
applyYStrategy(Strategies.MidPoint)
} else if (
align === "right-context-menu" ||
align === "left-context-menu"
) {
applyYStrategy(Strategies.StartToStart)
styles.top -= 4 // Manual adjustment for action menu padding
} else {
applyYStrategy(Strategies.StartToEnd)
}

View File

@ -27,7 +27,7 @@
const onClick = () => {
if (actionMenu && !noClose) {
actionMenu.hide()
actionMenu.hideAll()
}
dispatch("click")
}
@ -47,8 +47,9 @@
</div>
{/if}
<span class="spectrum-Menu-itemLabel"><slot /></span>
{#if keys?.length}
{#if keys?.length || $$slots.right}
<div class="keys">
<slot name="right" />
{#each keys as key}
<div class="key">
{#if key.startsWith("!")}

View File

@ -1,7 +1,7 @@
<script>
import "@spectrum-css/popover/dist/index-vars.css"
import Portal from "svelte-portal"
import { createEventDispatcher, getContext } from "svelte"
import { createEventDispatcher, getContext, onDestroy } from "svelte"
import positionDropdown from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition"
@ -28,7 +28,21 @@
export let resizable = true
export let wrap = false
const animationDuration = 260
let timeout
let blockPointerEvents = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
$: {
if (open && animate) {
blockPointerEvents = true
clearTimeout(timeout)
timeout = setTimeout(() => {
blockPointerEvents = false
}, animationDuration / 2)
}
}
export const show = () => {
dispatch("open")
@ -77,6 +91,10 @@
hide()
}
}
onDestroy(() => {
clearTimeout(timeout)
})
</script>
{#if open}
@ -104,9 +122,13 @@
class="spectrum-Popover is-open"
class:customZindex
class:hidden={!showPopover}
class:blockPointerEvents
role="presentation"
style="height: {customHeight}; --customZindex: {customZindex};"
transition:fly|local={{ y: -20, duration: animate ? 260 : 0 }}
transition:fly|local={{
y: -20,
duration: animate ? animationDuration : 0,
}}
on:mouseenter
on:mouseleave
>
@ -122,6 +144,9 @@
overflow: auto;
transition: opacity 260ms ease-out;
}
.blockPointerEvents {
pointer-events: none;
}
.hidden {
opacity: 0;
pointer-events: none;

View File

@ -6,7 +6,13 @@
contextMenuStore,
} from "stores/builder"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import { Icon, Button, Popover, ActionMenu, MenuItem } from "@budibase/bbui"
import {
Icon,
Button,
ActionButton,
ActionMenu,
MenuItem,
} from "@budibase/bbui"
import { params, url } from "@roxi/routify"
import EditViewModal from "./EditViewModal.svelte"
import DeleteViewModal from "./DeleteViewModal.svelte"
@ -19,12 +25,13 @@
import { alphabetical } from "components/backend/TableNavigator/utils"
import CreateViewModal from "./CreateViewModal.svelte"
import { onDestroy } from "svelte"
import { derived } from "svelte/store"
let viewContainer
// View overflow
let observer
let viewContainer
let viewVisibiltyMap = {}
let overflowPopover
let anchor
let overflowMenu
// Editing table
let createViewModal
@ -46,8 +53,15 @@
.filter(x => x.version === 2)
.slice()
.sort(alphabetical)
$: setUpObserver(viewContainer, views)
$: setUpObserver(views)
$: overflowedViews = views.filter(view => !viewVisibiltyMap[view.id])
$: viewHidden = viewVisibiltyMap[activeId] === false
const viewUrl = derived([url, params], ([$url, $params]) => viewId => {
return $url(`../${$params.tableId}/${encodeURIComponent(viewId)}`)
})
const tableUrl = derived(url, $url => tableId => $url(`../${tableId}`))
const openTableContextMenu = e => {
if (!tableEditable) {
@ -114,11 +128,24 @@
)
}
const setUpObserver = (viewContainer, views) => {
if (!views.length || !viewContainer) {
observer?.disconnect()
const editOverflowView = async view => {
editableView = view
await tick()
editViewModal?.show()
}
const deleteOverflowView = async view => {
editableView = view
await tick()
deleteViewModal?.show()
}
const setUpObserver = async views => {
observer?.disconnect()
if (!views.length) {
return
}
await tick()
observer = new IntersectionObserver(
entries => {
let updates = {}
@ -136,7 +163,9 @@
}
)
for (let child of viewContainer.children) {
observer.observe(child)
if (child.dataset.id) {
observer.observe(child)
}
}
}
@ -152,7 +181,7 @@
size="24"
/>
<a
href={$url(`../${tableId}`)}
href={$tableUrl(tableId)}
class="nav-item"
class:active={tableId === activeId}
on:contextmenu={openTableContextMenu}
@ -178,7 +207,7 @@
{#each views as view (view.id)}
{@const selectedBy = $userSelectedResourceMap[view.id]}
<a
href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)}
href={$viewUrl(view.id)}
class="nav-item"
class:active={view.id === activeId}
class:hidden={!viewVisibiltyMap[view.id]}
@ -209,27 +238,42 @@
</span>
{/if}
{#if overflowedViews.length}
<ActionMenu align="right">
<ActionMenu align="right" bind:this={overflowMenu}>
<div slot="control">
<Icon
name="ChevronDown"
size="XL"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
on:click={overflowPopover?.show}
/>
<ActionButton icon="ChevronDown" quiet selected={viewHidden}>
{overflowedViews.length} more
</ActionButton>
</div>
{#each overflowedViews as view}
<a
class="nav-overflow-item"
class:active={view.id === activeId}
href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)}
<ActionMenu
align="left-context-menu"
openOnHover
animate={false}
offset={-4}
>
<MenuItem>
{view.name}
<div slot="control">
<a
href={$viewUrl(view.id)}
class="nav-overflow-item"
class:active={view.id === activeId}
on:click={overflowMenu?.hide}
>
<MenuItem icon={viewHidden ? "Checkmark" : null}>
{view.name}
<Icon slot="right" name="ChevronRight" />
</MenuItem>
</a>
</div>
<a href={$viewUrl(view.id)}>
<MenuItem icon="Checkmark">Select</MenuItem>
</a>
<MenuItem icon="Edit" on:click={() => editOverflowView(view)}>
Edit
</MenuItem>
</a>
<MenuItem icon="Delete" on:click={() => deleteOverflowView(view)}>
Delete
</MenuItem>
</ActionMenu>
{/each}
</ActionMenu>
{/if}
@ -266,22 +310,23 @@
justify-content: flex-start;
align-items: center;
padding: 0 var(--spacing-xl);
gap: 8px;
gap: 12px;
}
.nav__views {
width: 0;
flex: 1 1 auto;
flex: 0 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
overflow: hidden;
gap: 8px;
margin-left: -4px;
}
/* Table and view items */
.nav-item {
padding: 6px 8px;
padding: 0 8px;
height: 32px;
border-radius: 4px;
display: flex;
flex-direction: row;
@ -309,4 +354,9 @@
white-space: nowrap;
overflow: hidden;
}
/* OVerflow items */
.nav-overflow-item:not(.active) :global(> .spectrum-Menu-item > .icon) {
visibility: hidden;
}
</style>