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