vuetify
Version:
Vue Material Component Framework
199 lines (197 loc) • 5.57 kB
JavaScript
// Utilities
import { onScopeDispose, shallowRef, toValue } from 'vue';
// Types
// pulled from v0, should be replaced by direct v0 import in v5.0
// the only change was adapting to jsDOM by adding `?` in `el.scrollIntoView?.({ block: 'nearest' })`
export function useVirtualFocus(items, options) {
const {
control,
orientation = 'vertical',
circular = false,
columns: _columns,
onHighlight
} = options;
const highlightedId = shallowRef();
let previousEl = null;
function applyHighlight(id) {
const item = items().find(i => i.id === id);
if (!item?.el) return false;
const el = toValue(item.el);
if (!el) return false;
const controlEl = toValue(control);
if (controlEl) {
const itemId = el.getAttribute('id');
if (itemId) controlEl.setAttribute('aria-activedescendant', itemId);
}
if (previousEl) delete previousEl.dataset.highlighted;
el.dataset.highlighted = '';
previousEl = el;
el.scrollIntoView?.({
block: 'nearest'
});
onHighlight?.(id);
return true;
}
function mod(n, i) {
return (i % n + n) % n;
}
function indexOf(id) {
return items().findIndex(item => item.id === id);
}
function go(id) {
highlightedId.value = id;
applyHighlight(id);
}
function step(stride) {
const all = items();
if (!all.length) return;
const current = highlightedId.value == null ? -1 : indexOf(highlightedId.value);
const dir = stride > 0 ? 1 : -1;
const abs = Math.abs(stride);
const maxHops = Math.ceil(all.length / abs);
let index = current + stride;
let hops = 0;
if (circular) {
index = mod(all.length, index);
} else if (index < 0 || index >= all.length) {
return;
}
while (hops < maxHops) {
const item = all[index];
if (item && !toValue(item.disabled)) {
go(item.id);
return;
}
hops++;
index = circular ? mod(all.length, index + stride) : index + dir * abs;
if (!circular && (index < 0 || index >= all.length)) return;
}
}
function first() {
const item = items().find(i => !toValue(i.disabled));
if (item) go(item.id);
}
function last() {
const item = [...items()].reverse().find(i => !toValue(i.disabled));
if (item) go(item.id);
}
function rowFirst() {
const cols = toValue(_columns) ?? 0;
if (!cols) return first();
const all = items();
const cur = highlightedId.value == null ? 0 : Math.max(0, indexOf(highlightedId.value));
const start = cur - cur % cols;
for (let i = start; i < Math.min(start + cols, all.length); i++) {
if (!toValue(all[i]?.disabled)) {
go(all[i].id);
return;
}
}
}
function rowLast() {
const cols = toValue(_columns) ?? 0;
if (!cols) return last();
const all = items();
const cur = highlightedId.value == null ? 0 : Math.max(0, indexOf(highlightedId.value));
const start = cur - cur % cols;
const end = Math.min(start + cols, all.length);
for (let i = end - 1; i >= start; i--) {
if (!toValue(all[i]?.disabled)) {
go(all[i].id);
return;
}
}
}
function highlight(id) {
const prev = highlightedId.value;
highlightedId.value = id;
if (!applyHighlight(id)) highlightedId.value = prev;
}
function focusHighlighted() {
if (highlightedId.value == null) return;
const item = items().find(i => i.id === highlightedId.value);
if (!item?.el) return;
toValue(item.el)?.focus();
}
function clear() {
highlightedId.value = undefined;
toValue(control)?.removeAttribute('aria-activedescendant');
if (previousEl) {
delete previousEl.dataset.highlighted;
previousEl = null;
}
}
function isRtl(e) {
const el = e.currentTarget;
return el ? getComputedStyle(el).direction === 'rtl' : false;
}
function onKeydown(e) {
const cols = toValue(_columns);
const rtl = isRtl(e);
if (cols) {
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
step(rtl ? -1 : 1);
break;
case 'ArrowLeft':
e.preventDefault();
step(rtl ? 1 : -1);
break;
case 'ArrowDown':
e.preventDefault();
step(cols);
break;
case 'ArrowUp':
e.preventDefault();
step(-cols);
break;
case 'Home':
e.preventDefault();
e.ctrlKey ? first() : rowFirst();
break;
case 'End':
e.preventDefault();
e.ctrlKey ? last() : rowLast();
break;
}
return;
}
const prevKeys = [];
const nextKeys = [];
if (orientation === 'vertical' || orientation === 'both') {
prevKeys.push('ArrowUp');
nextKeys.push('ArrowDown');
}
if (orientation === 'horizontal' || orientation === 'both') {
prevKeys.push(rtl ? 'ArrowRight' : 'ArrowLeft');
nextKeys.push(rtl ? 'ArrowLeft' : 'ArrowRight');
}
if (prevKeys.includes(e.key)) {
e.preventDefault();
step(-1);
} else if (nextKeys.includes(e.key)) {
e.preventDefault();
step(1);
} else if (e.key === 'Home') {
e.preventDefault();
first();
} else if (e.key === 'End') {
e.preventDefault();
last();
}
}
onScopeDispose(clear);
return {
highlightedId,
highlight,
focusHighlighted,
clear,
next: () => step(1),
prev: () => step(-1),
first,
last,
onKeydown
};
}
//# sourceMappingURL=virtualFocus.js.map