Skip to content

Commit

Permalink
Feat: Dropdown Menu (1/3) (#55)
Browse files Browse the repository at this point in the history
* init branch dropdownmenu

* built

* save

* add basic item

* add esc to closemodal, add focus trigger after close, add keydown arrowdown to open modal

* handle keydown arrowup arrowdown when nothing selected

* enable disabled api

* enable checkbox click, keyboard enter and space

* dropdownmenu 1/3 w/o nested
  • Loading branch information
khairulhaaziq committed Jun 25, 2023
1 parent 8c7fd34 commit c7e4272
Show file tree
Hide file tree
Showing 18 changed files with 937 additions and 3 deletions.
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>

0 comments on commit c7e4272

Please sign in to comment.