1
0
Fork 0
mirror of synced 2024-10-02 01:56:57 +13:00

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) {
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
}
// Normal left/right alignment (top popover edge = botom anchor edge)
// Otherwise position ourselves as normal
else {
styles.top = anchorBounds.bottom + offset
styles.maxHeight = maxHeight || window.innerHeight - styles.top
// 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
}
// Determine horizontal styles
// Use anchor width if required
if (!maxWidth && useAnchorWidth) {
styles.maxWidth = anchorBounds.width
// Applies a dynamic max height constraint if appropriate
const applyMaxHeight = height => {
if (!styles.maxHeight && fitToScreen) {
styles.maxHeight = height
}
if (useAnchorWidth) {
styles.minWidth = anchorBounds.width
}
// Right alignment (right popover edge = right anchor edge)
if (align === "right") {
styles.left =
anchorBounds.left + anchorBounds.width - elementBounds.width
}
// 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 {
// 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
}
}
// Remove max height restriction if we don't want to shrink
if (noShrink) {
delete styles.maxHeight
// 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") {
applyXStrategy(Strategies.EndToEnd)
} else if (align === "right-outside") {
applyXStrategy(Strategies.StartToEnd)
} else if (align === "left-outside") {
applyXStrategy(Strategies.EndToStart)
} else {
applyXStrategy(Strategies.StartToStart)
}
// 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)
}
}
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)
}
// 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
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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}
>