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) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 : () => {},
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in New Issue