Maybe its not related to svelte, but if do some DOM changes after input is focused, for example
<input onfocus={() => toggleVisibiltyOfOtherElement = true } onblur={() => console.log("blur")} />
(toggleVisibiltyOfOtherElement
triggers dom changes)
then blur is also triggered. Which doesn't make any sense, since visually the input is still focused. This makes it impossible to show another element only when input is focused, because showing that element unfocuses the input
Any way to fix this?
app.svelte
<script>
import Component from "./Component.svelte"
let s = $state("");
let sFiler = $state();
let showBox = $state(false);
</script>
<input bind:this={sFilter} type="text" bind:value={s} placeholder="search..."
onfocus={() => showBox = true }
onblur={() => showBox = false }
/>
<Component bind:visible={showBox}>
box content
</Component>
component.svelte
<script>
import { clickoutside } from "./onclickoutside.svelte";
let { visible = $bindable(), children } = $props()
</script>
{#if visible}
<div use:clickoutside onclickoutside={() => visible = false}>
{@render children()}
</div>
{/if}
<style>
div{
position: fixed;
background: pink;
left: 0;
top: 40px;
width: 500px;
height: 200px;
}
</style>
onclickoutside.svelte.js
export const clickoutside = (node, ignore) => {
function listener(event) {
const target = event.target;
if (!event.target || (ignore && target.closest(ignore))) {
return;
}
if (node && !node.contains(target) && !event.defaultPrevented) {
node.dispatchEvent(new CustomEvent("clickoutside", { detail: { target } }))
}
}
document.addEventListener("click", listener, true)
return {
destroy() {
document.removeEventListener("click", listener, true)
}
}
}
Maybe its not related to svelte, but if do some DOM changes after input is focused, for example
<input onfocus={() => toggleVisibiltyOfOtherElement = true } onblur={() => console.log("blur")} />
(toggleVisibiltyOfOtherElement
triggers dom changes)
then blur is also triggered. Which doesn't make any sense, since visually the input is still focused. This makes it impossible to show another element only when input is focused, because showing that element unfocuses the input
Any way to fix this?
app.svelte
<script>
import Component from "./Component.svelte"
let s = $state("");
let sFiler = $state();
let showBox = $state(false);
</script>
<input bind:this={sFilter} type="text" bind:value={s} placeholder="search..."
onfocus={() => showBox = true }
onblur={() => showBox = false }
/>
<Component bind:visible={showBox}>
box content
</Component>
component.svelte
<script>
import { clickoutside } from "./onclickoutside.svelte";
let { visible = $bindable(), children } = $props()
</script>
{#if visible}
<div use:clickoutside onclickoutside={() => visible = false}>
{@render children()}
</div>
{/if}
<style>
div{
position: fixed;
background: pink;
left: 0;
top: 40px;
width: 500px;
height: 200px;
}
</style>
onclickoutside.svelte.js
export const clickoutside = (node, ignore) => {
function listener(event) {
const target = event.target;
if (!event.target || (ignore && target.closest(ignore))) {
return;
}
if (node && !node.contains(target) && !event.defaultPrevented) {
node.dispatchEvent(new CustomEvent("clickoutside", { detail: { target } }))
}
}
document.addEventListener("click", listener, true)
return {
destroy() {
document.removeEventListener("click", listener, true)
}
}
}
Share
Improve this question
edited Mar 28 at 17:18
Alex
asked Mar 28 at 15:42
AlexAlex
66.1k185 gold badges459 silver badges651 bronze badges
1
- Please provide a complete, minimal reproduction. – brunnerh Commented Mar 28 at 16:42
2 Answers
Reset to default 1blur
is not triggered.
The logic works as expected if focus is moved via tab, but if you click on the input, focus triggers on mouse-down, and a click is emitted on mouse-up which immediately causes the click outside logic to set the flag back to false.
You could for example add special handling for the input or move the boundary for click-outside higher up in the tree so it considers both the input and the content to show to be inside.
Now with the code you can see that the problem is what Brunnerh gives in his answer, the mouseup
after focus
triggers the clickoutside
custom event and hides the box. The easiest solution is to simply use onclick
instead of onfocus
:
<script>
import Component from "./Component.svelte"
let s = $state("");
let sFiler = $state();
let showBox = $state(false);
</script>
<input bind:this={sFilter} type="text" bind:value={s} placeholder="search..."
onclick={() => showBox = true }
onblur={() => showBox = false }
/>
<Component bind:visible={showBox}>
box content
</Component>
Keep in mind that if a user for some reason presses the mouse inside the input, then drags out of it and then releases the mouse, the click action is never triggered and the box won't show up. That's a very unexpected behavior but you can get around it by doing the following:
//App.svelte
<script>
import Component from "./Component.svelte"
let s = $state("");
let sFiler = $state();
let showBox = $state(false);
let inputMouseDown = $state(false)
function revealBox() {
if (!inputMouseDown) return
showBox = true
inputMouseDown = false;
}
</script>
<input bind:this={sFilter} type="text" bind:value={s} placeholder="search..."
onmousedown={() => inputMouseDown = true}
ontouchstart={() => inputMouseDown = true}
onblur={() => showBox = false }
/>
<svelte:window onmouseup={revealBox} ontouchend={revealBox}/>
<Component bind:visible={showBox} {inputMouseDown}>
box content
</Component>
//Component.svelte
<script>
import { clickoutside } from "./onclickoutside.svelte";
let { visible = $bindable(), inputMouseDown, children } = $props()
function hideBox() {
if (inputMousedown) return
visible = false
}
</script>
{#if visible}
<div use:clickoutside onclickoutside={hideBox}>
{@render children()}
</div>
{/if}
<style>
div{
position: fixed;
background: pink;
left: 0;
top: 40px;
width: 500px;
height: 200px;
}
</style>
Also added touch support. Here's a working REPL.