Skip to content

Commit

Permalink
[Context Menu]feat: 1/3, update ver rc.8
Browse files Browse the repository at this point in the history
* fix contextmenu, support submenus
* update version rc.8
  • Loading branch information
k11q authored Jul 13, 2023
1 parent 8e88740 commit ebb995f
Show file tree
Hide file tree
Showing 21 changed files with 746 additions and 370 deletions.
2 changes: 1 addition & 1 deletion packages/radix-vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ pnpm install radix-vue

## Documentation

For full documentation, visit [radix-vue.com](https://radix-vue.com).
For full documentation, visit [radix-vue.com](https://radix-vue.com).
2 changes: 1 addition & 1 deletion packages/radix-vue/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "radix-vue",
"version": "0.0.1-rc-.7",
"version": "0.0.1-rc-.8",
"description": "Vue port for Radix UI Primitives.",
"type": "module",
"main": "./dist/index.umd.cjs",
Expand Down
123 changes: 38 additions & 85 deletions packages/radix-vue/src/ContextMenu/ContextMenuCheckboxItem.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { inject, watchEffect, provide, toRef } from "vue";
import {
CONTEXT_MENU_INJECTION_KEY,
type ContextMenuProvideValue,
} from "./ContextMenuRoot.vue";
import { PrimitiveDiv, usePrimitiveElement } from "@/Primitive";
<script lang="ts">
import { useVModel } from "@vueuse/core";
interface ContextMenuCheckboxItemProps {
asChild?: boolean;
checked?: boolean;
//onCheckedChange?: void;
modelValue?: boolean;
id?: string;
name?: string;
value?: string;
disabled?: boolean;
onSelect?: void;
textValue?: string;
}
</script>

export type ContextMenuCheckboxProvideValue = Readonly<Ref<boolean>>;
provide<ContextMenuCheckboxProvideValue>(
"modelValue",
toRef(() => props.modelValue)
);
<script setup lang="ts">
import { inject, computed, provide } from "vue";
import BaseMenuItem from "../shared/component/BaseMenuItem.vue";
import { CONTEXT_MENU_ITEM_SYMBOL } from "./utils";
import {
CONTEXT_MENU_INJECTION_KEY,
type ContextMenuProvideValue,
} from "./ContextMenuRoot.vue";
const injectedValue = inject<ContextMenuProvideValue>(
CONTEXT_MENU_INJECTION_KEY
Expand All @@ -31,98 +34,48 @@ const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
}>();
const { primitiveElement, currentElement } = usePrimitiveElement();
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();
}
}
}
}
const modelValue = useVModel(props, "modelValue", emit, {
passive: true,
});
watchEffect(() => {
if (injectedValue?.selectedElement.value === currentElement.value) {
currentElement.value?.focus();
}
const checkboxDataState = computed(() => {
return modelValue.value ? "checked" : "unchecked";
});
function handleHover() {
if (!props.disabled) {
injectedValue!.changeSelected(currentElement.value!);
}
function handleClick() {
modelValue.value = !modelValue.value;
}
function handleCloseMenu() {
function handleEscape() {
injectedValue?.hideTooltip();
document.querySelector("body")!.style.pointerEvents = "";
}
function updateModelValue() {
return emit("update:modelValue", !props.modelValue);
}
provide(CONTEXT_MENU_ITEM_SYMBOL, {
modelValue,
});
</script>

<template>
<PrimitiveDiv
role="menuitem"
ref="primitiveElement"
@keydown="handleKeydown"
data-radix-vue-collection-item
@click.prevent="updateModelValue"
@mouseenter="handleHover"
@mouseleave="injectedValue!.changeSelected(undefined)"
: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'
"
<BaseMenuItem
ref="currentElement"
:disabled="props.disabled"
:rootProvider="injectedValue"
:orientation="injectedValue?.orientation"
@handle-click="handleClick"
@escape-keydown="handleEscape"
role="menuitemcheckbox"
:data-state="checkboxDataState"
:aria-checked="props.modelValue ? true : false"
>
<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 />
</PrimitiveDiv>
</BaseMenuItem>
</template>
122 changes: 63 additions & 59 deletions packages/radix-vue/src/ContextMenu/ContextMenuContent.vue
Original file line number Diff line number Diff line change
@@ -1,102 +1,106 @@
<script lang="ts">
import { onClickOutside } from "@vueuse/core";
import { useCollection } from "@/shared";
export type Boundary = Element | null | Array<Element | null>;
export interface ContextMenuContentProps extends PopperContentProps {
asChild?: boolean;
forceMount?: boolean;
loop?: boolean; //false
//onOpenAutoFocus?: void;
//onCloseAutoFocus?: void;
//onEscapeKeyDown?: void;
//onPointerDownOutside?: void;
//onInteractOutside?: void;
}
</script>

<script setup lang="ts">
import { inject, watchEffect } from "vue";
import { useClickOutside } from "../shared/useClickOutside";
import {
CONTEXT_MENU_INJECTION_KEY,
type ContextMenuProvideValue,
} from "./ContextMenuRoot.vue";
import { PopperContent, type PopperContentProps } from "@/Popper";
import { usePrimitiveElement } from "@/Primitive";
import { inject, watchEffect, nextTick } from "vue";
import { PrimitiveDiv, usePrimitiveElement } from "@/Primitive";
const injectedValue = inject<ContextMenuProvideValue>(
CONTEXT_MENU_INJECTION_KEY
);
import { CONTEXT_MENU_INJECTION_KEY } from "./ContextMenuRoot.vue";
import { PopperContent, type PopperContentProps } from "@/Popper";
const props = withDefaults(defineProps<ContextMenuContentProps>(), {
side: "right",
side: "bottom",
align: "start",
alignOffset: 3,
avoidCollisions: true,
});
const injectedValue = inject(CONTEXT_MENU_INJECTION_KEY);
const { primitiveElement, currentElement: tooltipContentElement } =
usePrimitiveElement();
const { createCollection, getItems } = useCollection();
createCollection(tooltipContentElement);
watchEffect(() => {
if (tooltipContentElement.value) {
if (injectedValue?.modelValue.value) {
setTimeout(() => {
focusFirstRadixElement();
fillItemsArray();
}, 0);
window.addEventListener("mousedown", closeDialogWhenClickOutside);
injectedValue.itemsArray = getItems(tooltipContentElement.value);
window.addEventListener("keydown", handleKeydown);
window.addEventListener("scroll", closeDialogWhenScrolled);
} else {
cleanupEvents();
}
}
});
function closeDialogWhenClickOutside(e: MouseEvent) {
if (tooltipContentElement.value) {
const clickOutside = useClickOutside(e, tooltipContentElement.value);
if (clickOutside) {
injectedValue?.hideTooltip();
window.removeEventListener("mousedown", closeDialogWhenClickOutside);
}
watchEffect(() => {
if (injectedValue?.selectedElement.value) {
cleanupEvents();
}
});
function cleanupEvents() {
window.removeEventListener("keydown", handleKeydown);
window.removeEventListener("scroll", closeDialogWhenScrolled);
}
onClickOutside(tooltipContentElement, (event) => {
const target = event.target as HTMLElement;
if (target.closest('[role="menuitem"]')) return;
injectedValue?.hideTooltip();
});
function closeDialogWhenScrolled() {
injectedValue?.hideTooltip();
window.removeEventListener("scroll", closeDialogWhenScrolled);
}
function focusFirstRadixElement() {
if (!tooltipContentElement.value) {
return console.log("tooltipContentElement not found!");
}
const allToggleItem = Array.from(
tooltipContentElement.value.querySelectorAll(
"[data-radix-vue-collection-item]"
)
) as HTMLElement[];
if (allToggleItem.length) {
injectedValue!.selectedElement.value = allToggleItem[0];
allToggleItem[0].focus();
async function handleKeydown(e: KeyboardEvent) {
e.preventDefault();
if (e.key === "ArrowDown" || e.key === "Enter" || e.keyCode === 32) {
injectedValue?.changeSelected(injectedValue.itemsArray?.[0]);
injectedValue?.selectedElement.value?.focus();
} else if (e.key === "ArrowUp") {
const newSelectedElement =
injectedValue?.itemsArray?.[injectedValue?.itemsArray.length - 1];
injectedValue?.changeSelected(newSelectedElement!);
newSelectedElement?.focus();
}
}
function fillItemsArray() {
if (!tooltipContentElement.value) {
return console.log("tooltipContentElement not found!");
}
const allToggleItem = Array.from(
tooltipContentElement.value.querySelectorAll(
"[data-radix-vue-collection-item]:not([data-disabled])"
)
) as HTMLElement[];
injectedValue!.itemsArray = allToggleItem;
window.removeEventListener("keydown", handleKeydown);
}
</script>

<template>
<PopperContent
ref="primitiveElement"
v-if="injectedValue?.modelValue.value"
v-bind="props"
:data-state="injectedValue?.modelValue.value ? 'open' : 'closed'"
:asChild="props.asChild"
data-side="bottom"
role="tooltip"
style="pointer-events: auto"
v-if="injectedValue?.modelValue.value"
prioritize-position
>
<slot />
<PrimitiveDiv
ref="primitiveElement"
:data-state="injectedValue?.modelValue.value ? 'open' : 'closed'"
:data-side="props.side"
:data-align="props.align"
role="tooltip"
:asChild="props.asChild"
style="pointer-events: auto"
>
<slot />
</PrimitiveDiv>
</PopperContent>
</template>
4 changes: 2 additions & 2 deletions packages/radix-vue/src/ContextMenu/ContextMenuGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const props = withDefaults(defineProps<ContextMenuGroupProps>(), {
const emits = defineEmits(["update:modelValue"]);
const { primitiveElement, currentElement: parentElementRef } =
const { primitiveElement, currentElement: parentElement } =
usePrimitiveElement();
provide<ContextMenuGroupProvideValue>(CONTEXT_MENU_GROUP_INJECTION_KEY, {
Expand All @@ -57,7 +57,7 @@ provide<ContextMenuGroupProvideValue>(CONTEXT_MENU_GROUP_INJECTION_KEY, {
emits("update:modelValue", modelValueArray);
}
},
parentElement: parentElementRef,
parentElement,
});
</script>

Expand Down
Loading

0 comments on commit ebb995f

Please sign in to comment.