Skip to content

Commit

Permalink
[Tabs]feat: 2/3 (#182)
Browse files Browse the repository at this point in the history
* feat: Enhance useArrowNavigation to Skip Disabled

* feat: Added loop flag in useArrowNavigation

* feat: Tabs completed API

* fix preventdefault, tabindex

---------

Co-authored-by: khairulhaaziq <[email protected]>
  • Loading branch information
onmax and k11q authored Jul 14, 2023
1 parent ebb995f commit 4fa6fce
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 45 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ For changelog, visit [radix-vue.com/overview/releases](https://radix-vue.com/ove
| [Separator](https://github.com/radix-vue/radix-vue/issues/20) ||| |
| [Slider](https://github.com/radix-vue/radix-vue/issues/21) || | |
| [Switch](https://github.com/radix-vue/radix-vue/issues/22) ||| |
| [Tabs](https://github.com/radix-vue/radix-vue/issues/23) || | |
| [Tabs](https://github.com/radix-vue/radix-vue/issues/23) || | |
| [Toggle](https://github.com/radix-vue/radix-vue/issues/25) ||| |
| [Toggle Group](https://github.com/radix-vue/radix-vue/issues/26) || | |
| [Toolbar](https://github.com/radix-vue/radix-vue/issues/27) || | |
Expand Down
1 change: 1 addition & 0 deletions packages/radix-vue/src/Tabs/TabsContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const dataState = computed<"active" | "inactive">(() => {

<template>
<PrimitiveDiv
:asChild="asChild"
v-if="injectedValue?.modelValue?.value === props.value"
role="tabpanel"
:data-state="dataState"
Expand Down
1 change: 1 addition & 0 deletions packages/radix-vue/src/Tabs/TabsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const { primitiveElement, currentElement: parentElement } =
onMounted(() => {
injectedValue!.parentElement.value = parentElement.value;
injectedValue!.loop = props.loop;
});
</script>

Expand Down
23 changes: 19 additions & 4 deletions packages/radix-vue/src/Tabs/TabsRoot.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Ref, InjectionKey } from "vue";
import type { InjectionKey, Ref } from "vue";
import type { DataOrientation, Direction } from "../shared/types";
export interface TabsRootProps {
Expand All @@ -9,23 +9,27 @@ export interface TabsRootProps {
dir?: Direction;
activationMode?: "automatic" | "manual";
modelValue?: string;
onValueChange?: (value: string) => void;
}
export const TABS_INJECTION_KEY = Symbol() as InjectionKey<TabsProvideValue>;
export interface TabsProvideValue {
modelValue?: Readonly<Ref<string | undefined>>;
currentFocusedElement?: Ref<HTMLElement | undefined>;
changeModelValue: (value: string) => void;
parentElement: Ref<HTMLElement | undefined>;
orientation: DataOrientation;
dir?: Direction;
dir: Direction;
activationMode: "automatic" | "manual";
loop: boolean;
}
</script>

<script setup lang="ts">
import { ref, provide } from "vue";
import { PrimitiveDiv } from "@/Primitive";
import { useVModel } from "@vueuse/core";
import { provide, ref } from "vue";
const props = withDefaults(defineProps<TabsRootProps>(), {
asChild: false,
Expand All @@ -37,6 +41,7 @@ const props = withDefaults(defineProps<TabsRootProps>(), {
const emits = defineEmits(["update:modelValue"]);
const parentElementRef = ref<HTMLElement>();
const currentFocusedElementRef = ref<HTMLElement>();
const modelValue = useVModel(props, "modelValue", emits, {
defaultValue: props.defaultValue,
Expand All @@ -47,15 +52,25 @@ provide<TabsProvideValue>(TABS_INJECTION_KEY, {
modelValue,
changeModelValue: (value: string) => {
modelValue.value = value;
if (value && props.onValueChange) {
props.onValueChange(value);
}
},
currentFocusedElement: currentFocusedElementRef,
parentElement: parentElementRef,
orientation: props.orientation,
dir: props.dir,
loop: true,
activationMode: props.activationMode,
});
</script>

<template>
<PrimitiveDiv :dir="props.dir" :data-orientation="props.orientation">
<PrimitiveDiv
:asChild="asChild"
:dir="props.dir"
:data-orientation="props.orientation"
>
<slot />
</PrimitiveDiv>
</template>
45 changes: 35 additions & 10 deletions packages/radix-vue/src/Tabs/TabsTrigger.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
<script lang="ts">
export interface TabsTriggerProps {
asChild?: boolean;
value?: string;
value: string;
disabled: boolean;
asChild?: boolean;
}
</script>

<script setup lang="ts">
import { inject } from "vue";
import { inject, computed } from "vue";
import { PrimitiveButton, usePrimitiveElement } from "@/Primitive";
import { TABS_INJECTION_KEY } from "./TabsRoot.vue";
import type { TabsProvideValue } from "./TabsRoot.vue";
import { useArrowNavigation } from "../shared";
const injectedValue = inject<TabsProvideValue>(TABS_INJECTION_KEY);
const { primitiveElement, currentElement: currentToggleElement } =
usePrimitiveElement();
const { primitiveElement, currentElement } = usePrimitiveElement();
const props = withDefaults(defineProps<TabsTriggerProps>(), {
asChild: false,
Expand All @@ -27,33 +26,59 @@ function changeTab(value: string) {
}
function handleKeydown(e: KeyboardEvent) {
if (
injectedValue?.orientation === "horizontal" &&
(e.key === "ArrowLeft" || e.key === "ArrowRight")
) {
e.preventDefault();
} else if (
injectedValue?.orientation === "vertical" &&
(e.key === "ArrowUp" || e.key === "ArrowDown")
) {
e.preventDefault();
}
const newSelectedElement = useArrowNavigation(
e,
currentToggleElement.value!,
currentElement.value!,
injectedValue?.parentElement.value!,
{ arrowKeyOptions: "horizontal" }
{ arrowKeyOptions: injectedValue?.orientation, loop: injectedValue?.loop }
);
if (newSelectedElement) {
newSelectedElement.focus();
changeTab(newSelectedElement?.getAttribute("data-radix-vue-tab-value")!);
injectedValue!.currentFocusedElement!.value = newSelectedElement;
if (injectedValue?.activationMode === "automatic") {
changeTab(newSelectedElement?.getAttribute("data-radix-vue-tab-value")!);
}
}
}
const getTabIndex = computed(() => {
if (!injectedValue?.currentFocusedElement?.value) {
return injectedValue?.modelValue?.value === props.value ? "0" : "-1";
} else
return injectedValue?.currentFocusedElement?.value === currentElement.value
? "0"
: "-1";
});
</script>

<template>
<PrimitiveButton
ref="primitiveElement"
type="button"
:asChild="props.asChild"
role="tab"
:aria-selected="
injectedValue?.modelValue?.value === props.value ? 'true' : 'false'
"
:data-state="
injectedValue?.modelValue?.value === props.value ? 'active' : 'inactive'
"
:data-disabled="props.disabled"
:tabindex="injectedValue?.modelValue?.value === props.value ? '0' : '-1'"
:disabled="props.disabled"
:data-disabled="props.disabled ? '' : undefined"
:tabindex="getTabIndex"
:data-orientation="injectedValue?.orientation"
data-radix-vue-collection-item
:data-radix-vue-tab-value="props.value"
Expand Down
58 changes: 38 additions & 20 deletions packages/radix-vue/src/shared/useArrowNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,34 @@ interface ArrowNavigationOptions {
attributeName?: string;
arrowKeyOptions?: ArrowKeyOptions; //default to both
itemsArray?: HTMLElement[];
loop?: boolean;
}

// recursive function to find the next focusable element to avoid disabled elements
function findNextFocusableElement(
elements: HTMLElement[],
currentElement: HTMLElement,
direction: "next" | "previous",
loop = true
): HTMLElement | null {
const index = elements.indexOf(currentElement);
const newIndex = direction === "next" ? index + 1 : index - 1;

if (!loop && (newIndex < 0 || newIndex >= elements.length)) return null;

const adjustedNewIndex = (newIndex + elements.length) % elements.length;
const candidate = elements[adjustedNewIndex];
if (!candidate) return null;

const isDisabled =
candidate.hasAttribute("disabled") &&
candidate.getAttribute("disabled") !== "false";
if (isDisabled) {
return findNextFocusableElement(elements, candidate, direction, loop);
}
return candidate;
}

/**
* allow arrow navigation for every html element with data-radix-vue-collection-item tag
* @param e Keyboard event
Expand Down Expand Up @@ -32,10 +59,6 @@ export function useArrowNavigation(
}

if (allCollectionItems.length) {
const currentTabIndex = allCollectionItems.indexOf(currentElement!);

let newFocusedElement: HTMLElement | null = null;

let nextKeys = ["ArrowRight", "ArrowDown"];
let previousKeys = ["ArrowLeft", "ArrowUp"];

Expand All @@ -47,24 +70,19 @@ export function useArrowNavigation(
previousKeys = ["ArrowUp"];
}

if (nextKeys.includes(e.key)) {
e.preventDefault();
if (allCollectionItems[currentTabIndex + 1]) {
newFocusedElement = allCollectionItems[currentTabIndex + 1];
} else {
newFocusedElement = allCollectionItems[0];
}
}
const isNextKey = nextKeys.includes(e.key);
const isPreviousKey = previousKeys.includes(e.key);

if (previousKeys.includes(e.key)) {
e.preventDefault();
if (allCollectionItems[currentTabIndex - 1]) {
newFocusedElement = allCollectionItems[currentTabIndex - 1];
} else {
newFocusedElement = allCollectionItems[allCollectionItems.length - 1];
}
if (!isNextKey && !isPreviousKey) {
return null;
}
return newFocusedElement;

return findNextFocusableElement(
allCollectionItems,
currentElement,
nextKeys.includes(e.key) ? "next" : "previous",
options.loop
);
}

return null;
Expand Down
4 changes: 2 additions & 2 deletions playground/vue3/src/components/Demo/TabsDemo.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'radix-vue'
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from '../../../../../packages/radix-vue/src'
import { ref } from 'vue'
const toggleStateSingle = ref('tab1')
Expand All @@ -9,7 +9,7 @@ const toggleStateSingle = ref('tab1')
<div class="absolute left-4 top-3 text-sm">
<p>Value: {{ toggleStateSingle }}</p>
</div>
<TabsRoot class="flex flex-col w-[300px] shadow-[0_2px_10px] shadow-blackA4" default-value="tab1">
<TabsRoot orientation="vertical" class="flex flex-col w-[300px] shadow-[0_2px_10px] shadow-blackA4" default-value="tab1">
<TabsList class="shrink-0 flex border-b border-mauve6" aria-label="Manage your account">
<TabsTrigger
class="bg-white px-5 h-[45px] flex-1 flex items-center justify-center text-[15px] leading-none text-mauve11 select-none first:rounded-tl-md last:rounded-tr-md hover:text-violet11 data-[state=active]:text-violet11 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current data-[state=active]:focus:relative data-[state=active]:focus:shadow-[0_0_0_2px] data-[state=active]:focus:shadow-black outline-none cursor-default"
Expand Down
15 changes: 7 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4fa6fce

Please sign in to comment.