Final updates to position dropdown rewrite with support for wrapping when being responsive to screen width
This commit is contained in:
parent
557290628a
commit
bdb3e3056d
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,9 +66,9 @@
|
|||
on:open={onOpen}
|
||||
on:close={onClose}
|
||||
portalTarget={appendTo}
|
||||
maxHeight={null}
|
||||
{anchor}
|
||||
{align}
|
||||
fitToScreen={false}
|
||||
>
|
||||
{#if isOpen}
|
||||
<DatePickerPopoverContents
|
||||
|
|
|
@ -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 : () => {},
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
align={$visibleColumns.length ? "right" : "left"}
|
||||
on:close={close}
|
||||
maxHeight={null}
|
||||
fitToScreen
|
||||
>
|
||||
<div class="content">
|
||||
<slot />
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue