Merge pull request #403 from Budibase/colorpicker/use-portal

Colorpicker - Portal, Focus and Centering
This commit is contained in:
Conor_Mack 2020-06-30 13:32:36 +01:00 committed by GitHub
commit 7fc3aa5646
4 changed files with 168 additions and 132 deletions

View File

@ -13,16 +13,18 @@
import Palette from "./Palette.svelte"
import ButtonGroup from "./ButtonGroup.svelte"
import Input from "./Input.svelte"
import Portal from "./Portal.svelte"
export let value = "#3ec1d3ff"
export let open = false;
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false
export let format = "hexa"
export let open = false
export let style = ""
export let pickerHeight = 0
export let pickerWidth = 0
let colorPicker = null
let adder = null
let h = null
@ -38,6 +40,10 @@
getRecentColors()
}
if(colorPicker) {
colorPicker.focus()
}
if (format) {
convertAndSetHSVA()
}
@ -50,6 +56,12 @@
}
}
function handleEscape(e) {
if(open && e.key === "Escape") {
open = false
}
}
function setRecentColor(color) {
if (swatches.length === 12) {
swatches.splice(0, 1)
@ -145,85 +157,97 @@
}
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: style = buildStyle({ background: value, border })
$: selectedColorStyle = buildStyle({ background: value, border })
$: shrink = swatches.length > 0
</script>
<div
class="colorpicker-container"
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<Portal>
<div
class="colorpicker-container"
transition:fade
bind:this={colorPicker}
{style}
tabindex="0"
on:keydown={handleEscape}
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" {style} />
</CheckedBackground>
</div>
<div>
<Slider
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" style={selectedColorStyle} />
</CheckedBackground>
</div>
<div>
<Slider
type="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
</CheckedBackground>
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
on:dragend={dispatchValue} />
</CheckedBackground>
</div>
</div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div
bind:this={adder}
transition:fade
class="adder"
on:click={addSwatch}
class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input
{value}
on:input={event => handleColorInput(event.target.value)}
on:change={dispatchInputChange} />
</div>
</div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div
bind:this={adder}
transition:fade
class="adder"
on:click={addSwatch}
class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input
{value}
on:input={event => handleColorInput(event.target.value)}
on:change={dispatchInputChange} />
</div>
</div>
</div>
</Portal>
<style>
.colorpicker-container {
position: absolute;
outline: none;
z-index: 3;
display: flex;
font-size: 11px;
font-weight: 400;
flex-direction: column;
/* height: 265px; */
margin: 5px 0px;
height: auto;
width: 220px;
background: #ffffff;

View File

@ -1,7 +1,7 @@
<script>
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher, afterUpdate, beforeUpdate } from "svelte"
import { createEventDispatcher, beforeUpdate } from "svelte"
import { buildStyle } from "./helpers.js"
import { fade } from "svelte/transition"
@ -15,7 +15,8 @@
export let height = "25px"
let format = "hexa"
let dimensions = { top: 0, left: 0 }
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 }
let positionSide = "top"
let colorPreview = null
let previewHeight = null
@ -23,17 +24,8 @@
let pickerWidth = 0
let pickerHeight = 0
let anchorEl = null
let parentNodes = []
let errorMsg = null
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
top: `${dimensions.top}px`,
left: `${dimensions.left}px`,
})
const dispatch = createEventDispatcher()
beforeUpdate(() => {
@ -46,25 +38,6 @@
}
})
afterUpdate(() => {
if (colorPreview && colorPreview.offsetParent && !anchorEl) {
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
anchorEl = colorPreview.offsetParent
let curEl = colorPreview
let els = []
//Travel up dom tree from preview element to find parent elements that scroll
while (!anchorEl.isSameNode(curEl)) {
curEl = curEl.parentNode
let elOverflow = window
.getComputedStyle(curEl)
.getPropertyValue("overflow")
if (/scroll|auto/.test(elOverflow)) {
els.push(curEl)
}
}
parentNodes = els
}
})
function openColorpicker(event) {
if (colorPreview) {
@ -72,43 +45,45 @@
}
}
$: if (open && colorPreview) {
function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
$: if(open && colorPreview) {
const {
top: spaceAbove,
width,
bottom,
right,
left: spaceLeft,
left,
} = colorPreview.getBoundingClientRect()
const { innerHeight, innerWidth } = window
const { offsetLeft, offsetTop } = colorPreview
//get the scrollTop value for all scrollable parent elements
let scrollTop = parentNodes.reduce(
(scrollAcc, el) => (scrollAcc += el.scrollTop),
0
)
const spaceBelow = window.innerHeight - bottom
const previewCenter = previewWidth / 2
const spaceBelow = innerHeight - spaceAbove - previewHeight
const top =
spaceAbove > spaceBelow
? offsetTop - pickerHeight - scrollTop
: offsetTop + previewHeight - scrollTop
let y, x;
//TOO: Testing and Scroll Awareness for x Scroll
const spaceRight = innerWidth - spaceLeft + previewWidth
const left =
spaceRight > spaceLeft
? offsetLeft + previewWidth
: offsetLeft - pickerWidth
if(spaceAbove > spaceBelow) {
positionSide = "bottom"
y = (window.innerHeight - spaceAbove)
}else{
positionSide = "top"
y = bottom
}
dimensions = { top, left }
x = (left + previewCenter) - (pickerWidth / 2)
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) }
}
function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
[positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left}px`,
})
</script>
<div class="color-preview-container">
@ -124,19 +99,19 @@
</CheckedBackground>
{#if open}
<div transition:fade class="picker-container" style={pickerStyle}>
<Colorpicker
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
{swatches}
{disableSwatches}
{open} />
</div>
<Colorpicker
style={pickerStyle}
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
bind:open
{swatches}
{disableSwatches}
/>
<div on:click|self={() => (open = false)} class="overlay" />
{/if}
{:else}
@ -154,6 +129,7 @@
}
.color-preview {
cursor: pointer;
border-radius: 3px;
border: 1px solid #dedada;
}
@ -166,12 +142,12 @@
cursor: not-allowed;
}
.picker-container {
/* .picker-container {
position: absolute;
z-index: 3;
width: fit-content;
height: fit-content;
}
} */
.overlay {
position: fixed;

View File

@ -0,0 +1,37 @@
<script>
import { onMount } from "svelte";
export let target = document.body;
let targetEl;
let portal;
let componentInstance;
onMount(() => {
if (typeof target === "string") {
targetEl = document.querySelector(target);
// Force exit
if (targetEl === null) {
return () => {};
}
} else if (target instanceof HTMLElement) {
targetEl = target;
} else {
throw new TypeError(
`Unknown target type: ${typeof target}. Allowed types: String (CSS selector), HTMLElement.`
);
}
portal = document.createElement("div");
targetEl.appendChild(portal);
portal.appendChild(componentInstance);
return () => {
targetEl.removeChild(portal);
};
});
</script>
<div bind:this={componentInstance}>
<slot />
</div>

View File

@ -88,7 +88,6 @@
padding: 0;
display: flex;
flex-direction: column;
z-index: 5;
}
.preview-pane {