From bdb3e3056d2a6e29369780528bef9322b2888c84 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 25 Apr 2024 12:49:25 +0100 Subject: [PATCH] Final updates to position dropdown rewrite with support for wrapping when being responsive to screen width --- .../bbui/src/Actions/position_dropdown.js | 209 +++++++++++++----- .../Form/Core/DatePicker/DatePicker.svelte | 2 +- packages/bbui/src/Popover/Popover.svelte | 6 +- .../components/grid/cells/HeaderCell.svelte | 8 +- .../grid/layout/NewColumnButton.svelte | 1 + .../grid/overlays/GridPopover.svelte | 5 +- 6 files changed, 169 insertions(+), 62 deletions(-) diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 5ce35d15e0..8630c22770 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -1,3 +1,24 @@ +/** + * Valid alignment options are + * - left + * - right + * - left-outside + * - right-outside + **/ + +import { off } from "process" + +// Strategies are defined as [Popover]To[Anchor]. +// They can apply for both horizontal and vertical alignment. +const Strategies = { + StartToStart: "StartToStart", // e.g. left alignment + EndToEnd: "EndToEnd", // e.g. right alignment + StartToEnd: "StartToEnd", // e.g. right-outside alignment + EndToStart: "EndToStart", // e.g. left-outside alignment + MidPoint: "MidPoint", // centers relative to midpoints + ScreenEdge: "ScreenEdge", // locks to screen edge +} + export default function positionDropdown(element, opts) { let resizeObserver let latestOpts = opts @@ -19,7 +40,8 @@ export default function positionDropdown(element, opts) { useAnchorWidth, offset = 5, customUpdate, - noShrink, + fitToScreen, + wrap, } = opts if (!anchor) { return @@ -28,85 +50,158 @@ export default function positionDropdown(element, opts) { // Compute bounds const anchorBounds = anchor.getBoundingClientRect() const elementBounds = element.getBoundingClientRect() - const padding = 8 + const winWidth = window.innerWidth + const winHeight = window.innerHeight + const screenOffset = 8 let styles = { - maxHeight: null, - minWidth, - maxWidth, + maxHeight, + minWidth: useAnchorWidth ? anchorBounds.width : minWidth, + maxWidth: useAnchorWidth ? anchorBounds.width : maxWidth, left: null, top: null, } + // Ignore all our logic for custom logic if (typeof customUpdate === "function") { styles = customUpdate(anchorBounds, elementBounds, { ...styles, offset: opts.offset, }) - } else { - // "outside" alignment (popover vertical center = anchor vertical center) - if (align === "right-outside" || align === "left-outside") { - styles.top = - anchorBounds.top + anchorBounds.height / 2 - elementBounds.height / 2 - styles.maxHeight = maxHeight + } + + // Otherwise position ourselves as normal + else { + // Checks if we overflow off the screen. We only report that we overflow + // when the alternative dimension is larger than the one we are checking. + const doesXOverflow = () => { + const overflows = styles.left + elementBounds.width > winWidth + return overflows && anchorBounds.left > winWidth - anchorBounds.right + } + const doesYOverflow = () => { + const overflows = styles.top + elementBounds.height > winHeight + return overflows && anchorBounds.top > winHeight - anchorBounds.bottom } - // Normal left/right alignment (top popover edge = botom anchor edge) - else { - styles.top = anchorBounds.bottom + offset - styles.maxHeight = maxHeight || window.innerHeight - styles.top + // Applies a dynamic max height constraint if appropriate + const applyMaxHeight = height => { + if (!styles.maxHeight && fitToScreen) { + styles.maxHeight = height + } } - // Determine horizontal styles - // Use anchor width if required - if (!maxWidth && useAnchorWidth) { - styles.maxWidth = anchorBounds.width - } - if (useAnchorWidth) { - styles.minWidth = anchorBounds.width + // Applies the X strategy to our styles + const applyXStrategy = strategy => { + switch (strategy) { + case Strategies.StartToStart: + default: + styles.left = anchorBounds.left + break + case Strategies.EndToEnd: + styles.left = anchorBounds.right - elementBounds.width + break + case Strategies.StartToEnd: + styles.left = anchorBounds.right + offset + break + case Strategies.EndToStart: + styles.left = anchorBounds.left - elementBounds.width - offset + break + case Strategies.MidPoint: + styles.left = + anchorBounds.left + + anchorBounds.width / 2 - + elementBounds.width / 2 + break + case Strategies.ScreenEdge: + styles.left = winWidth - elementBounds.width - screenOffset + break + } } - // Right alignment (right popover edge = right anchor edge) + // Applies the Y strategy to our styles + const applyYStrategy = strategy => { + switch (strategy) { + case Strategies.StartToStart: + styles.top = anchorBounds.top + applyMaxHeight(winHeight - anchorBounds.top - screenOffset) + break + case Strategies.EndToEnd: + styles.top = anchorBounds.bottom - elementBounds.height + applyMaxHeight(anchorBounds.bottom - screenOffset) + break + case Strategies.StartToEnd: + default: + styles.top = anchorBounds.bottom + offset + applyMaxHeight(winHeight - anchorBounds.bottom - screenOffset) + break + case Strategies.EndToStart: + styles.top = anchorBounds.top - elementBounds.height - offset + applyMaxHeight(anchorBounds.top - screenOffset) + break + case Strategies.MidPoint: + styles.top = + anchorBounds.top + + anchorBounds.height / 2 - + elementBounds.height / 2 + break + case Strategies.ScreenEdge: + styles.top = winHeight - elementBounds.height - screenOffset + applyMaxHeight(winHeight - 2 * screenOffset) + break + } + } + + // Determine X strategy if (align === "right") { - styles.left = - anchorBounds.left + anchorBounds.width - elementBounds.width + applyXStrategy(Strategies.EndToEnd) + } else if (align === "right-outside") { + applyXStrategy(Strategies.StartToEnd) + } else if (align === "left-outside") { + applyXStrategy(Strategies.EndToStart) + } else { + applyXStrategy(Strategies.StartToStart) } - // Right outside alignment (left popover edge = right anchor edge) - else if (align === "right-outside") { - styles.left = anchorBounds.right + offset - } - - // Left outside alignment (right popover edge = left anchor edge) - else if (align === "left-outside") { - styles.left = anchorBounds.left - elementBounds.width - offset - } - - // Left alignment by default (left popover edge = left anchor edge) - else { - styles.left = anchorBounds.left - } - - // Remove max height restriction if we don't want to shrink - if (noShrink) { - delete styles.maxHeight + // Determine Y strategy + if (align === "right-outside" || align === "left-outside") { + applyYStrategy(Strategies.MidPoint) + } else { + applyYStrategy(Strategies.StartToEnd) } // Handle screen overflow - // Check right overflow - if (styles.left + elementBounds.width > window.innerWidth) { - styles.left = window.innerWidth - elementBounds.width - padding + if (doesXOverflow()) { + // Swap left to right + if (align === "left") { + applyXStrategy(Strategies.EndToEnd) + } + // Swap right-outside to left-outside + else if (align === "right-outside") { + applyXStrategy(Strategies.EndToStart) + } } - // Check bottom overflow - if (styles.top + elementBounds.height > window.innerHeight) { - styles.top = window.innerHeight - elementBounds.height - padding - - // If we overflowed off the bottom and therefore locked to the bottom - // edge, we might now be covering the anchor. Therefore we can try - // moving left or right to reveal the full anchor again. - if (anchorBounds.right + elementBounds.width < window.innerWidth) { - styles.left = anchorBounds.right - } else if (anchorBounds.left - elementBounds.width > 0) { - styles.left = anchorBounds.left - elementBounds.width + if (doesYOverflow()) { + // If wrapping, lock to the bottom of the screen and also reposition to + // the side to not block the anchor + if (wrap) { + applyYStrategy(Strategies.MidPoint) + if (doesYOverflow()) { + applyYStrategy(Strategies.ScreenEdge) + } + applyXStrategy(Strategies.StartToEnd) + if (doesXOverflow()) { + applyXStrategy(Strategies.EndToStart) + } + } + // Othewise invert as normal + else { + // If using an outside strategy then lock to the bottom of the screen + if (align === "left-outside" || align === "right-outside") { + applyYStrategy(Strategies.ScreenEdge) + } + // Otherwise flip above + else { + applyYStrategy(Strategies.EndToStart) + } } } } diff --git a/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte index 4587195865..28f1b1c9e0 100644 --- a/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte @@ -66,9 +66,9 @@ on:open={onOpen} on:close={onClose} portalTarget={appendTo} - maxHeight={null} {anchor} {align} + fitToScreen={false} > {#if isOpen} {}, diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index da9d179904..6cc21fb6f5 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -303,7 +303,13 @@ {#if open} - + {#if editIsOpen}
diff --git a/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte b/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte index 970a645a74..3e2a3cf128 100644 --- a/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte @@ -39,6 +39,7 @@ align={$visibleColumns.length ? "right" : "left"} on:close={close} maxHeight={null} + fitToScreen >
diff --git a/packages/frontend-core/src/components/grid/overlays/GridPopover.svelte b/packages/frontend-core/src/components/grid/overlays/GridPopover.svelte index d8b8b0fdae..06fb4f04a5 100644 --- a/packages/frontend-core/src/components/grid/overlays/GridPopover.svelte +++ b/packages/frontend-core/src/components/grid/overlays/GridPopover.svelte @@ -13,6 +13,8 @@ export let maxHeight = PopoverMaxHeight export let align = "left" export let open = true + export let fitToScreen = false + export let wrap = true const { gridID } = getContext("grid") const dispatch = createEventDispatcher() @@ -38,7 +40,8 @@ {open} {anchor} {align} - noShrink + {fitToScreen} + {wrap} portalTarget="#{gridID} .grid-popover-container" offset={0} >