Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Dropdown Menu (1/3) #55

Merged
merged 14 commits into from
Jun 25, 2023
Merged
4 changes: 4 additions & 0 deletions packages/playground-vue/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import AvatarDemo from "./components/Demo/AvatarDemo.vue";
import TooltipDemo from "./components/Demo/TooltipDemo.vue";
import HoverCardDemo from "./components/Demo/HoverCardDemo.vue";
import PopoverDemo from "./components/Demo/PopoverDemo.vue";
import DropdownMenuDemo from "./components/Demo/DropdownMenuDemo.vue";
</script>

<template>
Expand Down Expand Up @@ -82,6 +83,9 @@ import PopoverDemo from "./components/Demo/PopoverDemo.vue";
<Card>
<PopoverDemo />
</Card>
<Card>
<DropdownMenuDemo />
</Card>
</div>
</div>
</template>
Expand Down
124 changes: 124 additions & 0 deletions packages/playground-vue/src/components/Demo/DropdownMenuDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script setup lang="ts">
import {
DropdownMenuArrow,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRoot,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from "radix-vue";
import { Icon } from "@iconify/vue";
import { ref } from "vue";

const toggleState = ref(false);
const checkboxOne = ref(false)
const checkboxTwo = ref(false)
const person = ref("pedro")

function handleClick(){
alert('hello!')
}
</script>

<template>
<div class="absolute left-4 top-3 text-sm">
<p>Dropdown Open: {{ toggleState ? "open" : "close" }}</p>
<p>Checkbox 1: {{ checkboxOne ? "checked" : "unchecked" }}</p>
<p>Checkbox 2: {{ checkboxTwo ? "checked" : "unchecked" }}</p>
<p>Person: {{ person }}</p>
</div>
<DropdownMenuRoot v-model="toggleState">
<DropdownMenuTrigger class="rounded-full w-[35px] h-[35px] inline-flex items-center justify-center text-violet11 bg-white shadow-[0_2px_10px] shadow-blackA7 outline-none hover:bg-violet3 focus:shadow-[0_0_0_2px] focus:shadow-black"
aria-label="Customise options">
<Icon icon="radix-icons:hamburger-menu" />
</DropdownMenuTrigger>

<DropdownMenuPortal>
<DropdownMenuContent
class="min-w-[220px] bg-white rounded-md p-[5px] shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade"
sideOffset={5}>
<DropdownMenuItem value="New Tab" @click="handleClick"
class="group text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1">
New Tab
<div
class="ml-auto pl-[20px] text-mauve11 group-data-[highlighted]:text-white group-data-[disabled]:text-mauve8">
⌘+T
</div>
</DropdownMenuItem>
<DropdownMenuItem value="New Window"
class="group text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1">
New Window
<div
class="ml-auto pl-[20px] text-mauve11 group-data-[highlighted]:text-white group-data-[disabled]:text-mauve8">
⌘+N
</div>
</DropdownMenuItem>
<DropdownMenuItem value="New Private Window"
class="group text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1"
disabled>
New Private Window
<div
class="ml-auto pl-[20px] text-mauve11 group-data-[highlighted]:text-white group-data-[disabled]:text-mauve8">
⇧+⌘+N
</div>
</DropdownMenuItem>
<DropdownMenuSeparator class="h-[1px] bg-violet6 m-[5px]" />
<DropdownMenuCheckboxItem v-model="checkboxOne"
class="group text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1"
checked={bookmarksChecked}
onCheckedChange={setBookmarksChecked}
>
<DropdownMenuItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
<Icon icon="radix-icons:check" />
</DropdownMenuItemIndicator>
Show Bookmarks
<div class="ml-auto pl-[20px] text-mauve11 group-data-[highlighted]:text-white group-data-[disabled]:text-mauve8">
⌘+B
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem v-model="checkboxTwo"
class="text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1"
checked={urlsChecked}
onCheckedChange={setUrlsChecked}
>
<DropdownMenuItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
<Icon icon="radix-icons:check" />
</DropdownMenuItemIndicator>
Show Full URLs
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator class="h-[1px] bg-violet6 m-[5px]" />

<DropdownMenuLabel class="pl-[25px] text-xs leading-[25px] text-mauve11">
People
</DropdownMenuLabel>
<DropdownMenuRadioGroup v-model="person">
<DropdownMenuRadioItem
class="text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1"
value="pedro"
>
<DropdownMenuItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
<Icon icon="radix-icons:dot-filled" />
</DropdownMenuItemIndicator>
Pedro Duarte
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
class="text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1"
value="colm"
>
<DropdownMenuItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
<Icon icon="radix-icons:dot-filled" />
</DropdownMenuItemIndicator>
Colm Tuite
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuArrow class="fill-white" />
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</template>
6 changes: 3 additions & 3 deletions packages/radix-vue/src/Checkbox/CheckboxRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,17 @@ let dataState: "checked" | "unchecked" | "indeterminate";
<div
:value="props.value"
role="checkbox"
:aria-checked="modelValue"
:aria-checked="props.modelValue"
:data-state="dataState"
style="position: relative"
:data-disabled="props.disabled ? '' : undefined"
>
<input
type="checkbox"
:id="props.id"
v-bind="modelValue"
v-bind="props.modelValue"
@change="updateModelValue"
:checked="modelValue"
:checked="props.modelValue"
:name="props.name"
aria-hidden="true"
:disabled="props.disabled"
Expand Down
35 changes: 35 additions & 0 deletions packages/radix-vue/src/DropdownMenu/DropdownMenuArrow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { inject, ref, onMounted } from "vue";
import {
DROPDOWN_MENU_INJECTION_KEY,
type DropdownMenuProvideValue,
} from "./DropdownMenuRoot.vue";

const injectedValue = inject<DropdownMenuProvideValue>(
DROPDOWN_MENU_INJECTION_KEY
);

const props = defineProps({
class: String,
});

const arrowElement = ref<HTMLElement>();
onMounted(() => {
injectedValue!.arrowElement.value = arrowElement.value;
});
</script>

<template>
<span ref="arrowElement" style="position: absolute">
<svg
:class="props.class"
width="10"
height="5"
viewBox="0 0 30 10"
preserveAspectRatio="none"
style="display: block"
>
<polygon points="0,0 30,0 15,10"></polygon>
</svg>
</span>
</template>
130 changes: 130 additions & 0 deletions packages/radix-vue/src/DropdownMenu/DropdownMenuCheckboxItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { ref, inject, watchEffect, provide, toRef } from "vue";
import {
DROPDOWN_MENU_INJECTION_KEY,
type DropdownMenuProvideValue,
} from "./DropdownMenuRoot.vue";

interface DropdownMenuCheckboxItemProps {
modelValue?: boolean;
id?: string;
name?: string;
value?: string;
disabled?: boolean;
}

export type DropdownMenuCheckboxProvideValue = Readonly<Ref<boolean>>;
provide<DropdownMenuCheckboxProvideValue>(
"modelValue",
toRef(() => props.modelValue)
);

const injectedValue = inject<DropdownMenuProvideValue>(
DROPDOWN_MENU_INJECTION_KEY
);

const props = defineProps<DropdownMenuCheckboxItemProps>();

const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
}>();

const currentElement = ref<HTMLElement | undefined>();

function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
handleCloseMenu();
}
const allToggleItem = injectedValue!.itemsArray;
if (allToggleItem.length) {
const currentTabIndex = allToggleItem.indexOf(currentElement.value!);
if (e.key === "ArrowDown") {
e.preventDefault();
if (!injectedValue?.selectedElement.value) {
injectedValue?.changeSelected(allToggleItem[0]);
} else if (allToggleItem[currentTabIndex + 1]) {
injectedValue?.changeSelected(allToggleItem[currentTabIndex + 1]);
} else {
injectedValue?.changeSelected(allToggleItem[0]);
}
}

if (e.key === "ArrowUp") {
e.preventDefault();
if (!injectedValue?.selectedElement.value) {
injectedValue?.changeSelected(allToggleItem[allToggleItem.length - 1]);
} else if (allToggleItem[currentTabIndex - 1]) {
injectedValue?.changeSelected(allToggleItem[currentTabIndex - 1]);
} else {
injectedValue?.changeSelected(allToggleItem[allToggleItem.length - 1]);
}
}

if (e.keyCode === 32 || e.key === "Enter") {
if (injectedValue?.selectedElement.value) {
updateModelValue();
}
}
}
}

watchEffect(() => {
if (injectedValue?.selectedElement.value === currentElement.value) {
currentElement.value?.focus();
}
});

function handleHover() {
if (!props.disabled) {
injectedValue!.changeSelected(currentElement.value!);
}
}

function handleCloseMenu() {
injectedValue?.hideTooltip();
document.querySelector("body")!.style.pointerEvents = "";
setTimeout(() => {
injectedValue?.triggerElement.value?.focus();
}, 0);
}

function updateModelValue() {
return emit("update:modelValue", !props.modelValue);
}
</script>

<template>
<div
role="menuitem"
ref="currentElement"
@keydown="handleKeydown"
data-radix-vue-collection-item
@click.prevent="updateModelValue"
@mouseenter="handleHover"
@mouseleave="injectedValue!.changeSelected(null)"
:data-highlighted="
injectedValue?.selectedElement.value === currentElement ? '' : null
"
:aria-disabled="props.disabled ? true : undefined"
:data-disabled="props.disabled ? '' : undefined"
:data-orientation="injectedValue?.orientation"
:tabindex="
injectedValue?.selectedElement.value === currentElement ? '0' : '-1'
"
>
<input
type="checkbox"
:id="props.id"
:aria-valuenow="props.modelValue"
v-bind="props.modelValue"
@change="updateModelValue"
:checked="props.modelValue"
:name="props.name"
aria-hidden="true"
:disabled="props.disabled"
style="opacity: 0; position: absolute; inset: 0"
/>
<slot />
</div>
</template>
Loading