mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
fix: prevent layout shift when ComboBox dropdown opens inside modal
Two root causes of the shift were identified and fixed:
1. Melt UI's `usePopper` applies `position: absolute` to the dropdown
via floating-ui after a `tick()` delay. During that one frame the
`ul` was briefly in normal document flow, momentarily growing the
`<dialog>` element and causing a reflow. Pre-setting
`[data-melt-combobox-menu] { position: absolute }` in base.css
removes the element from flow immediately at mount.
2. Melt UI's ComboBox has `preventScroll: true` by default. On open it
calls `removeScroll()` which sets `body.overflow: hidden` and adds
compensatory `padding-right` for the scrollbar width — a body
reflow that shifts the dialog. Pre-setting `data-melt-scroll-lock`
on the body in `modal.svelte` tells Melt UI the lock is already
active so it returns early without touching the body.
Also fix `on:search` on `Input.ComboBox` (which the component never
dispatches) by replacing it with native `oninput`/`onfocusin` handlers
on the wrapper `<div>`, so typed text actually triggers debounced
server-side project search. Projects are now ordered newest-first
(`Query.orderDesc('')`) matching the org projects page.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,16 @@
|
||||
$: if (error) {
|
||||
alert?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
|
||||
}
|
||||
|
||||
// Melt UI's ComboBox (and other listbox-based components) call removeScroll()
|
||||
// when they open, which adds overflow:hidden + compensatory padding-right
|
||||
$: if (typeof document !== 'undefined') {
|
||||
if (show) {
|
||||
document.body.setAttribute('data-melt-scroll-lock', '');
|
||||
} else {
|
||||
document.body.removeAttribute('data-melt-scroll-lock');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form isModal {onSubmit}>
|
||||
|
||||
@@ -4,3 +4,15 @@
|
||||
scrollbar-width: thin;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/*
|
||||
* Melt UI applies `position: absolute` to the combobox menu via floating-ui,
|
||||
* but only after `tick()` — a one-frame delay. During that initial frame the
|
||||
* menu is in the normal document flow, momentarily expanding any ancestor with
|
||||
* `overflow: visible` (e.g. the <dialog> element) and causing a visible layout
|
||||
* shift. Pre-setting `position: absolute` here removes the element from flow
|
||||
* immediately at mount, so there is no reflow before floating-ui takes over.
|
||||
*/
|
||||
[data-melt-combobox-menu] {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
rowSearching[index] = true;
|
||||
const queries: string[] = [
|
||||
Query.limit(25),
|
||||
Query.orderDesc(''),
|
||||
Query.equal('teamId', $organization.$id)
|
||||
];
|
||||
if (search) {
|
||||
@@ -83,7 +84,16 @@
|
||||
<Layout.Stack gap="s">
|
||||
{#each projectAccess as access, i}
|
||||
<Layout.Stack direction="row" gap="s" alignItems="flex-end">
|
||||
<div style:flex="1">
|
||||
<div
|
||||
style:flex="1"
|
||||
onfocusin={() => {
|
||||
if (!rowOptions[i]?.length) loadProjects(i);
|
||||
}}
|
||||
oninput={(e) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
getDebouncedSearch(i)(e.target.value);
|
||||
}
|
||||
}}>
|
||||
<Input.ComboBox
|
||||
id="project-{i}"
|
||||
label={i === 0 ? 'Project' : ''}
|
||||
@@ -91,9 +101,9 @@
|
||||
placeholder="Search projects"
|
||||
bind:value={access.projectId}
|
||||
options={rowOptions[i] ?? []}
|
||||
noResultsOption={rowSearching[i] ? { disabled: true, message: 'Searching...' } : undefined}
|
||||
on:search={(e) => getDebouncedSearch(i)(e.detail)}
|
||||
on:focus={() => { if (!rowOptions[i]?.length) loadProjects(i); }} />
|
||||
noResultsOption={rowSearching[i]
|
||||
? { disabled: true, message: 'Searching...' }
|
||||
: undefined} />
|
||||
</div>
|
||||
<div style:width="140px">
|
||||
<InputSelect
|
||||
|
||||
Reference in New Issue
Block a user