forked from unovue/reka-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Dropdown menu 2/3 (unovue#291)
* 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
Showing
48 changed files
with
611 additions
and
876 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
86
packages/radix-vue/src/DropdownMenu/DropdownMenuCheckboxItem.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
120
packages/radix-vue/src/DropdownMenu/DropdownMenuContent.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.