mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Show label validation in a popover
So it looks the same as the assignee input. Both popovers now open above the input when there's room for the full list and below it otherwise, instead of switching sides depending on how many results there are.
This commit is contained in:
@@ -1,15 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
|
||||
import {
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
flip,
|
||||
hide,
|
||||
offset,
|
||||
shift,
|
||||
size,
|
||||
} from "@floating-ui/dom";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
import type { AssigneeSuggestion } from "@app/lib/assigneeSuggestions";
|
||||
@@ -17,6 +8,7 @@
|
||||
matchesAssignee,
|
||||
rankAssigneeSuggestions,
|
||||
} from "@app/lib/assigneeSuggestions";
|
||||
import { positionInputPopover } from "@app/lib/inputPopover";
|
||||
import { parseAssignee } from "@app/lib/inputValidation";
|
||||
import { cachedSearchAliases, invoke } from "@app/lib/invoke";
|
||||
import { portal } from "@app/lib/portal";
|
||||
@@ -131,33 +123,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (!panelVisible || !floatingEl || !anchorEl) return;
|
||||
const reference = anchorEl;
|
||||
const floating = floatingEl;
|
||||
|
||||
return autoUpdate(reference, floating, () => {
|
||||
void computePosition(reference, floating, {
|
||||
placement: "bottom-start",
|
||||
middleware: [
|
||||
offset(8),
|
||||
flip(),
|
||||
shift({ padding: 8 }),
|
||||
hide({ padding: 8 }),
|
||||
size({
|
||||
padding: 8,
|
||||
apply({ availableHeight, rects, elements }) {
|
||||
elements.floating.style.minWidth = `${rects.reference.width}px`;
|
||||
elements.floating.style.maxHeight = `${Math.min(availableHeight, 384)}px`;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}).then(({ x, y, middlewareData }) => {
|
||||
floating.style.left = `${x}px`;
|
||||
floating.style.top = `${y}px`;
|
||||
floating.style.visibility = middlewareData.hide?.referenceHidden
|
||||
? "hidden"
|
||||
: "visible";
|
||||
});
|
||||
});
|
||||
return positionInputPopover(anchorEl, floatingEl);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { positionInputPopover } from "@app/lib/inputPopover";
|
||||
import { labelError } from "@app/lib/inputValidation";
|
||||
import { portal } from "@app/lib/portal";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
@@ -31,6 +33,16 @@
|
||||
|
||||
let removeToggles: Record<string, boolean> = $state({});
|
||||
|
||||
let anchorEl: HTMLDivElement | undefined = $state();
|
||||
let floatingEl: HTMLDivElement | undefined = $state();
|
||||
|
||||
const panelVisible = $derived(showInput && validationMessage !== undefined);
|
||||
|
||||
$effect(() => {
|
||||
if (!panelVisible || !floatingEl || !anchorEl) return;
|
||||
return positionInputPopover(anchorEl, floatingEl);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Reset component state whenever the labels change in the parent. This
|
||||
// happens when the issue ID changes for example when the user navigates
|
||||
@@ -64,13 +76,23 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.popover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
z-index: 400;
|
||||
max-width: 28rem;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--border-radius-md);
|
||||
background-color: var(--color-surface-canvas);
|
||||
box-shadow: var(--elevation-low);
|
||||
}
|
||||
.validation-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: var(--color-feedback-error-text);
|
||||
position: relative;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
font: var(--txt-body-m-regular);
|
||||
}
|
||||
.removable-label {
|
||||
display: flex;
|
||||
@@ -113,15 +135,19 @@
|
||||
<div class="row">
|
||||
{#if showInput}
|
||||
<div class="input-row">
|
||||
<div style:flex="1" style:min-width="0">
|
||||
<div style:flex="1" style:min-width="0" bind:this={anchorEl}>
|
||||
<TextInput
|
||||
autofocus
|
||||
{valid}
|
||||
disabled={submitInProgress}
|
||||
placeholder="Add label"
|
||||
bind:value={inputValue}
|
||||
onSubmit={addLabel} />
|
||||
</div>
|
||||
{#if panelVisible}
|
||||
<div class="popover" bind:this={floatingEl} use:portal>
|
||||
<div class="validation-message">{validationMessage}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
@@ -160,10 +186,4 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if !valid && validationMessage}
|
||||
<div class="validation-message">
|
||||
<Icon name="warning" />{validationMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
hide,
|
||||
offset,
|
||||
shift,
|
||||
size,
|
||||
} from "@floating-ui/dom";
|
||||
|
||||
const gap = 8;
|
||||
const padding = 8;
|
||||
const maxHeight = 384;
|
||||
|
||||
export function positionInputPopover(
|
||||
reference: HTMLElement,
|
||||
floating: HTMLElement,
|
||||
): () => void {
|
||||
return autoUpdate(reference, floating, () => {
|
||||
const spaceAbove = reference.getBoundingClientRect().top - gap - padding;
|
||||
const placement = spaceAbove >= maxHeight ? "top-start" : "bottom-start";
|
||||
|
||||
void computePosition(reference, floating, {
|
||||
placement,
|
||||
middleware: [
|
||||
offset(gap),
|
||||
shift({ padding }),
|
||||
hide({ padding }),
|
||||
size({
|
||||
padding,
|
||||
apply({ availableHeight, rects, elements }) {
|
||||
elements.floating.style.minWidth = `${rects.reference.width}px`;
|
||||
elements.floating.style.maxHeight = `${Math.min(availableHeight, maxHeight)}px`;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}).then(({ x, y, middlewareData }) => {
|
||||
floating.style.left = `${x}px`;
|
||||
floating.style.top = `${y}px`;
|
||||
floating.style.visibility = middlewareData.hide?.referenceHidden
|
||||
? "hidden"
|
||||
: "visible";
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user