Skip to content

Commit 6838fef

Browse files
author
mdatelle
committed
refactor: swap out dropdown with radix components
1 parent eecfd3c commit 6838fef

File tree

4 files changed

+83
-76
lines changed

4 files changed

+83
-76
lines changed

web/components/UserProfile.ce.vue

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script lang="ts" setup>
22
import { useI18n } from 'vue-i18n';
33
import { storeToRefs } from 'pinia';
4-
import { OnClickOutside } from '@vueuse/components';
54
import { useClipboard } from '@vueuse/core';
65
76
import { devConfig } from '~/helpers/env';
@@ -12,6 +11,7 @@ import { useCallbackActionsStore, useCallbackStore } from '~/store/callbackActio
1211
import { useDropdownStore } from '~/store/dropdown';
1312
import { useServerStore } from '~/store/server';
1413
import { useThemeStore } from '~/store/theme';
14+
import Dropdown from './UserProfile/Dropdown.vue';
1515
1616
export interface Props {
1717
server?: Server | string;
@@ -29,18 +29,6 @@ const { dropdownVisible } = storeToRefs(dropdownStore);
2929
const { name, description, guid, keyfile, lanIp, connectPluginInstalled } = storeToRefs(serverStore);
3030
const { bannerGradient, theme } = storeToRefs(useThemeStore());
3131
32-
/**
33-
* Close dropdown when clicking outside
34-
* @note If in testing you have two variants of the component on a page the clickOutside will fire twice making it seem like it doesn't work
35-
*/
36-
const clickOutsideTarget = ref();
37-
const clickOutsideIgnoreTarget = ref();
38-
const outsideDropdown = () => {
39-
if (dropdownVisible.value) {
40-
return dropdownStore.dropdownToggle();
41-
}
42-
};
43-
4432
/**
4533
* Copy LAN IP on server name click
4634
*/
@@ -154,14 +142,11 @@ onMounted(() => {
154142
<NotificationsSidebar />
155143
</template>
156144

157-
<OnClickOutside
158-
class="flex items-center justify-end h-full"
159-
:options="{ ignore: [clickOutsideIgnoreTarget] }"
160-
@trigger="outsideDropdown"
161-
>
162-
<UpcDropdownTrigger ref="clickOutsideIgnoreTarget" :t="t" />
163-
<UpcDropdown ref="clickOutsideTarget" :t="t" />
164-
</OnClickOutside>
145+
<Dropdown :t="t">
146+
<template #trigger>
147+
<UpcDropdownTrigger :t="t" />
148+
</template>
149+
</Dropdown>
165150
</div>
166151
</div>
167152
</template>
@@ -171,23 +156,6 @@ onMounted(() => {
171156
@import '@unraid/ui/styles';
172157
@import '~/assets/main.css';
173158
174-
.DropdownWrapper_blip {
175-
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-popover-foreground);
176-
177-
&::before {
178-
@apply absolute z-20 block;
179-
180-
content: '';
181-
width: 0;
182-
height: 0;
183-
top: -10px;
184-
right: 42px;
185-
border-right: 11px solid transparent;
186-
border-bottom: 11px solid var(--color-headerTextPrimary);
187-
border-left: 11px solid transparent;
188-
}
189-
}
190-
191159
.unraid_mark_2,
192160
.unraid_mark_4 {
193161
animation: mark_2 1.5s ease infinite;
Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,53 @@
1-
<script setup lang="ts">
2-
import { storeToRefs } from 'pinia';
1+
<script lang="ts" setup>
2+
import { ref } from 'vue';
33
4-
import { TransitionRoot } from '@headlessui/vue';
4+
import {
5+
DropdownMenuArrow,
6+
DropdownMenuContent,
7+
DropdownMenuPortal,
8+
DropdownMenuRoot,
9+
DropdownMenuTrigger,
10+
} from 'radix-vue';
511
612
import type { ComposerTranslation } from 'vue-i18n';
713
8-
import { useDropdownStore } from '~/store/dropdown';
14+
import useTeleport from '~/composables/useTeleport';
915
import { useServerStore } from '~/store/server';
1016
1117
defineProps<{ t: ComposerTranslation }>();
1218
13-
const dropdownStore = useDropdownStore();
14-
15-
const { dropdownVisible } = storeToRefs(dropdownStore);
1619
const { state } = storeToRefs(useServerStore());
20+
const open = ref(false);
21+
const { teleportTarget, determineTeleportTarget } = useTeleport();
1722
1823
const showLaunchpad = computed(() => state.value === 'ENOKEYFILE');
24+
25+
const onOpenChange = (newOpen: boolean) => {
26+
if (newOpen) {
27+
determineTeleportTarget();
28+
}
29+
open.value = newOpen;
30+
};
1931
</script>
2032

2133
<template>
22-
<TransitionRoot
23-
:show="dropdownVisible"
24-
enter="transition-all duration-200"
25-
enter-from="opacity-0 translate-y-[16px]"
26-
enter-to="opacity-100"
27-
leave="transition-all duration-150"
28-
leave-from="opacity-100"
29-
leave-to="opacity-0 translate-y-[16px]"
30-
>
31-
<UpcDropdownWrapper
32-
class="DropdownWrapper_blip text-foreground absolute z-30 top-full right-0 transition-all"
33-
>
34-
<UpcDropdownLaunchpad v-if="showLaunchpad" :t="t" />
35-
<UpcDropdownContent v-else :t="t" />
36-
</UpcDropdownWrapper>
37-
</TransitionRoot>
34+
<div class="relative">
35+
<DropdownMenuRoot v-model:open="open" @update:open="onOpenChange">
36+
<DropdownMenuTrigger class="outline-none">
37+
<slot name="trigger" />
38+
</DropdownMenuTrigger>
39+
<DropdownMenuPortal :to="teleportTarget as HTMLElement">
40+
<DropdownMenuContent
41+
class="text-foreground bg-popover rounded-md shadow-lg min-w-[300px] max-w-[350px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
42+
:side-offset="4"
43+
:align="'end'"
44+
:side="'bottom'"
45+
>
46+
<DropdownMenuArrow class="fill-popover text-popover-foreground w-5 h-3" />
47+
<UpcDropdownLaunchpad v-if="showLaunchpad" :t="t" />
48+
<UpcDropdownContent v-else :t="t" />
49+
</DropdownMenuContent>
50+
</DropdownMenuPortal>
51+
</DropdownMenuRoot>
52+
</div>
3853
</template>

web/components/UserProfile/DropdownTrigger.vue

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
<script setup lang="ts">
22
import { storeToRefs } from 'pinia';
3+
34
import {
4-
Bars3Icon,
55
Bars3BottomRightIcon,
6+
Bars3Icon,
67
BellAlertIcon,
78
ExclamationTriangleIcon,
89
InformationCircleIcon,
910
ShieldExclamationIcon,
1011
} from '@heroicons/vue/24/solid';
12+
1113
import type { ComposerTranslation } from 'vue-i18n';
1214
1315
import { useDropdownStore } from '~/store/dropdown';
1416
import { useErrorsStore } from '~/store/errors';
1517
import { useServerStore } from '~/store/server';
1618
import { useUpdateOsStore } from '~/store/updateOs';
1719
18-
const props = defineProps<{ t: ComposerTranslation; }>();
20+
const props = defineProps<{ t: ComposerTranslation }>();
1921
2022
const dropdownStore = useDropdownStore();
2123
const { dropdownVisible } = storeToRefs(dropdownStore);
@@ -26,14 +28,22 @@ const { available: osUpdateAvailable } = storeToRefs(useUpdateOsStore());
2628
const showErrorIcon = computed(() => errors.value.length || stateData.value.error);
2729
2830
const text = computed((): string => {
29-
if ((stateData.value.error) && state.value !== 'EEXPIRED') { return props.t('Fix Error'); }
31+
if (stateData.value.error && state.value !== 'EEXPIRED') {
32+
return props.t('Fix Error');
33+
}
3034
return '';
3135
});
3236
3337
const title = computed((): string => {
34-
if (state.value === 'ENOKEYFILE') { return props.t('Get Started'); }
35-
if (state.value === 'EEXPIRED') { return props.t('Trial Expired, see options below'); }
36-
if (showErrorIcon.value) { return props.t('Learn more about the error'); }
38+
if (state.value === 'ENOKEYFILE') {
39+
return props.t('Get Started');
40+
}
41+
if (state.value === 'EEXPIRED') {
42+
return props.t('Trial Expired, see options below');
43+
}
44+
if (showErrorIcon.value) {
45+
return props.t('Learn more about the error');
46+
}
3747
return dropdownVisible.value ? props.t('Close Dropdown') : props.t('Open Dropdown');
3848
});
3949
</script>
@@ -45,16 +55,30 @@ const title = computed((): string => {
4555
@click="dropdownStore.dropdownToggle()"
4656
>
4757
<template v-if="errors.length && errors[0].level">
48-
<InformationCircleIcon v-if="errors[0].level === 'info'" class="text-unraid-red fill-current relative w-24px h-24px" />
49-
<ExclamationTriangleIcon v-if="errors[0].level === 'warning'" class="text-unraid-red fill-current relative w-24px h-24px" />
50-
<ShieldExclamationIcon v-if="errors[0].level === 'error'" class="text-unraid-red fill-current relative w-24px h-24px" />
58+
<InformationCircleIcon
59+
v-if="errors[0].level === 'info'"
60+
class="text-unraid-red fill-current relative w-24px h-24px"
61+
/>
62+
<ExclamationTriangleIcon
63+
v-if="errors[0].level === 'warning'"
64+
class="text-unraid-red fill-current relative w-24px h-24px"
65+
/>
66+
<ShieldExclamationIcon
67+
v-if="errors[0].level === 'error'"
68+
class="text-unraid-red fill-current relative w-24px h-24px"
69+
/>
5170
</template>
52-
<span v-if="text" class="relative leading-none ">
71+
<span v-if="text" class="relative leading-none">
5372
<span>{{ text }}</span>
54-
<span class="absolute bottom-[-3px] inset-x-0 h-2px w-full bg-gradient-to-r from-unraid-red to-orange rounded opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity" />
73+
<span
74+
class="absolute bottom-[-3px] inset-x-0 h-2px w-full bg-gradient-to-r from-unraid-red to-orange rounded opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity"
75+
/>
5576
</span>
5677

57-
<BellAlertIcon v-if="osUpdateAvailable && !rebootType" class="hover:animate-pulse fill-current relative w-16px h-16px" />
78+
<BellAlertIcon
79+
v-if="osUpdateAvailable && !rebootType"
80+
class="hover:animate-pulse fill-current relative w-16px h-16px"
81+
/>
5882

5983
<Bars3Icon v-if="!dropdownVisible" class="w-20px" />
6084
<Bars3BottomRightIcon v-else class="w-20px" />

web/pages/index.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import AES from 'crypto-js/aes';
66
77
import type { SendPayloads } from '~/store/callback';
88
9+
import LogViewerCe from '~/components/Logs/LogViewer.ce.vue';
910
import SsoButtonCe from '~/components/SsoButton.ce.vue';
1011
import { useThemeStore } from '~/store/theme';
11-
import LogViewerCe from '~/components/Logs/LogViewer.ce.vue';
1212
1313
const serverStore = useDummyServerStore();
1414
const { serverState } = storeToRefs(serverStore);
@@ -96,12 +96,12 @@ const bannerImage = watch(theme, () => {
9696
<ColorSwitcherCe />
9797
<h2 class="text-xl font-semibold font-mono">Vue Components</h2>
9898
<h3 class="text-lg font-semibold font-mono">UserProfileCe</h3>
99-
<header
99+
<header
100100
class="bg-header-background-color flex justify-between items-center"
101101
:style="{
102102
backgroundImage: bannerImage,
103103
backgroundSize: 'cover',
104-
backgroundPosition: 'center'
104+
backgroundPosition: 'center',
105105
}"
106106
>
107107
<div class="inline-flex flex-col gap-4 items-start px-4">

0 commit comments

Comments
 (0)