Final updates to position dropdown rewrite with support for wrapping when being responsive to screen width

This commit is contained in:
Andrew Kingston 2024-04-25 12:49:25 +01:00
parent 557290628a
commit bdb3e3056d
6 changed files with 169 additions and 62 deletions

View File

@ -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) { export default function positionDropdown(element, opts) {
let resizeObserver let resizeObserver
let latestOpts = opts let latestOpts = opts
@ -19,7 +40,8 @@ export default function positionDropdown(element, opts) {
useAnchorWidth, useAnchorWidth,
offset = 5, offset = 5,
customUpdate, customUpdate,
noShrink, fitToScreen,
wrap,
} = opts } = opts
if (!anchor) { if (!anchor) {
return return
@ -28,85 +50,158 @@ export default function positionDropdown(element, opts) {
// Compute bounds // Compute bounds
const anchorBounds = anchor.getBoundingClientRect() const anchorBounds = anchor.getBoundingClientRect()
const elementBounds = element.getBoundingClientRect() const elementBounds = element.getBoundingClientRect()
const padding = 8 const winWidth = window.innerWidth
const winHeight = window.innerHeight
const screenOffset = 8
let styles = { let styles = {
maxHeight: null, maxHeight,
minWidth, minWidth: useAnchorWidth ? anchorBounds.width : minWidth,
maxWidth, maxWidth: useAnchorWidth ? anchorBounds.width : maxWidth,
left: null, left: null,
top: null, top: null,
} }
// Ignore all our logic for custom logic
if (typeof customUpdate === "function") { if (typeof customUpdate === "function") {
styles = customUpdate(anchorBounds, elementBounds, { styles = customUpdate(anchorBounds, elementBounds, {
...styles, ...styles,
offset: opts.offset, offset: opts.offset,
}) })
} else { }
// "outside" alignment (popover vertical center = anchor vertical center)
if (align === "right-outside" || align === "left-outside") { // Otherwise position ourselves as normal
styles.top = else {
anchorBounds.top + anchorBounds.height / 2 - elementBounds.height / 2 // Checks if we overflow off the screen. We only report that we overflow
styles.maxHeight = maxHeight // 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) // Applies a dynamic max height constraint if appropriate
else { const applyMaxHeight = height => {
styles.top = anchorBounds.bottom + offset if (!styles.maxHeight && fitToScreen) {
styles.maxHeight = maxHeight || window.innerHeight - styles.top styles.maxHeight = height
}
} }
// Determine horizontal styles // Applies the X strategy to our styles
// Use anchor width if required const applyXStrategy = strategy => {
if (!maxWidth && useAnchorWidth) { switch (strategy) {
styles.maxWidth = anchorBounds.width case Strategies.StartToStart:
} default:
if (useAnchorWidth) { styles.left = anchorBounds.left
styles.minWidth = anchorBounds.width 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") { if (align === "right") {
styles.left = applyXStrategy(Strategies.EndToEnd)
anchorBounds.left + anchorBounds.width - elementBounds.width } 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) // Determine Y strategy
else if (align === "right-outside") { if (align === "right-outside" || align === "left-outside") {
styles.left = anchorBounds.right + offset applyYStrategy(Strategies.MidPoint)
} } else {
applyYStrategy(Strategies.StartToEnd)
// 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
} }
// Handle screen overflow // Handle screen overflow
// Check right overflow if (doesXOverflow()) {
if (styles.left + elementBounds.width > window.innerWidth) { // Swap left to right
styles.left = window.innerWidth - elementBounds.width - padding if (align === "left") {
applyXStrategy(Strategies.EndToEnd)
}
// Swap right-outside to left-outside
else if (align === "right-outside") {
applyXStrategy(Strategies.EndToStart)
}
} }
// Check bottom overflow if (doesYOverflow()) {
if (styles.top + elementBounds.height > window.innerHeight) { // If wrapping, lock to the bottom of the screen and also reposition to
styles.top = window.innerHeight - elementBounds.height - padding // the side to not block the anchor
if (wrap) {
// If we overflowed off the bottom and therefore locked to the bottom applyYStrategy(Strategies.MidPoint)
// edge, we might now be covering the anchor. Therefore we can try if (doesYOverflow()) {
// moving left or right to reveal the full anchor again. applyYStrategy(Strategies.ScreenEdge)
if (anchorBounds.right + elementBounds.width < window.innerWidth) { }
styles.left = anchorBounds.right applyXStrategy(Strategies.StartToEnd)
} else if (anchorBounds.left - elementBounds.width > 0) { if (doesXOverflow()) {
styles.left = anchorBounds.left - elementBounds.width 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)
}
} }
} }
} }

View File

@ -66,9 +66,9 @@
on:open={onOpen} on:open={onOpen}
on:close={onClose} on:close={onClose}
portalTarget={appendTo} portalTarget={appendTo}
maxHeight={null}
{anchor} {anchor}
{align} {align}
fitToScreen={false}
> >
{#if isOpen} {#if isOpen}
<DatePickerPopoverContents <DatePickerPopoverContents

View File

@ -25,7 +25,8 @@
export let handlePostionUpdate export let handlePostionUpdate
export let showPopover = true export let showPopover = true
export let clickOutsideOverride = false export let clickOutsideOverride = false
export let noShrink = false export let fitToScreen = true
export let wrap = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
@ -92,7 +93,8 @@
useAnchorWidth, useAnchorWidth,
offset, offset,
customUpdate: handlePostionUpdate, customUpdate: handlePostionUpdate,
noShrink, fitToScreen,
wrap,
}} }}
use:clickOutside={{ use:clickOutside={{
callback: dismissible ? handleOutsideClick : () => {}, callback: dismissible ? handleOutsideClick : () => {},

View File

@ -303,7 +303,13 @@
</div> </div>
{#if open} {#if open}
<GridPopover {anchor} align="right" on:close={close} maxHeight={null}> <GridPopover
{anchor}
align="right"
on:close={close}
maxHeight={null}
fitToScreen
>
{#if editIsOpen} {#if editIsOpen}
<div class="content"> <div class="content">
<slot /> <slot />

View File

@ -39,6 +39,7 @@
align={$visibleColumns.length ? "right" : "left"} align={$visibleColumns.length ? "right" : "left"}
on:close={close} on:close={close}
maxHeight={null} maxHeight={null}
fitToScreen
> >
<div class="content"> <div class="content">
<slot /> <slot />

View File

@ -13,6 +13,8 @@
export let maxHeight = PopoverMaxHeight export let maxHeight = PopoverMaxHeight
export let align = "left" export let align = "left"
export let open = true export let open = true
export let fitToScreen = false
export let wrap = true
const { gridID } = getContext("grid") const { gridID } = getContext("grid")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -38,7 +40,8 @@
{open} {open}
{anchor} {anchor}
{align} {align}
noShrink {fitToScreen}
{wrap}
portalTarget="#{gridID} .grid-popover-container" portalTarget="#{gridID} .grid-popover-container"
offset={0} offset={0}
> >