Skip to content

Commit

Permalink
fix focus, and should remove content when focus outside from the navbar
Browse files Browse the repository at this point in the history
  • Loading branch information
zernonia committed Jul 15, 2023
1 parent 75addac commit 5014df5
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const commonProps = computed(() => ({
triggerRef: itemContext!.triggerRef,
focusProxyRef: itemContext!.focusProxyRef,
wasEscapeCloseRef: itemContext!.wasEscapeCloseRef,
// onContentFocusOutside: itemContext!.onContentFocusOutside,
onContentFocusOutside: itemContext!.onContentFocusOutside,
// onRootContentClose: itemContext!.onRootContentClose,
}));
Expand Down
55 changes: 25 additions & 30 deletions packages/radix-vue/src/NavigationMenu/NavigationMenuContentImpl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface NavigationMenuContentImplProps {
triggerRef: Ref<HTMLElement | undefined>;
focusProxyRef: Ref<HTMLElement | undefined>;
wasEscapeCloseRef: Ref<boolean>;
// onContentFocusOutside(): void;
onContentFocusOutside(): void;
// onRootContentClose(): void;
}
</script>
Expand All @@ -20,7 +20,7 @@ import {
makeContentId,
makeTriggerId,
} from "./utils";
import { useArrowNavigation, useCollection } from "@/shared";
import { useArrowNavigation, useCollection, onFocusOutside } from "@/shared";
const props = defineProps<NavigationMenuContentImplProps>();
const emits = defineEmits<{
Expand All @@ -29,6 +29,7 @@ const emits = defineEmits<{
const { getItems } = useCollection();
const elementRef = ref<HTMLElement>();
const context = inject(NAVIGATION_MENU_INJECTION_KEY);
const triggerId = makeTriggerId(context!.baseId, props.value);
const contentId = makeContentId(context!.baseId, props.value);
Expand Down Expand Up @@ -67,44 +68,37 @@ const motionAttribute = computed(() => {
return attribute;
});
onFocusOutside(elementRef, (ev) => {
props.onContentFocusOutside();
const target = ev.target as HTMLElement;
// Only dismiss content when focus moves outside of the menu
if (context!.rootNavigationMenu?.value?.contains(target)) ev.preventDefault();
});
const handleKeydown = (ev: KeyboardEvent) => {
const isMetaKey = ev.altKey || ev.ctrlKey || ev.metaKey;
const isTabKey = ev.key === "Tab" && !isMetaKey;
const candidates = getTabbableCandidates(ev.currentTarget as HTMLElement);
const triggerItems = getItems(context?.rootNavigationMenu.value);
const triggerButtonIndex = triggerItems.findIndex(
(i) => i === props.triggerRef.value
);
const nextTriggerButton = triggerItems[triggerButtonIndex + 1];
if (isTabKey) {
const focusedElement = document.activeElement;
const index = candidates.findIndex(
(candidate) => candidate === focusedElement
);
if (
index === candidates.length - 1 &&
triggerButtonIndex !== triggerItems.length - 1
) {
return nextTriggerButton.focus();
}
const isMovingBackwards = ev.shiftKey;
if (isMovingBackwards && index === 0) {
props.triggerRef.value?.focus();
const nextCandidates = isMovingBackwards
? candidates.slice(0, index).reverse()
: candidates.slice(index + 1, candidates.length);
if (focusFirst(nextCandidates)) {
// prevent browser tab keydown because we've handled focus
ev.preventDefault();
} else {
const nextCandidates = isMovingBackwards
? candidates.slice(0, index).reverse()
: candidates.slice(index + 1, candidates.length);
if (focusFirst(nextCandidates)) {
// prevent browser tab keydown because we've handled focus
ev.preventDefault();
} else {
// If we can't focus that means we're at the edges
// so focus the proxy and let browser handle
// tab/shift+tab keypress on the proxy instead
props.focusProxyRef.value?.focus();
}
// If we can't focus that means we're at the edges
// so focus the proxy and let browser handle
// tab/shift+tab keypress on the proxy instead
props.focusProxyRef.value?.focus();
return;
}
}
Expand All @@ -114,8 +108,8 @@ const handleKeydown = (ev: KeyboardEvent) => {
undefined,
{ itemsArray: candidates, loop: false }
);
newSelectedElement?.focus();
ev.preventDefault();
};
const handleEscape = (ev: KeyboardEvent) => {
Expand All @@ -129,10 +123,11 @@ defineExpose({

<template>
<div
ref="elementRef"
:id="contentId"
:aria-labelledby="triggerId"
:data-motion="motionAttribute"
@keydown.prevent="handleKeydown"
@keydown="handleKeydown"
@keydown.escape.prevent="handleEscape"
>
<slot></slot>
Expand Down
40 changes: 27 additions & 13 deletions packages/radix-vue/src/NavigationMenu/NavigationMenuItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export type NavigationMenuItemContextValue = {
focusProxyRef: Ref<HTMLElement | undefined>;
wasEscapeCloseRef: Ref<boolean>;
onEntryKeyDown(): void;
// onFocusProxyEnter(side: 'start' | 'end'): void;
onFocusProxyEnter(side: "start" | "end"): void;
// onRootContentClose(): void;
// onContentFocusOutside(): void;
onContentFocusOutside(): void;
};
export const NAVIGATION_MENU_ITEM_INJECTION_KEY =
Expand All @@ -31,7 +31,7 @@ import {
} from "vue";
import { PrimitiveLi } from "@/Primitive";
import { useArrowNavigation, useCollection, useId } from "@/shared";
import { getTabbableCandidates, focusFirst } from "./utils";
import { getTabbableCandidates, removeFromTabOrder, focusFirst } from "./utils";
import { unrefElement } from "@vueuse/core";
const props = defineProps<NavigationMenuItemProps>();
Expand All @@ -42,25 +42,39 @@ const value = props.value || useId();
const triggerRef = ref<HTMLElement>();
const contentRef = ref<VNode>();
const focusProxyRef = ref<HTMLElement>();
let restoreContentTabOrderRef: () => void = () => ({});
const wasEscapeCloseRef = ref(false);
const handleContentEntry = async (side = "start") => {
// @ts-ignore
const el = contentRef.value?.children?.[0]?.el.parentElement as HTMLElement;
if (el) {
restoreContentTabOrderRef();
const candidates = getTabbableCandidates(unrefElement(el) as HTMLElement);
if (candidates.length)
focusFirst(side === "start" ? candidates : candidates.reverse());
}
};
const handleContentExit = () => {
// @ts-ignore
const el = contentRef.value?.children?.[0]?.el.parentElement as HTMLElement;
if (el) {
const candidates = getTabbableCandidates(unrefElement(el) as HTMLElement);
if (candidates.length)
restoreContentTabOrderRef = removeFromTabOrder(candidates);
}
};
provide(NAVIGATION_MENU_ITEM_INJECTION_KEY, {
value,
triggerRef,
contentRef,
focusProxyRef,
wasEscapeCloseRef,
onEntryKeyDown: (side = "start") => {
// @ts-ignore
const el = contentRef.value?.children?.[0]?.el.parentElement as HTMLElement;
if (el) {
// @ts-ignore
const candidates = getTabbableCandidates(unrefElement(el));
if (candidates.length)
focusFirst(side === "start" ? candidates : candidates.reverse());
}
},
onEntryKeyDown: handleContentEntry,
onFocusProxyEnter: handleContentEntry,
onContentFocusOutside: handleContentExit,
});
const handleClose = () => {
Expand Down
35 changes: 29 additions & 6 deletions packages/radix-vue/src/NavigationMenu/NavigationMenuRoot.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { type InjectionKey, type Ref } from "vue";
import type { Direction, Orientation } from "./utils";
import { useCollection, useId } from "@/shared";
import { onFocusOutside, useCollection, useId } from "@/shared";
export interface NavigationMenuProps {
modelValue?: string;
Expand All @@ -21,6 +21,10 @@ export interface NavigationMenuProps {
skipDelayDuration?: number;
}
interface VNodeWithParentProps extends VNode {
parentProps: any;
}
export interface NavigationMenuContextValue {
isRootMenu: boolean;
modelValue: Ref<string>;
Expand All @@ -33,8 +37,11 @@ export interface NavigationMenuContextValue {
onIndicatorTrackChange(indicatorTrack: HTMLElement | undefined): void;
viewport: Ref<HTMLElement | undefined>;
onViewportChange(viewport: HTMLElement | undefined): void;
viewportContent: Ref<Map<string, VNode>>;
onViewportContentChange(contentValue: string, contentData: VNode): void;
viewportContent: Ref<Map<string, VNodeWithParentProps>>;
onViewportContentChange(
contentValue: string,
contentData: VNodeWithParentProps
): void;
onViewportContentRemove(contentValue: string): void;
onTriggerEnter(itemValue: string): void;
onTriggerLeave(): void;
Expand All @@ -49,8 +56,13 @@ export const NAVIGATION_MENU_INJECTION_KEY =
</script>

<script setup lang="ts">
import { useDebounceFn, useVModel } from "@vueuse/core";
import { provide, ref, type VNode } from "vue";
import {
onClickOutside,
useDebounceFn,
useFocusWithin,
useVModel,
} from "@vueuse/core";
import { provide, ref, watch, type VNode } from "vue";
import { PrimitiveNav, usePrimitiveElement } from "@/Primitive";
const props = withDefaults(defineProps<NavigationMenuProps>(), {
Expand All @@ -76,9 +88,20 @@ const { primitiveElement, currentElement: rootNavigationMenu } =
const { createCollection } = useCollection();
createCollection();
const closeMenu = () => {
modelValue.value = "";
};
onClickOutside(rootNavigationMenu, () => {
closeMenu();
});
onFocusOutside(rootNavigationMenu, () => {
closeMenu();
});
const indicatorTrack = ref<HTMLElement>();
const viewport = ref<HTMLElement>();
const viewportContent = ref<Map<string, VNode>>(new Map());
const viewportContent = ref<Map<string, VNodeWithParentProps>>(new Map());
const debouncedFn = useDebounceFn((val: string) => {
previousValue.value = modelValue.value;
Expand Down
40 changes: 36 additions & 4 deletions packages/radix-vue/src/NavigationMenu/NavigationMenuTrigger.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { NAVIGATION_MENU_INJECTION_KEY } from "./NavigationMenuRoot.vue";
import { NAVIGATION_MENU_ITEM_INJECTION_KEY } from "./NavigationMenuItem.vue";
import { computed, inject, onMounted, ref } from "vue";
import { computed, inject, onMounted, ref, type VNode } from "vue";
import { PrimitiveButton, usePrimitiveElement } from "@/Primitive";
import { VisuallyHidden } from "@/VisuallyHidden";
import { makeTriggerId, makeContentId, getOpenState } from "./utils";
import { unrefElement } from "@vueuse/core";
const props = defineProps<{
disabled?: boolean;
Expand Down Expand Up @@ -56,7 +58,11 @@ const handlePointerLeave = (ev: PointerEvent) => {
};
const handleClick = (ev: MouseEvent) => {
context?.onItemSelect(itemContext!.value);
if (open.value) {
context?.onItemSelect("");
} else {
context?.onItemSelect(itemContext!.value);
}
wasClickCloseRef.value = open.value;
};
Expand All @@ -65,13 +71,33 @@ const handleKeydown = (ev: KeyboardEvent) => {
const entryKey = { horizontal: "ArrowDown", vertical: verticalEntryKey }[
context!.orientation
];
if (open.value && (ev.key === entryKey || ev.key === "Tab")) {
if (open.value && ev.key === entryKey) {
itemContext!.onEntryKeyDown();
// Prevent FocusGroupItem from handling the event
ev.preventDefault();
ev.stopPropagation();
}
};
const setFocusProxyRef = (node: VNode) => {
// @ts-ignore
itemContext!.focusProxyRef.value = unrefElement(node);
return undefined;
};
const handleVisuallyHiddenFocus = (ev: FocusEvent) => {
const content = // @ts-ignore
(itemContext!.contentRef.value?.children?.[0].el as HTMLElement)
.parentElement;
const prevFocusedElement = ev.relatedTarget as HTMLElement | null;
const wasTriggerFocused = prevFocusedElement === triggerElement.value;
const wasFocusFromContent = content?.contains(prevFocusedElement);
if (wasTriggerFocused || !wasFocusFromContent) {
itemContext!.onFocusProxyEnter(wasTriggerFocused ? "start" : "end");
}
};
</script>

<template>
Expand All @@ -95,7 +121,13 @@ const handleKeydown = (ev: KeyboardEvent) => {
</PrimitiveButton>

<template v-if="open">
<!-- VisuallyHiddenPrimitive -->
<VisuallyHidden
aria-hidden
:tabIndex="0"
:ref="setFocusProxyRef"
@focus="handleVisuallyHiddenFocus"
>
</VisuallyHidden>
<span :aria-owns="contentId" v-if="context?.viewport"></span>
</template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,7 @@ defineOptions({
<Presence :present="activeContentValue === node.props?.value">
<NavigationMenuContentImpl
:ref="setRef"
v-bind="
//@ts-ignore
node.parentProps
"
:value="node.props?.value"
:triggerRef="node.props?.triggerRef"
:focusProxyRef="node.props?.focusProxyRef"
:wasEscapeCloseRef="node.props?.wasEscapeCloseRef"
v-bind="{ ...node.props, ...node.parentProps }"
@escape="handleClose(node)"
>
<component :is="node"></component>
Expand Down

0 comments on commit 5014df5

Please sign in to comment.