mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-20 08:03:28 +00:00
finish keynav
This commit is contained in:
@@ -7,16 +7,42 @@ import { tabbable } from 'tabbable'
|
||||
* returned by `getNext`. preventDefault suppresses the browser's own
|
||||
* Enter→click so we don't double-fire.
|
||||
*
|
||||
* By default the handler bails inside `<textarea>` / contenteditable so
|
||||
* Enter keeps inserting a newline there. Pass `{ requireModifier: true }`
|
||||
* on textarea-bearing steps — then bare Enter still inserts a newline
|
||||
* and only Cmd/Ctrl+Enter triggers the advance.
|
||||
*
|
||||
* Pass `{ timeout: <ms> }` when the next container only renders as a
|
||||
* consequence of the click (e.g. a bottom panel gated on the freshly
|
||||
* set selection): both the `getNext()` lookup and the focus shift run
|
||||
* after the delay, giving the framework time to flush its render.
|
||||
*
|
||||
* {@attach arrowTabNav({ onKeyDown: selectAndAdvanceTo(() => nextEl) })}
|
||||
*/
|
||||
export function selectAndAdvanceTo(getNext: () => HTMLElement | undefined) {
|
||||
export function selectAndAdvanceTo(
|
||||
getNext: () => HTMLElement | undefined,
|
||||
opts: { requireModifier?: boolean; timeout?: number } = {}
|
||||
) {
|
||||
return (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Enter') return
|
||||
const t = e.target as HTMLElement | null
|
||||
if (!t || t.tagName === 'TEXTAREA' || t.isContentEditable) return
|
||||
if (!t) return
|
||||
if (opts.requireModifier) {
|
||||
if (!e.metaKey && !e.ctrlKey) return
|
||||
} else if (t.tagName === 'TEXTAREA' || t.isContentEditable) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
t.click()
|
||||
const next = getNext()
|
||||
if (next) tabbable(next)[0]?.focus()
|
||||
|
||||
const advance = () => {
|
||||
const next = getNext()
|
||||
if (next) tabbable(next)[0]?.focus()
|
||||
}
|
||||
if (opts.timeout && opts.timeout > 0) {
|
||||
setTimeout(advance, opts.timeout)
|
||||
} else {
|
||||
advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
// focus off to the first tabbable in the next column.
|
||||
let languageEl: HTMLElement | undefined
|
||||
let outputEl: HTMLElement | undefined
|
||||
// Refs into the bottom panel for the Output → Path → AI Prompt → Save chain.
|
||||
let pathEl: HTMLElement | undefined
|
||||
let aiPromptEl: HTMLElement | undefined
|
||||
let saveEl: HTMLElement | undefined
|
||||
|
||||
let compatibleKinds = $derived.by<PipelineOutputKind[]>(() => {
|
||||
if (!selected.language) return []
|
||||
@@ -143,9 +147,7 @@
|
||||
showBottomPanel ? 'h-[14rem] border-t p-4' : 'h-0'
|
||||
)}
|
||||
>
|
||||
{#if showBottomPanel}
|
||||
{@render bottomSection(close)}
|
||||
{/if}
|
||||
{@render bottomSection(close)}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
@@ -227,7 +229,7 @@
|
||||
'flex flex-col gap-1 p-2 grow w-56 overflow-auto transition-opacity',
|
||||
selected.triggerId && selected.language ? '' : 'opacity-20'
|
||||
)}
|
||||
{@attach arrowTabNav()}
|
||||
{@attach arrowTabNav({ onKeyDown: selectAndAdvanceTo(() => pathEl, { timeout: 50 }) })}
|
||||
>
|
||||
<div class="text-2xs font-normal text-secondary ml-2 mb-1">Output asset</div>
|
||||
{#each visibleOutputKinds.length ? visibleOutputKinds : PIPELINE_OUTPUT_KINDS as k}
|
||||
@@ -253,7 +255,8 @@
|
||||
|
||||
{#snippet bottomSection(close: () => void)}
|
||||
<Label label="Path">
|
||||
<div class="flex">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div bind:this={pathEl} class="flex" onkeydown={selectAndAdvanceTo(() => aiPromptEl)}>
|
||||
<div
|
||||
class="border rounded-md rounded-r-none border-r-0 text-xs w-fit flex items-center px-2 text-secondary bg-surface-input"
|
||||
>
|
||||
@@ -263,20 +266,28 @@
|
||||
</div>
|
||||
</Label>
|
||||
<Label label="AI Prompt (optional)">
|
||||
<TextInput
|
||||
class="resize-none h-12 !max-h-12"
|
||||
underlyingInputEl="textarea"
|
||||
inputProps={{
|
||||
placeholder: 'Describe what the script should do — leave empty to use a template'
|
||||
}}
|
||||
bind:value={selected.aiPrompt}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={aiPromptEl}
|
||||
onkeydown={(e) => (e.metaKey || e.ctrlKey) && e.key === 'Enter' && confirm(close)}
|
||||
>
|
||||
<TextInput
|
||||
class="resize-none h-12 !max-h-12"
|
||||
underlyingInputEl="textarea"
|
||||
inputProps={{
|
||||
placeholder: 'Describe what the script should do — leave empty to use a template'
|
||||
}}
|
||||
bind:value={selected.aiPrompt}
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<Button
|
||||
variant="accent"
|
||||
btnClasses="w-fit ml-auto"
|
||||
disabled={!selected.scriptPath.trim()}
|
||||
onClick={() => confirm(close)}
|
||||
shortCut={{ Icon: CornerDownLeft, withoutModifier: true }}>Create</Button
|
||||
>
|
||||
<div bind:this={saveEl} class="flex">
|
||||
<Button
|
||||
variant="accent"
|
||||
btnClasses="w-fit ml-auto"
|
||||
disabled={!selected.scriptPath.trim()}
|
||||
onClick={() => confirm(close)}
|
||||
shortCut={{ Icon: CornerDownLeft, withoutModifier: true }}>Create</Button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
Reference in New Issue
Block a user