Update pickers to use absolutely positioned root popover so that overflow does not matter
This commit is contained in:
parent
0fe295c367
commit
3058fcbc35
|
@ -1,4 +1,4 @@
|
||||||
const ignoredClasses = [".flatpickr-calendar", ".modal-container"]
|
const ignoredClasses = [".flatpickr-calendar"]
|
||||||
let clickHandlers = []
|
let clickHandlers = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
export default function positionDropdown(element, { anchor, align, maxWidth }) {
|
export default function positionDropdown(
|
||||||
|
element,
|
||||||
|
{ anchor, align, maxWidth, useAnchorWidth }
|
||||||
|
) {
|
||||||
let positionSide = "top"
|
let positionSide = "top"
|
||||||
let maxHeight = 0
|
let maxHeight = 0
|
||||||
|
let minWidth = 0
|
||||||
let dimensions = getDimensions(anchor)
|
let dimensions = getDimensions(anchor)
|
||||||
|
|
||||||
function getDimensions() {
|
function getDimensions() {
|
||||||
|
@ -14,8 +18,7 @@ export default function positionDropdown(element, { anchor, align, maxWidth }) {
|
||||||
const containerRect = element.getBoundingClientRect()
|
const containerRect = element.getBoundingClientRect()
|
||||||
|
|
||||||
let y
|
let y
|
||||||
|
if (window.innerHeight - bottom < 100) {
|
||||||
if (spaceAbove > spaceBelow) {
|
|
||||||
positionSide = "bottom"
|
positionSide = "bottom"
|
||||||
maxHeight = spaceAbove - 20
|
maxHeight = spaceAbove - 20
|
||||||
y = window.innerHeight - spaceAbove + 5
|
y = window.innerHeight - spaceAbove + 5
|
||||||
|
@ -25,6 +28,13 @@ export default function positionDropdown(element, { anchor, align, maxWidth }) {
|
||||||
maxHeight = spaceBelow - 20
|
maxHeight = spaceBelow - 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!maxWidth && useAnchorWidth) {
|
||||||
|
maxWidth = width
|
||||||
|
}
|
||||||
|
if (useAnchorWidth) {
|
||||||
|
minWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[positionSide]: y,
|
[positionSide]: y,
|
||||||
left,
|
left,
|
||||||
|
@ -36,9 +46,9 @@ export default function positionDropdown(element, { anchor, align, maxWidth }) {
|
||||||
function calcLeftPosition() {
|
function calcLeftPosition() {
|
||||||
let left
|
let left
|
||||||
|
|
||||||
if (align == "right") {
|
if (align === "right") {
|
||||||
left = dimensions.left + dimensions.width - dimensions.containerWidth
|
left = dimensions.left + dimensions.width - dimensions.containerWidth
|
||||||
} else if (align == "right-side") {
|
} else if (align === "right-side") {
|
||||||
left = dimensions.left + dimensions.width
|
left = dimensions.left + dimensions.width
|
||||||
} else {
|
} else {
|
||||||
left = dimensions.left
|
left = dimensions.left
|
||||||
|
@ -52,7 +62,9 @@ export default function positionDropdown(element, { anchor, align, maxWidth }) {
|
||||||
if (maxWidth) {
|
if (maxWidth) {
|
||||||
element.style.maxWidth = `${maxWidth}px`
|
element.style.maxWidth = `${maxWidth}px`
|
||||||
}
|
}
|
||||||
element.style.minWidth = `${dimensions.width}px`
|
if (minWidth) {
|
||||||
|
element.style.minWidth = `${minWidth}px`
|
||||||
|
}
|
||||||
element.style.maxHeight = `${maxHeight.toFixed(0)}px`
|
element.style.maxHeight = `${maxHeight.toFixed(0)}px`
|
||||||
element.style.transformOrigin = `center ${positionSide}`
|
element.style.transformOrigin = `center ${positionSide}`
|
||||||
element.style[positionSide] = `${dimensions[positionSide]}px`
|
element.style[positionSide] = `${dimensions[positionSide]}px`
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import "@spectrum-css/picker/dist/index-vars.css"
|
import "@spectrum-css/picker/dist/index-vars.css"
|
||||||
import "@spectrum-css/popover/dist/index-vars.css"
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
import "@spectrum-css/menu/dist/index-vars.css"
|
import "@spectrum-css/menu/dist/index-vars.css"
|
||||||
import { fly } from "svelte/transition"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import clickOutside from "../../Actions/click_outside"
|
import clickOutside from "../../Actions/click_outside"
|
||||||
import Search from "./Search.svelte"
|
import Search from "./Search.svelte"
|
||||||
import Icon from "../../Icon/Icon.svelte"
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||||
|
import Popover from "../../Popover/Popover.svelte"
|
||||||
|
|
||||||
export let id = null
|
export let id = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -33,7 +33,10 @@
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let searchTerm = null
|
let searchTerm = null
|
||||||
|
let button
|
||||||
|
let popover
|
||||||
|
|
||||||
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
|
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
|
||||||
$: filteredOptions = getFilteredOptions(
|
$: filteredOptions = getFilteredOptions(
|
||||||
|
@ -76,77 +79,117 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:clickOutside={() => (open = false)}>
|
<button
|
||||||
<button
|
{id}
|
||||||
{id}
|
class="spectrum-Picker spectrum-Picker--sizeM"
|
||||||
class="spectrum-Picker spectrum-Picker--sizeM"
|
class:spectrum-Picker--quiet={quiet}
|
||||||
class:spectrum-Picker--quiet={quiet}
|
{disabled}
|
||||||
{disabled}
|
class:is-invalid={!!error}
|
||||||
class:is-invalid={!!error}
|
class:is-open={open}
|
||||||
class:is-open={open}
|
aria-haspopup="listbox"
|
||||||
aria-haspopup="listbox"
|
on:click={onClick}
|
||||||
on:click={onClick}
|
use:clickOutside={() => (open = false)}
|
||||||
>
|
bind:this={button}
|
||||||
{#if fieldIcon}
|
>
|
||||||
<span class="option-extra icon">
|
{#if fieldIcon}
|
||||||
<Icon size="S" name={fieldIcon} />
|
<span class="option-extra icon">
|
||||||
</span>
|
<Icon size="S" name={fieldIcon} />
|
||||||
{/if}
|
|
||||||
{#if fieldColour}
|
|
||||||
<span class="option-extra">
|
|
||||||
<StatusLight square color={fieldColour} />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<span
|
|
||||||
class="spectrum-Picker-label"
|
|
||||||
class:is-placeholder={isPlaceholder}
|
|
||||||
class:auto-width={autoWidth}
|
|
||||||
>
|
|
||||||
{fieldText}
|
|
||||||
</span>
|
</span>
|
||||||
{#if error}
|
{/if}
|
||||||
<svg
|
{#if fieldColour}
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
|
<span class="option-extra">
|
||||||
focusable="false"
|
<StatusLight square color={fieldColour} />
|
||||||
aria-hidden="true"
|
</span>
|
||||||
aria-label="Folder"
|
{/if}
|
||||||
>
|
<span
|
||||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
class="spectrum-Picker-label"
|
||||||
</svg>
|
class:is-placeholder={isPlaceholder}
|
||||||
{/if}
|
class:auto-width={autoWidth}
|
||||||
|
>
|
||||||
|
{fieldText}
|
||||||
|
</span>
|
||||||
|
{#if error}
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
aria-label="Folder"
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
{/if}
|
||||||
{#if open}
|
<svg
|
||||||
<div
|
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
focusable="false"
|
||||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
aria-hidden="true"
|
||||||
class:auto-width={autoWidth}
|
>
|
||||||
>
|
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||||
{#if autocomplete}
|
</svg>
|
||||||
<Search
|
</button>
|
||||||
value={searchTerm}
|
|
||||||
on:change={event => (searchTerm = event.detail)}
|
<Popover
|
||||||
{disabled}
|
anchor={button}
|
||||||
placeholder="Search"
|
align="left"
|
||||||
/>
|
portalTarget={document.documentElement}
|
||||||
|
bind:this={popover}
|
||||||
|
{open}
|
||||||
|
on:close={() => (open = false)}
|
||||||
|
useAnchorWidth={!autoWidth}
|
||||||
|
maxWidth={autoWidth ? 400 : null}
|
||||||
|
>
|
||||||
|
<div class="popover-content" class:auto-width={autoWidth}>
|
||||||
|
{#if autocomplete}
|
||||||
|
<Search
|
||||||
|
value={searchTerm}
|
||||||
|
on:change={event => (searchTerm = event.detail)}
|
||||||
|
{disabled}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
{#if placeholderOption}
|
||||||
|
<li
|
||||||
|
class="spectrum-Menu-item placeholder"
|
||||||
|
class:is-selected={isPlaceholder}
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() => onSelectOption(null)}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
{#if filteredOptions.length}
|
||||||
{#if placeholderOption}
|
{#each filteredOptions as option, idx}
|
||||||
<li
|
<li
|
||||||
class="spectrum-Menu-item placeholder"
|
class="spectrum-Menu-item"
|
||||||
class:is-selected={isPlaceholder}
|
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected="true"
|
aria-selected="true"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:click={() => onSelectOption(null)}
|
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||||
|
class:is-disabled={!isOptionEnabled(option)}
|
||||||
>
|
>
|
||||||
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
{#if getOptionIcon(option, idx)}
|
||||||
|
<span class="option-extra icon">
|
||||||
|
<Icon size="S" name={getOptionIcon(option, idx)} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if getOptionColour(option, idx)}
|
||||||
|
<span class="option-extra">
|
||||||
|
<StatusLight square color={getOptionColour(option, idx)} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
{getOptionLabel(option, idx)}
|
||||||
|
</span>
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
|
@ -155,61 +198,13 @@
|
||||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
</svg>
|
</svg>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/each}
|
||||||
{#if filteredOptions.length}
|
{/if}
|
||||||
{#each filteredOptions as option, idx}
|
</ul>
|
||||||
<li
|
</div>
|
||||||
class="spectrum-Menu-item"
|
</Popover>
|
||||||
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
|
||||||
role="option"
|
|
||||||
aria-selected="true"
|
|
||||||
tabindex="0"
|
|
||||||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
|
||||||
class:is-disabled={!isOptionEnabled(option)}
|
|
||||||
>
|
|
||||||
{#if getOptionIcon(option, idx)}
|
|
||||||
<span class="option-extra icon">
|
|
||||||
<Icon size="S" name={getOptionIcon(option, idx)} />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{#if getOptionColour(option, idx)}
|
|
||||||
<span class="option-extra">
|
|
||||||
<StatusLight square color={getOptionColour(option, idx)} />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<span class="spectrum-Menu-itemLabel">
|
|
||||||
{getOptionLabel(option, idx)}
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
|
||||||
</svg>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Popover {
|
|
||||||
max-height: 240px;
|
|
||||||
z-index: 999;
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
.spectrum-Popover:not(.auto-width) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.spectrum-Popover.auto-width :global(.spectrum-Menu-itemLabel) {
|
|
||||||
max-width: 400px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.spectrum-Picker {
|
.spectrum-Picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -229,9 +224,6 @@
|
||||||
.spectrum-Picker-label.auto-width.is-placeholder {
|
.spectrum-Picker-label.auto-width.is-placeholder {
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
.auto-width .spectrum-Menu-item {
|
|
||||||
padding-right: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icon and colour alignment */
|
/* Icon and colour alignment */
|
||||||
.spectrum-Menu-checkmark {
|
.spectrum-Menu-checkmark {
|
||||||
|
@ -245,26 +237,44 @@
|
||||||
margin: 0 -1px;
|
margin: 0 -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-Popover :global(.spectrum-Search) {
|
/* Popover */
|
||||||
|
.popover-content {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.popover-content.auto-width .spectrum-Menu-itemLabel {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.popover-content:not(.auto-width) .spectrum-Menu-itemLabel {
|
||||||
|
width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.popover-content.auto-width .spectrum-Menu-item {
|
||||||
|
padding-right: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.spectrum-Menu-item.is-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search styles inside popover */
|
||||||
|
.popover-content :global(.spectrum-Search) {
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
width: calc(100% + 2px);
|
width: calc(100% + 2px);
|
||||||
}
|
}
|
||||||
.spectrum-Popover :global(.spectrum-Search input) {
|
.popover-content :global(.spectrum-Search input) {
|
||||||
height: auto;
|
height: auto;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
padding-top: var(--spectrum-global-dimension-size-100);
|
padding-top: var(--spectrum-global-dimension-size-100);
|
||||||
padding-bottom: var(--spectrum-global-dimension-size-100);
|
padding-bottom: var(--spectrum-global-dimension-size-100);
|
||||||
}
|
}
|
||||||
.spectrum-Popover :global(.spectrum-Search .spectrum-ClearButton) {
|
.popover-content :global(.spectrum-Search .spectrum-ClearButton) {
|
||||||
right: 1px;
|
right: 1px;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
}
|
}
|
||||||
.spectrum-Popover :global(.spectrum-Search .spectrum-Textfield-icon) {
|
.popover-content :global(.spectrum-Search .spectrum-Textfield-icon) {
|
||||||
top: 9px;
|
top: 9px;
|
||||||
}
|
}
|
||||||
.spectrum-Menu-item.is-disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import positionDropdown from "../Actions/position_dropdown"
|
import positionDropdown from "../Actions/position_dropdown"
|
||||||
import clickOutside from "../Actions/click_outside"
|
import clickOutside from "../Actions/click_outside"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -12,9 +13,10 @@
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
export let dataCy
|
export let dataCy
|
||||||
export let maxWidth
|
export let maxWidth
|
||||||
|
|
||||||
export let direction = "bottom"
|
export let direction = "bottom"
|
||||||
export let showTip = false
|
export let showTip = false
|
||||||
|
export let open = false
|
||||||
|
export let useAnchorWidth = false
|
||||||
|
|
||||||
let tipSvg =
|
let tipSvg =
|
||||||
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
|
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
|
||||||
|
@ -35,13 +37,22 @@
|
||||||
|
|
||||||
const handleOutsideClick = e => {
|
const handleOutsideClick = e => {
|
||||||
if (open) {
|
if (open) {
|
||||||
e.stopPropagation()
|
// Stop propagation if the source is the anchor
|
||||||
|
let node = e.target
|
||||||
|
let fromAnchor = false
|
||||||
|
while (!fromAnchor && node && node.parentNode) {
|
||||||
|
fromAnchor = node === anchor
|
||||||
|
node = node.parentNode
|
||||||
|
}
|
||||||
|
if (fromAnchor) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the popover
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let open = null
|
|
||||||
|
|
||||||
function handleEscape(e) {
|
function handleEscape(e) {
|
||||||
if (open && e.key === "Escape") {
|
if (open && e.key === "Escape") {
|
||||||
hide()
|
hide()
|
||||||
|
@ -53,12 +64,13 @@
|
||||||
<Portal target={portalTarget}>
|
<Portal target={portalTarget}>
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
use:positionDropdown={{ anchor, align, maxWidth }}
|
use:positionDropdown={{ anchor, align, maxWidth, useAnchorWidth }}
|
||||||
use:clickOutside={handleOutsideClick}
|
use:clickOutside={handleOutsideClick}
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class={"spectrum-Popover is-open " + (tooltipClasses || "")}
|
class={"spectrum-Popover is-open " + (tooltipClasses || "")}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
>
|
>
|
||||||
{#if showTip}
|
{#if showTip}
|
||||||
{@html tipSvg}
|
{@html tipSvg}
|
||||||
|
|
Loading…
Reference in New Issue