feat: added vueuse for onClickOutside, upgraded functionality for hamburger menu and locale picker

This commit is contained in:
Benjamin Singleton 2025-06-14 12:38:57 -05:00
parent 3b74fc00f1
commit 6ea53dc652
4 changed files with 80 additions and 32 deletions

View file

@ -11,6 +11,8 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.6", "@tailwindcss/vite": "^4.1.6",
"@types/node": "^22.15.17", "@types/node": "^22.15.17",
"@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0",
"bulma": "^1.0.4", "bulma": "^1.0.4",
"tailwindcss": "^4.1.6", "tailwindcss": "^4.1.6",
"vue": "^3.5.13", "vue": "^3.5.13",

View file

@ -3,16 +3,21 @@ import "./assets/style.scss";
import { ref, type Ref } from "vue"; import { ref, type Ref } from "vue";
import { SAMPLE } from "@repo/common/sample"; import { SAMPLE } from "@repo/common/sample";
import LocalePicker from "./components/organisms/LocalePicker.vue"; import LocalePicker from "./components/organisms/LocalePicker.vue";
import { vOnClickOutside } from "@vueuse/components";
const burgerOpen: Ref<boolean> = ref<boolean>(false); const burgerOpen: Ref<boolean> = ref<boolean>(false);
const toggleBurger = (): void => { const toggleBurger = (): void => {
burgerOpen.value = !burgerOpen.value; burgerOpen.value = !burgerOpen.value;
}; };
const closeBurger = (): void => {
burgerOpen.value = false;
};
</script> </script>
<template> <template>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col" v-on-click-outside="closeBurger">
<!-- Main application wrapper --> <!-- Main application wrapper -->
<nav <nav
class="navbar is-fixed-top" class="navbar is-fixed-top"
@ -37,15 +42,24 @@ const toggleBurger = (): void => {
<div :class="`navbar-menu ${burgerOpen ? 'is-active' : ''}`"> <div :class="`navbar-menu ${burgerOpen ? 'is-active' : ''}`">
<div class="navbar-start"> <div class="navbar-start">
<RouterLink class="navbar-item" to="/" <RouterLink
class="navbar-item"
to="/"
@click="closeBurger()"
>What is Viossa?</RouterLink >What is Viossa?</RouterLink
> >
<RouterLink class="navbar-item" to="/resources" <RouterLink
class="navbar-item"
to="/resources"
@click="closeBurger()"
>Resources</RouterLink >Resources</RouterLink
> >
<RouterLink class="navbar-item" to="/resources">{{ <RouterLink
SAMPLE class="navbar-item"
}}</RouterLink> to="/resources"
@click="closeBurger()"
>{{ SAMPLE }}</RouterLink
>
<LocalePicker /> <LocalePicker />
</div> </div>
</div> </div>

View file

@ -1,44 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { LOCALE_IDS, localeId, useLocale, type LocaleId } from "@/i18n"; import { LOCALE_IDS, localeId, useLocale, type LocaleId } from "@/i18n";
import { onMounted, onUnmounted, ref, useTemplateRef } from "vue"; import { ref } from "vue";
import { vOnClickOutside } from "@vueuse/components";
const isOpen = ref<boolean>(false); const isOpen = ref<boolean>(false);
const dropdownRef = useTemplateRef("dropdown");
const toggleOpen = (): void => { const toggleOpen = (): void => {
isOpen.value = !isOpen.value; isOpen.value = !isOpen.value;
}; };
const close = (): void => {
isOpen.value = false;
};
const setLocaleId = (id: LocaleId): void => { const setLocaleId = (id: LocaleId): void => {
localeId.value = id; localeId.value = id;
close();
}; };
const detectFocus = (e: MouseEvent) => {
if (!isOpen) {
return;
}
const dropdown = dropdownRef.value;
if (!dropdown) {
return;
}
const { target } = e;
const focused =
target instanceof Node
&& (dropdown === target || dropdown.contains(target));
if (!focused) {
isOpen.value = false;
}
};
onMounted(() => window.addEventListener("click", detectFocus));
onUnmounted(() => window.removeEventListener("click", detectFocus));
</script> </script>
<template> <template>
<div :class="['dropdown', isOpen && 'is-active']" ref="dropdown"> <div
:class="['dropdown', isOpen && 'is-active']"
v-on-click-outside="close">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button <button
class="button" class="button"

48
pnpm-lock.yaml generated
View file

@ -36,6 +36,12 @@ importers:
'@types/node': '@types/node':
specifier: ^22.15.17 specifier: ^22.15.17
version: 22.15.31 version: 22.15.31
'@vueuse/components':
specifier: ^13.3.0
version: 13.3.0(vue@3.5.16(typescript@5.8.3))
'@vueuse/core':
specifier: ^13.3.0
version: 13.3.0(vue@3.5.16(typescript@5.8.3))
bulma: bulma:
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.0.4 version: 1.0.4
@ -693,6 +699,9 @@ packages:
'@types/serve-static@1.15.8': '@types/serve-static@1.15.8':
resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@typescript-eslint/eslint-plugin@8.34.0': '@typescript-eslint/eslint-plugin@8.34.0':
resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==} resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -831,6 +840,24 @@ packages:
vue: vue:
optional: true optional: true
'@vueuse/components@13.3.0':
resolution: {integrity: sha512-ZnJiVknPtlWyeE4qwIXkDOlHM3W4bgMCxgeXj1Dec/aF/+8N+yAj+7rRdRUWUnqr8uKRin368RjG1FPKsF2erA==}
peerDependencies:
vue: ^3.5.0
'@vueuse/core@13.3.0':
resolution: {integrity: sha512-uYRz5oEfebHCoRhK4moXFM3NSCd5vu2XMLOq/Riz5FdqZMy2RvBtazdtL3gEcmDyqkztDe9ZP/zymObMIbiYSg==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@13.3.0':
resolution: {integrity: sha512-42IzJIOYCKIb0Yjv1JfaKpx8JlCiTmtCWrPxt7Ja6Wzoq0h79+YVXmBV03N966KEmDEESTbp5R/qO3AB5BDnGw==}
'@vueuse/shared@13.3.0':
resolution: {integrity: sha512-L1QKsF0Eg9tiZSFXTgodYnu0Rsa2P0En2LuLrIs/jgrkyiDuJSsPZK+tx+wU0mMsYHUYEjNsuE41uqqkuR8VhA==}
peerDependencies:
vue: ^3.5.0
accepts@2.0.0: accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -2423,6 +2450,8 @@ snapshots:
'@types/node': 22.15.31 '@types/node': 22.15.31
'@types/send': 0.17.5 '@types/send': 0.17.5
'@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.1
@ -2622,6 +2651,25 @@ snapshots:
typescript: 5.8.3 typescript: 5.8.3
vue: 3.5.16(typescript@5.8.3) vue: 3.5.16(typescript@5.8.3)
'@vueuse/components@13.3.0(vue@3.5.16(typescript@5.8.3))':
dependencies:
'@vueuse/core': 13.3.0(vue@3.5.16(typescript@5.8.3))
'@vueuse/shared': 13.3.0(vue@3.5.16(typescript@5.8.3))
vue: 3.5.16(typescript@5.8.3)
'@vueuse/core@13.3.0(vue@3.5.16(typescript@5.8.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 13.3.0
'@vueuse/shared': 13.3.0(vue@3.5.16(typescript@5.8.3))
vue: 3.5.16(typescript@5.8.3)
'@vueuse/metadata@13.3.0': {}
'@vueuse/shared@13.3.0(vue@3.5.16(typescript@5.8.3))':
dependencies:
vue: 3.5.16(typescript@5.8.3)
accepts@2.0.0: accepts@2.0.0:
dependencies: dependencies:
mime-types: 3.0.1 mime-types: 3.0.1