Skip to content

Commit

Permalink
feat: Dropdown menu 2/3 (unovue#291)
Browse files Browse the repository at this point in the history
* export props, emits from menu

* refactor dropdown

* add new composables for forwarding emits

* fix focusScope not allowing multiple stack

* fix subtrigger element missing

* populate default popper content props

* improve useBodyScroll. to populate initial value, and keep track of how many stack

* expose popper child element

* cleanup, add more emitsAsProps

* fix build pipeline

* add comment to function, fix focus

* fix props inherrited wrongly

* revert changes

* fix ssr issue

* fix typeahead that has empty space at start

* fix modal props not working correctly

* fix handleCloseAutoFocus not focusing on trigger element when click outside

* revert playground
  • Loading branch information
zernonia authored Aug 10, 2023
1 parent 831a548 commit aee7639
Show file tree
Hide file tree
Showing 48 changed files with 611 additions and 876 deletions.
2 changes: 2 additions & 0 deletions .histoire/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vueuse/components": "^10.2.1",
"@vueuse/core": "^10.3.0",
"@vueuse/shared": "^10.3.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.43.0",
"histoire": "^0.16.2",
Expand Down
18 changes: 12 additions & 6 deletions packages/radix-vue/src/DropdownMenu/DropdownMenu.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,25 @@ import {
} from "./";
const toggleState = ref(false);
const toggleState2 = ref(false);
const checkboxOne = ref(false);
const checkboxTwo = ref(false);
const person = ref("pedro");
function handleClick() {
alert("hello!");
// alert("hello!");
}
const handleCheck = (ev: any) => {
// checkboxOne.value = ev;
console.log(ev);
};
</script>

<template>
<Story title="DropdownMenu" :layout="{ type: 'single', iframe: true }">
<Story title="DropdownMenu" :layout="{ type: 'single', iframe: false }">
<Variant title="default">
<DropdownMenuRoot v-model="toggleState">
<DropdownMenuRoot v-model:open="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"
Expand All @@ -43,7 +49,6 @@ function handleClick() {
<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"
:side-offset="5"
>
<DropdownMenuItem
value="New Tab"
Expand Down Expand Up @@ -279,7 +284,8 @@ function handleClick() {
</DropdownMenuSub>
<DropdownMenuSeparator class="h-[1px] bg-violet6 m-[5px]" />
<DropdownMenuCheckboxItem
v-model="checkboxOne"
v-model:checked="checkboxOne"
@select="handleCheck"
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"
>
<DropdownMenuItemIndicator
Expand All @@ -295,7 +301,7 @@ function handleClick() {
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
v-model="checkboxTwo"
v-model:checked="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"
>
<DropdownMenuItemIndicator
Expand Down
8 changes: 6 additions & 2 deletions packages/radix-vue/src/DropdownMenu/DropdownMenuArrow.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { PopperArrow } from "@/Popper";
import { MenuArrow, type MenuArrowProps } from "@/Menu";
const props = defineProps<MenuArrowProps>();
</script>

<template>
<PopperArrow></PopperArrow>
<MenuArrow v-bind="props">
<slot></slot>
</MenuArrow>
</template>
86 changes: 11 additions & 75 deletions packages/radix-vue/src/DropdownMenu/DropdownMenuCheckboxItem.vue
Original file line number Diff line number Diff line change
@@ -1,83 +1,19 @@
<script lang="ts">
import { useVModel } from "@vueuse/core";
import { type PrimitiveProps } from "@/Primitive";
interface DropdownMenuCheckboxItemProps extends PrimitiveProps {
checked?: boolean;
//onCheckedChange?: void;
modelValue?: boolean;
id?: string;
name?: string;
value?: string;
disabled?: boolean;
onSelect?: void;
textValue?: string;
}
</script>

<script setup lang="ts">
import { inject, computed, provide } from "vue";
import BaseMenuItem from "../shared/component/BaseMenuItem.vue";
import { DROPDOWN_MENU_ITEM_SYMBOL } from "./utils";
import {
DROPDOWN_MENU_INJECTION_KEY,
type DropdownMenuProvideValue,
} from "./DropdownMenuRoot.vue";
const injectedValue = inject<DropdownMenuProvideValue>(
DROPDOWN_MENU_INJECTION_KEY
);
const props = defineProps<DropdownMenuCheckboxItemProps>();
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
}>();
const modelValue = useVModel(props, "modelValue", emit, {
passive: true,
});
const checkboxDataState = computed(() => {
return modelValue.value ? "checked" : "unchecked";
});
MenuCheckboxItem,
type MenuCheckboxItemProps,
type MenuCheckboxItemEmits,
} from "@/Menu";
import { useEmitAsProps } from "@/shared";
function handleClick() {
modelValue.value = !modelValue.value;
}
function handleEscape() {
injectedValue?.hideTooltip();
}
const props = defineProps<MenuCheckboxItemProps>();
const emits = defineEmits<MenuCheckboxItemEmits>();
provide(DROPDOWN_MENU_ITEM_SYMBOL, {
modelValue,
});
const emitsAsProps = useEmitAsProps(emits);
</script>

<template>
<BaseMenuItem
ref="currentElement"
:disabled="props.disabled"
:rootProvider="injectedValue"
:orientation="injectedValue?.orientation"
@handle-click="handleClick"
@escape-keydown="handleEscape"
role="menuitemcheckbox"
:data-state="checkboxDataState"
:as-child="props.asChild"
:as="as"
:aria-checked="props.modelValue ? true : false"
>
<input
type="checkbox"
:id="props.id"
v-bind="props.modelValue"
:checked="props.modelValue"
:name="props.name"
aria-hidden="true"
:disabled="props.disabled"
style="opacity: 0; position: absolute; inset: 0"
/>
<slot />
</BaseMenuItem>
<MenuCheckboxItem v-bind="{ ...props, ...emitsAsProps }">
<slot></slot>
</MenuCheckboxItem>
</template>
120 changes: 53 additions & 67 deletions packages/radix-vue/src/DropdownMenu/DropdownMenuContent.vue
Original file line number Diff line number Diff line change
@@ -1,80 +1,66 @@
<script lang="ts">
import { onClickOutside } from "@vueuse/core";
import { useCollection } from "@/shared";
export type Boundary = Element | null | Array<Element | null>;
export interface DropdownMenuContentProps extends PopperContentProps {
loop?: boolean; //false
//onOpenAutoFocus?: void;
//onCloseAutoFocus?: void;
//onEscapeKeyDown?: void;
//onPointerDownOutside?: void;
//onInteractOutside?: void;
}
</script>

<script setup lang="ts">
import { inject, watchEffect } from "vue";
import { Primitive, usePrimitiveElement } from "@/Primitive";
import { inject, ref } from "vue";
import {
MenuContent,
type MenuContentEmits,
type MenuContentProps,
} from "@/Menu";
import { DROPDOWN_MENU_INJECTION_KEY } from "./DropdownMenuRoot.vue";
import { PopperContent, type PopperContentProps } from "@/Popper";
import { useEmitAsProps } from "@/shared";
const props = withDefaults(defineProps<DropdownMenuContentProps>(), {
side: "bottom",
align: "center",
avoidCollisions: true,
});
export interface DropdownMenuContentProps extends MenuContentProps {}
export type DropdownMenuContentEmits = MenuContentEmits;
const injectedValue = inject(DROPDOWN_MENU_INJECTION_KEY);
const props = defineProps<DropdownMenuContentProps>();
const emits = defineEmits<DropdownMenuContentEmits>();
const { primitiveElement, currentElement: tooltipContentElement } =
usePrimitiveElement();
const context = inject(DROPDOWN_MENU_INJECTION_KEY);
const { createCollection, getItems } = useCollection();
createCollection(tooltipContentElement);
const hasInteractedOutsideRef = ref(false);
watchEffect(() => {
if (tooltipContentElement.value) {
if (injectedValue?.modelValue.value) {
document.querySelector("body")!.style.pointerEvents = "none";
injectedValue.itemsArray = getItems(tooltipContentElement.value);
} else {
if (injectedValue?.triggerElement.value) {
handleCloseMenu();
}
}
}
});
const emitsAsProps = useEmitAsProps(emits);
function handleCloseMenu() {
document.querySelector("body")!.style.pointerEvents = "";
setTimeout(() => {
injectedValue?.triggerElement.value?.focus();
}, 0);
}
const handleCloseAutoFocus = (event: Event) => {
emits("closeAutoFocus", event);
if (event.defaultPrevented) return;
if (!hasInteractedOutsideRef.value) {
setTimeout(() => {
context?.triggerElement.value?.focus();
}, 0);
}
hasInteractedOutsideRef.value = false;
onClickOutside(tooltipContentElement, (event) => {
const target = event.target as HTMLElement;
if (target.closest('[role="menuitem"]')) return;
injectedValue?.hideTooltip();
});
// Always prevent auto focus because we either focus manually or want user agent focus
event.preventDefault();
};
</script>

<template>
<PopperContent v-bind="props" v-if="injectedValue?.modelValue.value">
<Primitive
ref="primitiveElement"
:data-state="injectedValue?.modelValue.value ? 'open' : 'closed'"
:data-side="props.side"
:data-align="props.align"
role="tooltip"
:as-child="props.asChild"
:as="as"
style="pointer-events: auto"
>
<slot />
</Primitive>
</PopperContent>
<MenuContent
v-bind="{ ...props, ...emitsAsProps }"
:id="context?.contentId"
:aria-labelledby="context?.triggerId"
@close-auto-focus="handleCloseAutoFocus"
@interact-outside="(event) => {
emits('interactOutside', event)
if(event.defaultPrevented) return
const originalEvent = event.detail.originalEvent as PointerEvent;
const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true;
const isRightClick = originalEvent.button === 2 || ctrlLeftClick;
if (!context?.modal.value || isRightClick) hasInteractedOutsideRef = true;
}"
:style="{
'--radix-dropdown-menu-content-transform-origin':
'var(--radix-popper-transform-origin)',
'--radix-dropdown-menu-content-available-width':
'var(--radix-popper-available-width)',
'--radix-dropdown-menu-content-available-height':
'var(--radix-popper-available-height)',
'--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)',
'--radix-dropdown-menu-trigger-height':
'var(--radix-popper-anchor-height)',
}"
>
<slot></slot>
</MenuContent>
</template>
78 changes: 6 additions & 72 deletions packages/radix-vue/src/DropdownMenu/DropdownMenuGroup.vue
Original file line number Diff line number Diff line change
@@ -1,79 +1,13 @@
<script lang="ts">
import type { Ref, InjectionKey } from "vue";
import type { DataOrientation, Direction } from "../shared/types";
type TypeEnum = "single" | "multiple";
export interface DropdownMenuGroupProps extends PrimitiveProps {
type?: TypeEnum;
value?: string;
defaultValue?: string;
disabled?: boolean;
rovingFocus?: boolean;
orientation?: DataOrientation;
dir?: Direction;
loop?: boolean;
modelValue?: string | string[];
}
export const DROPDOWN_MENU_GROUP_INJECTION_KEY =
Symbol() as InjectionKey<DropdownMenuGroupProvideValue>;
export interface DropdownMenuGroupProvideValue {
type: TypeEnum;
modelValue?: Readonly<Ref<string | string[] | undefined>>;
changeModelValue: (value: string) => void;
parentElement: Ref<HTMLElement | undefined>;
}
</script>

<script setup lang="ts">
import { toRef, provide } from "vue";
import {
Primitive,
usePrimitiveElement,
type PrimitiveProps,
} from "@/Primitive";
const props = withDefaults(defineProps<DropdownMenuGroupProps>(), {
type: "single",
});
const emits = defineEmits(["update:modelValue"]);
import { MenuGroup, type MenuGroupProps } from "@/Menu";
const { primitiveElement, currentElement: parentElement } =
usePrimitiveElement();
interface DropdownMenuGroupProps extends MenuGroupProps {}
provide<DropdownMenuGroupProvideValue>(DROPDOWN_MENU_GROUP_INJECTION_KEY, {
type: props.type,
modelValue: toRef(() => props.modelValue),
changeModelValue: (value: string) => {
if (props.type === "single") {
emits("update:modelValue", value);
} else {
let modelValueArray = props.modelValue as string[];
if (modelValueArray.includes(value)) {
let index = modelValueArray.findIndex((i) => i === value);
modelValueArray.splice(index, 1);
} else {
modelValueArray.push(value);
}
emits("update:modelValue", modelValueArray);
}
},
parentElement,
});
const props = defineProps<DropdownMenuGroupProps>();
</script>

<template>
<Primitive
ref="primitiveElement"
role="group"
:dir="props.dir"
:as-child="props.asChild"
:as="as"
aria-label="Text alignment"
>
<slot />
</Primitive>
<MenuGroup v-bind="props">
<slot></slot>
</MenuGroup>
</template>
Loading

0 comments on commit aee7639

Please sign in to comment.