eefd7abf83
Reorganize components into category folders (forms/canvas/overlays/etc.); add the media-editor headless family (timeline, curve-editor, waveform, crop, color picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag state, gesture-leak teardown, shallowRef color state, rect caching) and replace source `any` with proper types.
124 lines
3.5 KiB
Vue
124 lines
3.5 KiB
Vue
<script lang="ts">
|
|
import type { PrimitiveProps } from '../../internal/primitive';
|
|
|
|
/**
|
|
* A single focusable item within a `RovingFocusGroup`. It registers itself with
|
|
* the group's collection, exposes itself as the sole tab stop when current
|
|
* (`tabindex="0"`, others `-1`), and handles the arrow-key navigation that moves
|
|
* focus to its siblings. Use `focusable` to opt an item out of the tab order and
|
|
* `active` to mark the current selection so it gets focus on group entry.
|
|
*/
|
|
export interface RovingFocusItemProps extends PrimitiveProps {
|
|
/** Unique tab-stop id. Auto-generated via config `useId` when omitted. */
|
|
tabStopId?: string;
|
|
/**
|
|
* Whether this item is focusable.
|
|
* @default true
|
|
*/
|
|
focusable?: boolean;
|
|
/** Marks the item as active (current selection). */
|
|
active?: boolean;
|
|
/**
|
|
* Allow `Shift+Arrow` for navigation.
|
|
* @default false
|
|
*/
|
|
allowShiftKey?: boolean;
|
|
}
|
|
</script>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, onUnmounted } from 'vue';
|
|
import { focusFirst, getFocusIntent, wrapArray } from './utils';
|
|
import { Primitive } from '../../internal/primitive';
|
|
import { RovingFocusGroupCtx } from './RovingFocusGroup.vue';
|
|
import { useCollectionInjector } from '../collection';
|
|
import { useId } from '../config-provider';
|
|
|
|
const {
|
|
tabStopId,
|
|
focusable = true,
|
|
active = false,
|
|
allowShiftKey = false,
|
|
as = 'span',
|
|
} = defineProps<RovingFocusItemProps>();
|
|
|
|
const context = RovingFocusGroupCtx.inject();
|
|
// `useId` returns a `ComputedRef<string>` in this repo — unwrap where needed.
|
|
const autoId = useId();
|
|
const id = computed(() => tabStopId ?? autoId.value);
|
|
const isCurrentTabStop = computed(() => context.currentTabStopId.value === id.value);
|
|
|
|
const { getItems, CollectionItem } = useCollectionInjector();
|
|
|
|
onMounted(() => {
|
|
if (focusable) context.onFocusableItemAdd();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (focusable) context.onFocusableItemRemove();
|
|
});
|
|
|
|
function handleKeydown(event: KeyboardEvent): void {
|
|
if (event.key === 'Tab' && event.shiftKey) {
|
|
context.onItemShiftTab();
|
|
return;
|
|
}
|
|
|
|
if (event.target !== event.currentTarget) return;
|
|
|
|
const focusIntent = getFocusIntent(event, context.orientation.value, context.dir.value);
|
|
if (focusIntent === undefined) return;
|
|
|
|
if (
|
|
event.metaKey
|
|
|| event.ctrlKey
|
|
|| event.altKey
|
|
|| (allowShiftKey ? false : event.shiftKey)
|
|
)
|
|
return;
|
|
|
|
event.preventDefault();
|
|
|
|
let candidateNodes = getItems()
|
|
.map(i => i.ref)
|
|
.filter(i => i.dataset['disabled'] !== '');
|
|
|
|
if (focusIntent === 'last') {
|
|
candidateNodes.reverse();
|
|
}
|
|
else if (focusIntent === 'prev' || focusIntent === 'next') {
|
|
if (focusIntent === 'prev') candidateNodes.reverse();
|
|
|
|
const currentIndex = candidateNodes.indexOf(event.currentTarget as HTMLElement);
|
|
|
|
candidateNodes = context.loop.value
|
|
? wrapArray(candidateNodes, currentIndex + 1)
|
|
: candidateNodes.slice(currentIndex + 1);
|
|
}
|
|
|
|
nextTick(() => focusFirst(candidateNodes));
|
|
}
|
|
|
|
function handleMousedown(event: MouseEvent): void {
|
|
if (!focusable) event.preventDefault();
|
|
else context.onItemFocus(id.value);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<CollectionItem>
|
|
<Primitive
|
|
:tabindex="isCurrentTabStop ? 0 : -1"
|
|
:data-orientation="context.orientation.value"
|
|
:data-active="active ? '' : undefined"
|
|
:data-disabled="!focusable ? '' : undefined"
|
|
:as="as"
|
|
@mousedown="handleMousedown"
|
|
@focus="context.onItemFocus(id)"
|
|
@keydown="handleKeydown"
|
|
>
|
|
<slot />
|
|
</Primitive>
|
|
</CollectionItem>
|
|
</template>
|