refactor(admin): migrate dropdowns to clickOutside action

Updated components to use the new clickOutside action instead of manual
event listener management:
- ProjectListItem: Add clickOutside action and dropdown coordination
- AdminSegmentedController: Replace $effect with clickOutside action
- BubbleTextStyleMenu: Simplify click-outside handling
- BubbleColorPicker: Simplify click-outside handling
- Posts/Projects pages: Remove redundant page-level click handlers

The clickOutside action provides a cleaner, more maintainable way to
handle click-outside behavior with proper lifecycle management.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-10-07 21:58:34 -07:00
parent 97a80d9c3e
commit 9cc7baddc6
6 changed files with 27 additions and 83 deletions

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores'
import BaseSegmentedController from './BaseSegmentedController.svelte'
import { clickOutside } from '$lib/actions/clickOutside'
const currentPath = $derived($page.url.pathname)
@ -34,20 +35,9 @@
: ''
)
// Close dropdown when clicking outside
$effect(() => {
function handleClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.closest('.dropdown-container')) {
showDropdown = false
}
}
if (showDropdown) {
document.addEventListener('click', handleClickOutside)
return () => document.removeEventListener('click', handleClickOutside)
}
})
function handleClickOutside() {
showDropdown = false
}
</script>
<nav class="admin-segmented-controller">
@ -66,7 +56,11 @@
{/snippet}
</BaseSegmentedController>
<div class="dropdown-container">
<div
class="dropdown-container"
use:clickOutside={{ enabled: showDropdown }}
on:clickoutside={handleClickOutside}
>
<button
class="dropdown-trigger"
onclick={() => (showDropdown = !showDropdown)}

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation'
import { createEventDispatcher, onMount } from 'svelte'
import AdminByline from './AdminByline.svelte'
import { clickOutside } from '$lib/actions/clickOutside'
import type { AdminProject } from '$lib/types/admin'
@ -48,6 +49,10 @@
function handleToggleDropdown(event: MouseEvent) {
event.stopPropagation()
// Close all other dropdowns before toggling this one
if (!isDropdownOpen) {
document.dispatchEvent(new CustomEvent('closeDropdowns'))
}
isDropdownOpen = !isDropdownOpen
}
@ -63,6 +68,10 @@
dispatch('delete', { project })
}
function handleClickOutside() {
isDropdownOpen = false
}
onMount(() => {
function handleCloseDropdowns() {
isDropdownOpen = false
@ -99,7 +108,11 @@
/>
</div>
<div class="dropdown-container">
<div
class="dropdown-container"
use:clickOutside={{ enabled: isDropdownOpen }}
on:clickoutside={handleClickOutside}
>
<button
class="action-button"
type="button"

View file

@ -1,6 +1,7 @@
<script lang="ts">
import type { Editor } from '@tiptap/core'
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker'
import { clickOutside } from '$lib/actions/clickOutside'
interface Props {
editor: Editor
@ -97,30 +98,10 @@
applyColor(color)
onClose()
}
function handleClickOutside(e: MouseEvent) {
if (isOpen) {
// Check if click is inside the color picker popup
const pickerElement = document.querySelector('.bubble-color-picker')
if (pickerElement && !pickerElement.contains(e.target as Node)) {
onClose()
}
}
}
$effect(() => {
if (isOpen) {
// Add a small delay to prevent immediate closing
setTimeout(() => {
document.addEventListener('click', handleClickOutside)
}, 10)
return () => document.removeEventListener('click', handleClickOutside)
}
})
</script>
{#if isOpen}
<div class="bubble-color-picker">
<div class="bubble-color-picker" use:clickOutside on:clickoutside={onClose}>
<div class="color-picker-header">
<span>{mode === 'text' ? 'Text Color' : 'Highlight Color'}</span>
<button class="remove-color-btn" onclick={removeColor}> Remove </button>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { Editor } from '@tiptap/core'
import { clickOutside } from '$lib/actions/clickOutside'
interface Props {
editor: Editor
@ -67,29 +68,10 @@
action()
onClose()
}
function handleClickOutside(e: MouseEvent) {
if (isOpen) {
const menu = e.currentTarget as HTMLElement
if (!menu?.contains(e.target as Node)) {
onClose()
}
}
}
$effect(() => {
if (isOpen) {
// Small delay to prevent immediate closing
setTimeout(() => {
document.addEventListener('click', handleClickOutside)
}, 10)
return () => document.removeEventListener('click', handleClickOutside)
}
})
</script>
{#if isOpen}
<div class="bubble-text-style-menu">
<div class="bubble-text-style-menu" use:clickOutside on:clickoutside={onClose}>
{#each textStyles as style}
<button
class="text-style-option"

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { onMount } from 'svelte'
import { goto, invalidate } from '$app/navigation'
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
@ -68,18 +67,6 @@ const statusFilterOptions = [
{ value: 'status-draft', label: 'Draft first' }
]
onMount(() => {
document.addEventListener('click', handleOutsideClick)
return () => document.removeEventListener('click', handleOutsideClick)
})
function handleOutsideClick(event: MouseEvent) {
const target = event.target as HTMLElement
if (!target.closest('.dropdown-container')) {
document.dispatchEvent(new CustomEvent('closeDropdowns'))
}
}
function handleNewEssay() {
goto('/admin/posts/new?type=essay')
}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { onMount } from 'svelte'
import { goto } from '$app/navigation'
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
@ -70,18 +69,6 @@
{ value: 'status-draft', label: 'Draft first' }
]
onMount(() => {
document.addEventListener('click', handleOutsideClick)
return () => document.removeEventListener('click', handleOutsideClick)
})
function handleOutsideClick(event: MouseEvent) {
const target = event.target as HTMLElement
if (!target.closest('.dropdown-container')) {
document.dispatchEvent(new CustomEvent('closeDropdowns'))
}
}
function handleEdit(event: CustomEvent<{ project: AdminProject }>) {
goto(`/admin/projects/${event.detail.project.id}/edit`)
}