popup rework (#1904)

* popup rework

* feat(frontend): adapt all popup

* feat(frontend): fix build

* feat(frontend): fix calendar

* feat(frontend): Fix merge
This commit is contained in:
Faton Ramadani
2023-07-26 13:52:10 +03:00
committed by GitHub
parent 577522ca02
commit fc72f7e189
6 changed files with 185 additions and 343 deletions
@@ -4,7 +4,7 @@
import { forbiddenIds } from '$lib/components/flows/idUtils'
import { ArrowRight, Pencil } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { fade, slide } from 'svelte/transition'
import { slide } from 'svelte/transition'
import { Button, Popup } from '../../../../common'
const { app, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
@@ -13,7 +13,6 @@
const dispatch = createEventDispatcher()
const regex = /^[a-zA-Z][a-zA-Z0-9]*$/
let value = id
let button: HTMLButtonElement
let input: HTMLInputElement
let error = ''
@@ -39,25 +38,19 @@
}
</script>
<button
on:click|stopPropagation={() => {
$selectedComponent = [id]
}}
bind:this={button}
title="Edit ID"
class="flex items-center px-1 rounded-sm bg-surface-selected hover:text-primary text-tertiary"
aria-label="Open component ID editor"
>
<Pencil size={14} />
</button>
<Popup
ref={button}
options={{ placement: 'top-start' }}
transition={fade}
wrapperClasses="!z-[1002]"
outerClasses="rounded shadow-xl bg-surface border p-3"
on:close={() => (value = id)}
>
<Popup floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}>
<svelte:fragment slot="button">
<button
on:click={() => {
$selectedComponent = [id]
}}
title="Edit ID"
class="flex items-center px-1 rounded-sm bg-surface-secondary hover:text-primary text-secondary"
aria-label="Open component ID editor"
>
<Pencil size={14} />
</button>
</svelte:fragment>
<label class="block text-primary">
<div class="pb-1 text-sm text-secondary">Component ID</div>
<div class="flex w-full">
@@ -2,16 +2,13 @@
import type { StaticInput } from '../../../inputType'
import { Loader2 } from 'lucide-svelte'
import { ClearableInput, Popup } from '../../../../common'
import { fade } from 'svelte/transition'
export let componentInput: StaticInput<string>
let anchor: HTMLElement
let loading = false
let items: { label: string; icon: any }[]
let filteredItems: { label: string; icon: any }[]
let search = ''
let openPopup: () => void
$: if (search) {
filteredItems = items.filter((item) => {
@@ -23,7 +20,6 @@
async function getData() {
if (items) {
return openPopup()
}
loading = true
const data = await import('lucide-svelte/dist/svelte/icons')
@@ -33,7 +29,6 @@
.map(([key, icon]) => ({ label: key, icon }))
loading = false
openPopup()
}
function select(label: string) {
@@ -46,76 +41,70 @@
}
</script>
<div bind:this={anchor} class="relative">
<ClearableInput
readonly
value={componentInput.value}
on:change={({ detail }) => (componentInput.value = detail)}
on:focus={getData}
class="!pr-6"
/>
{#if loading}
<div class="center-center absolute right-2 top-1/2 transform -translate-y-1/2 p-0.5">
<Loader2 class="animate-spin" size={16} />
<Popup
let:close
floatingConfig={{
strategy: 'absolute',
placement: 'bottom-end'
}}
>
<svelte:fragment slot="button">
<div class="relative">
<ClearableInput
readonly
value={componentInput.value}
on:change={({ detail }) => (componentInput.value = detail)}
on:focus={getData}
class="!pr-6"
/>
{#if loading}
<div class="center-center absolute right-2 top-1/2 transform -translate-y-1/2 p-0.5">
<Loader2 class="animate-spin" size={16} />
</div>
{/if}
</div>
{/if}
</div>
{#if anchor}
<Popup
ref={anchor}
closeOn={[]}
options={{
placement: 'bottom-start',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 6] } }]
}}
bind:open={openPopup}
let:close
transition={fade}
wrapperClasses=" z-[1002]"
>
{#if !loading}
<div class="w-72 shadow-[0_10px_40px_-5px_rgba(0,0,0,0.25)] bg-surface rounded-md p-2">
{#if filteredItems}
<input
on:keydown={(event) => {
if (!['ArrowDown', 'ArrowUp'].includes(event.key)) {
event.stopPropagation()
}
}}
bind:value={search}
type="text"
placeholder="Search"
class="col-span-4 mb-2"
/>
<div class="grid gap-1 grid-cols-4 max-h-[300px] overflow-auto">
{#each filteredItems as { label, icon }}
<button
type="button"
title={label}
on:click={() => {
select(label)
close()
}}
class="w-full center-center flex-col font-normal p-1
hover:bg-gray-100 focus:bg-gray-100 rounded duration-200
</svelte:fragment>
{#if !loading}
{#if filteredItems}
<div class="w-72">
<input
on:keydown={(event) => {
if (!['ArrowDown', 'ArrowUp'].includes(event.key)) {
event.stopPropagation()
}
}}
bind:value={search}
type="text"
placeholder="Search"
class="col-span-4 mb-2"
/>
<div class="grid gap-1 grid-cols-4 max-h-[300px] overflow-auto">
{#each filteredItems as { label, icon }}
<button
type="button"
title={label}
on:click={() => {
select(label)
close(null)
}}
class="w-full center-center flex-col font-normal p-1
hover:bg-gray-100 focus:bg-gray-100 rounded duration-200 dark:hover:bg-frost-900 dark:focus:bg-frost-900
{label === componentInput.value ? 'text-blue-600 bg-blue-50 pointer-events-none' : ''}"
>
<svelte:component this={icon} size={22} />
<span class="inline-block w-full text-[10px] ellipsize pt-0.5">
{label}
</span>
</button>
{:else}
<div class="col-span-4 text-center text-secondary text-sm p-2">
No icons match your search
</div>
{/each}
</div>
{:else}
<div class="text-center text-sm text-secondary p-2"> Couldn't load options </div>
{/if}
>
<svelte:component this={icon} size={22} />
<span class="inline-block w-full text-[10px] ellipsize pt-0.5">
{label}
</span>
</button>
{:else}
<div class="col-span-4 text-center text-secondary text-sm p-2">
No icons match your search
</div>
{/each}
</div>
</div>
{:else}
<div class="text-center text-sm text-secondary p-2"> Couldn't load options </div>
{/if}
</Popup>
{/if}
{/if}
</Popup>
@@ -7,7 +7,6 @@
import type Editor from '../Editor.svelte'
import { faCheck, faClose, faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
import Popup from '../common/popup/Popup.svelte'
import { fade } from 'svelte/transition'
import { Icon } from 'svelte-awesome'
import { dbSchema, existsOpenaiResourcePath } from '$lib/stores'
import type DiffEditor from '../DiffEditor.svelte'
@@ -38,12 +37,6 @@
return
}
try {
// close popup ^^
const elem = document.activeElement as HTMLElement
if (elem.blur) {
elem.blur()
}
genLoading = true
if (isEdit && selection) {
const selectedCode = editor?.getSelectedLines() || ''
@@ -171,41 +164,40 @@
</Button>
</div>
{/if}
{:else if inlineScript}
<Button
size="lg"
bind:element={button}
color="light"
btnClasses="!px-2 !bg-surface-secondary hover:!bg-surface-hover"
loading={genLoading}
>
<Icon scale={0.8} data={faMagicWandSparkles} />
</Button>
{:else}
<Button
title="Generate code from prompt"
btnClasses="!font-medium text-scondary"
size="xs"
color="light"
spacingSize="md"
bind:element={button}
startIcon={{ icon: faMagicWandSparkles }}
{iconOnly}
loading={genLoading}
>
{isEdit ? 'AI Edit' : 'AI Gen'}
</Button>
{/if}
{#if !generatedCode && !genLoading}
<Popup
ref={button}
options={{ placement: 'top-start' }}
transition={fade}
closeOn={[]}
wrapperClasses="!z-[1002]"
outerClasses="rounded shadow-xl bg-surface border p-3 w-96"
>
<label class="block text-primary">
<Popup floatingConfig={{ placement: 'bottom-end', strategy: 'absolute' }}>
<svelte:fragment slot="button">
{#if inlineScript}
<Button
size="lg"
bind:element={button}
color="light"
btnClasses="!px-2 !bg-surface-secondary hover:!bg-surface-hover"
loading={genLoading}
nonCaptureEvent={true}
>
<Icon scale={0.8} data={faMagicWandSparkles} />
</Button>
{:else}
<Button
title="Generate code from prompt"
btnClasses="!font-medium text-secondary"
size="xs"
color="light"
spacingSize="md"
bind:element={button}
startIcon={{ icon: faMagicWandSparkles }}
{iconOnly}
loading={genLoading}
nonCaptureEvent={true}
>
{isEdit ? 'AI Edit' : 'AI Gen'}
</Button>
{/if}
</svelte:fragment>
<label class="block text-primary w-96">
<div class="pb-1 text-sm text-secondary">Prompt</div>
<div class="flex w-full">
<input
type="text"
@@ -234,11 +226,12 @@
</Button>
</div>
{#if lang === 'postgresql' && $dbSchema}
<p class="text-sm mt-2"
>Will take into account the DB schema <Tooltip
>In order to better generate the script, we pass the selected DB schema to GPT-4.</Tooltip
></p
>
<p class="text-sm mt-2">
Will take into account the DB schema
<Tooltip>
In order to better generate the script, we pass the selected DB schema to GPT-4.
</Tooltip>
</p>
{/if}
</label>
</Popup>
@@ -1,7 +1,6 @@
<script lang="ts">
import { ArrowRight, Calendar } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import { Button, Popup } from '..'
export let date: string | undefined
@@ -9,7 +8,6 @@
const dispatch = createEventDispatcher()
let value = date
let button: HTMLButtonElement
let input: HTMLInputElement
$: if (date && input) {
@@ -21,37 +19,37 @@
input.blur()
}
function close() {
const elem = document.activeElement as HTMLElement
if (elem.blur) {
elem.blur()
function bg(e: KeyboardEvent) {
if (e.key === 'Tab') {
e.stopPropagation()
}
}
</script>
<button
bind:this={button}
title="Open calendar picker"
class="absolute bottom-1 right-1 top-1 py-1 min-w-min !px-2 items-center text-gray-800 bg-surface border rounded center-center hover:bg-gray-50 transition-all cursor-pointer"
aria-label="Open calendar picker"
on:click={() => {
input.focus()
}}
>
<Calendar size={14} />
</button>
<Popup
ref={button}
options={{ placement: 'top-start' }}
transition={fade}
wrapperClasses="!z-[1002]"
outerClasses="rounded shadow-xl bg-surface border p-3"
closeOn={[]}
>
<Popup floatingConfig={{ placement: 'top-end', strategy: 'absolute' }} let:close>
<svelte:fragment slot="button">
<button
title="Open calendar picker"
class="absolute bottom-1 right-2 top-1 py-1 min-w-min !px-2 items-center text-primary bg-surface border rounded center-center hover:bg-surface-hover transition-all cursor-pointer"
aria-label="Open calendar picker"
on:click={() => {
input?.focus()
}}
>
<Calendar size={14} />
</button>
</svelte:fragment>
<label class="block text-primary">
<div class="pb-1 text-sm text-tertiary">{label}</div>
<div class="pb-1 text-sm text-secondary">{label}</div>
<div class="flex w-full">
<input type="datetime-local" bind:value class="!w-auto grow" bind:this={input} />
<input
type="datetime-local"
bind:value
class="!w-auto grow"
bind:this={input}
on:keydown={bg}
/>
<Button
size="xs"
color="dark"
@@ -60,7 +58,7 @@
aria-label="Save ID"
on:click={() => {
save()
close()
close(null)
}}
>
<ArrowRight size={18} />
@@ -1,173 +1,40 @@
<svelte:options accessors />
<script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte'
import { slide, type TransitionConfig } from 'svelte/transition'
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
import { clickOutside } from '../../../utils'
import { createStateMachine } from '../../../stateMachine'
import { Popover, PopoverButton, PopoverPanel, Transition } from '@rgossiaux/svelte-headlessui'
import Portal from 'svelte-portal'
import { createFloatingActions, type ComputeConfig } from 'svelte-floating-ui'
export let ref: HTMLElement | undefined
export let options: PopperOptions<any> = { placement: 'auto', strategy: 'fixed' }
/** Events on the reference element */
export let openOn: (keyof HTMLElementEventMap)[] = ['focus']
/** Events on the reference element */
export let closeOn: (keyof HTMLElementEventMap)[] = ['blur']
export let innerClasses = ''
export let outerClasses = ''
export let wrapperClasses = ''
export let transition: (node: Element, params?: Record<string, any>) => TransitionConfig = slide
export { openFocusIn as open, closed as close }
const states = ['closed', 'open-focus-in', 'open-focus-out'] as const
const stateMachine = createStateMachine(states, {
to: {
closed: ({ previousState, currentState }) => {
return isFocusContained() ? previousState : currentState
}
}
})
const [popperRef, popperContent, getInstance] = createPopperActions()
const dispatch = createEventDispatcher()
let popup: HTMLElement | undefined
let focusableElements: HTMLElement[]
function getFocusableElements() {
let elements: HTMLElement[] = []
popup
?.querySelectorAll<HTMLElement>(
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
)
.forEach((elem) => elements.push(elem))
focusableElements = elements.filter(
(el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')
)
focusableElements.forEach((el) => {
el.tabIndex = -1
el.addEventListener('click', openFocusIn)
el.addEventListener('blur', conditionalClosed)
})
export let floatingConfig: ComputeConfig = {
strategy: 'absolute',
placement: 'bottom-start'
}
function isFocusContained() {
const activeElem = document.activeElement
return popup?.contains(activeElem) || ref?.contains(activeElem)
}
export let containerClasses: string = 'rounded-lg shadow-lg p-4 bg-surface'
function closed() {
if ($stateMachine.currentState === 'open-focus-out') {
setTimeout(() => {
stateMachine.setState('closed')
dispatch('close')
}, 0)
} else {
stateMachine.setState('closed')
dispatch('close')
}
}
function conditionalClosed() {
setTimeout(() => {
if (isFocusContained()) return
closed()
}, 0)
}
function openFocusOut() {
stateMachine.setState('open-focus-out')
}
function openFocusIn() {
stateMachine.setState('open-focus-in')
}
function keyDown(event: KeyboardEvent & { currentTarget: EventTarget & Window }) {
const modifiers = ['Shift', 'Control', 'Command', 'Alt', 'Meta']
// Prevent closing the popup when the only key pressed is a modifier key
if (modifiers.includes(event.key) || $stateMachine.currentState === 'closed') return
if (event.key === 'Escape') {
;(<HTMLElement>document.activeElement)?.blur()
return conditionalClosed()
}
const prev = ['Up', 'Left']
const next = ['Down', 'Right']
if (![...prev, ...next].some((dir) => `Arrow${dir}` === event.key)) return
event.preventDefault()
if (popup?.contains(document.activeElement)) {
const index = focusableElements.findIndex((elem) => elem === document.activeElement)
if (index === -1) return
let targetIndex: number | undefined = undefined
if (prev.some((dir) => `Arrow${dir}` === event.key)) {
targetIndex = index === 0 ? focusableElements.length - 1 : index - 1
} else if (next.some((dir) => `Arrow${dir}` === event.key)) {
targetIndex = index + 1 === focusableElements.length ? 0 : index + 1
}
if (targetIndex !== undefined) {
focusableElements[targetIndex].focus()
stateMachine.setState('open-focus-in')
}
} else {
const elem = focusableElements[event.key === 'ArrowUp' ? focusableElements.length - 1 : 0]
if (elem) {
elem.focus()
stateMachine.setState('open-focus-in')
}
}
}
function addRefListeners() {
if (!ref) return
openOn.forEach((action) => ref!.addEventListener(action, openFocusOut))
closeOn.forEach((action) => ref!.addEventListener(action, closed))
}
function removeAllListeners() {
focusableElements?.forEach((el) => el.removeEventListener('click', openFocusIn))
focusableElements?.forEach((el) => el.removeEventListener('blur', conditionalClosed))
if (!ref) return
openOn.forEach((action) => ref!.removeEventListener(action, openFocusOut))
closeOn.forEach((action) => ref!.removeEventListener(action, closed))
}
$: if ($stateMachine.currentState === 'closed') {
focusableElements?.forEach((el) => el.removeEventListener('click', openFocusIn))
} else {
setTimeout(() => {
getFocusableElements()
getInstance()?.update()
}, 0)
}
$: if (ref) {
popperRef(ref)
addRefListeners()
}
$: $$slots.default && getInstance()?.update()
onDestroy(removeAllListeners)
const [floatingRef, floatingContent] = createFloatingActions(floatingConfig)
</script>
<svelte:window on:keydown={keyDown} />
<div
class="z-50 {wrapperClasses}"
bind:this={popup}
use:popperContent={options}
use:clickOutside
on:click_outside={closed}
aria-haspopup="true"
aria-expanded={$stateMachine.currentState !== 'closed'}
>
{#if $stateMachine.currentState !== 'closed'}
<div transition:transition|local={{ duration: 100 }} class={outerClasses}>
<div class={innerClasses}>
<slot open={openFocusIn} close={closed} />
</div>
<Popover let:open on:close>
<PopoverButton>
<div use:floatingRef>
<slot name="button" />
</div>
{/if}
</div>
</PopoverButton>
<Portal>
<div use:floatingContent class="z5000" hidden={!open}>
<Transition
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel let:close>
<div class={containerClasses}>
<slot {close} />
</div>
</PopoverPanel>
</Transition>
</div>
</Portal>
</Popover>
@@ -315,16 +315,18 @@
</div>
</div>
<div class="relative w-full">
<span class="text-xs absolute -top-4">Max datetime</span>
<input type="text" value={maxTs ?? 'zoom x axis to set max'} disabled />
<CalendarPicker
date={maxTs}
label="Max datetimes"
on:change={async ({ detail }) => {
maxTs = new Date(detail).toISOString()
jobs = await fetchJobs(maxTs, minTs)
}}
/>
<div class="flex gap-1 relative w-full">
<span class="text-xs absolute -top-4">Max datetime</span>
<input type="text" value={maxTs ?? 'zoom x axis to set max'} disabled />
<CalendarPicker
date={maxTs}
label="Max datetimes"
on:change={async ({ detail }) => {
maxTs = new Date(detail).toISOString()
jobs = await fetchJobs(maxTs, minTs)
}}
/>
</div>
</div>
<Button
color="light"