Update pickers to use absolutely positioned root popover so that overflow does not matter

This commit is contained in:
Andrew Kingston 2023-01-12 09:18:17 +00:00
parent 0fe295c367
commit 3058fcbc35
4 changed files with 169 additions and 135 deletions

View File

@ -1,4 +1,4 @@
const ignoredClasses = [".flatpickr-calendar", ".modal-container"] const ignoredClasses = [".flatpickr-calendar"]
let clickHandlers = [] let clickHandlers = []
/** /**

View File

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

View File

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

View File

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