Vue.js composition API wrappers for popular utility libraries enabling seamless integration of third-party tools
—
User interface enhancements including drag-and-drop sorting and focus management for improved accessibility and user experience.
Drag-and-drop sorting functionality using SortableJS with reactive list updates.
/**
* Drag-and-drop sorting with reactive list updates
* @param selector - CSS selector for sortable container
* @param list - Reactive array to keep in sync with DOM order
* @param options - SortableJS and VueUse configuration options
* @returns Sortable control interface
*/
function useSortable<T>(
selector: string,
list: MaybeRef<T[]>,
options?: UseSortableOptions
): UseSortableReturn;
/**
* Drag-and-drop sorting with element reference
* @param el - Element or element getter function
* @param list - Reactive array to keep in sync with DOM order
* @param options - SortableJS and VueUse configuration options
* @returns Sortable control interface
*/
function useSortable<T>(
el: MaybeRefOrGetter<MaybeElement>,
list: MaybeRef<T[]>,
options?: UseSortableOptions
): UseSortableReturn;
interface UseSortableReturn {
/** Start the sortable functionality */
start: () => void;
/** Stop the sortable functionality */
stop: () => void;
/** Get or set SortableJS options */
option: (<K extends keyof Sortable.Options>(name: K, value: Sortable.Options[K]) => void)
& (<K extends keyof Sortable.Options>(name: K) => Sortable.Options[K]);
}
type UseSortableOptions = Sortable.Options & ConfigurableDocument;
// SortableJS Options (key ones)
namespace Sortable {
interface Options {
group?: string | GroupOptions;
sort?: boolean;
disabled?: boolean;
animation?: number;
handle?: string;
filter?: string;
draggable?: string;
ghostClass?: string;
chosenClass?: string;
dragClass?: string;
direction?: 'vertical' | 'horizontal';
touchStartThreshold?: number;
emptyInsertThreshold?: number;
onStart?: (evt: SortableEvent) => void;
onEnd?: (evt: SortableEvent) => void;
onAdd?: (evt: SortableEvent) => void;
onUpdate?: (evt: SortableEvent) => void;
onSort?: (evt: SortableEvent) => void;
onRemove?: (evt: SortableEvent) => void;
onMove?: (evt: MoveEvent) => boolean | -1 | 1 | void;
onClone?: (evt: SortableEvent) => void;
onChange?: (evt: SortableEvent) => void;
}
interface SortableEvent {
oldIndex?: number;
newIndex?: number;
item: HTMLElement;
from: HTMLElement;
to: HTMLElement;
clone: HTMLElement;
}
}Usage Examples:
import { useSortable } from "@vueuse/integrations/useSortable";
import { ref } from 'vue';
// Basic sortable list
const items = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
const { start, stop } = useSortable('.sortable-list', items, {
animation: 150,
ghostClass: 'ghost',
onEnd: (evt) => {
console.log('Item moved from', evt.oldIndex, 'to', evt.newIndex);
}
});
// With element reference
const sortableEl = ref<HTMLElement>();
const { start, stop } = useSortable(sortableEl, items);
// Advanced configuration
const todoItems = ref([
{ id: 1, text: 'Learn Vue.js', completed: false },
{ id: 2, text: 'Build an app', completed: false },
{ id: 3, text: 'Deploy to production', completed: true }
]);
const { start, stop, option } = useSortable('.todo-list', todoItems, {
handle: '.drag-handle',
filter: '.no-drag',
animation: 200,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onStart: (evt) => {
console.log('Drag started');
},
onEnd: (evt) => {
console.log('Drag ended');
// List is automatically updated
}
});
// Dynamic options
option('animation', 300);
const currentAnimation = option('animation');
// Multiple lists with shared group
const list1 = ref(['A', 'B', 'C']);
const list2 = ref(['X', 'Y', 'Z']);
useSortable('.list-1', list1, {
group: 'shared',
animation: 150
});
useSortable('.list-2', list2, {
group: 'shared',
animation: 150
});Helper functions for DOM manipulation in sortable contexts.
/**
* Insert a DOM node at a specific index within a parent element
* @param parentElement - Parent container element
* @param element - Element to insert
* @param index - Target index position
*/
function insertNodeAt(parentElement: Element, element: Element, index: number): void;
/**
* Remove a DOM node from its parent
* @param node - Node to remove
*/
function removeNode(node: Node): void;
/**
* Move an array element from one index to another
* @param list - Reactive array to modify
* @param from - Source index
* @param to - Target index
* @param e - Optional SortableJS event for additional context
*/
function moveArrayElement<T>(
list: MaybeRef<T[]>,
from: number,
to: number,
e?: Sortable.SortableEvent | null
): void;Declarative sortable component for template-based usage.
/**
* Declarative sortable component
*/
const UseSortable = defineComponent({
name: 'UseSortable',
props: {
/** Reactive array model */
modelValue: {
type: Array as PropType<any[]>,
required: true
},
/** HTML tag to render */
tag: {
type: String,
default: 'div'
},
/** SortableJS options */
options: {
type: Object as PropType<UseSortableOptions>,
required: true
}
},
emits: ['update:modelValue'],
slots: {
default: (props: {
item: any;
index: number;
}) => any;
}
});Component Usage:
<template>
<UseSortable
v-model="items"
tag="ul"
:options="sortableOptions"
class="sortable-list"
>
<template #default="{ item, index }">
<li :key="item.id" class="sortable-item">
<span class="drag-handle">⋮⋮</span>
{{ item.text }}
<button @click="removeItem(index)">Remove</button>
</li>
</template>
</UseSortable>
</template>
<script setup>
import { UseSortable } from "@vueuse/integrations/useSortable";
import { ref } from 'vue';
const items = ref([
{ id: 1, text: 'First item' },
{ id: 2, text: 'Second item' },
{ id: 3, text: 'Third item' }
]);
const sortableOptions = {
handle: '.drag-handle',
animation: 150,
ghostClass: 'ghost-item'
};
const removeItem = (index) => {
items.value.splice(index, 1);
};
</script>
<style>
.sortable-list {
list-style: none;
padding: 0;
}
.sortable-item {
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 5px;
cursor: move;
}
.ghost-item {
opacity: 0.5;
}
.drag-handle {
color: #999;
margin-right: 10px;
cursor: grab;
}
</style>Focus management and accessibility enhancement using focus-trap.
/**
* Focus management and accessibility enhancement
* @param target - Target element(s) to trap focus within
* @param options - Focus trap configuration options
* @returns Focus trap control interface
*/
function useFocusTrap(
target: MaybeRefOrGetter<Arrayable<MaybeRefOrGetter<string> | MaybeComputedElementRef>>,
options?: UseFocusTrapOptions
): UseFocusTrapReturn;
interface UseFocusTrapReturn {
/** Whether focus trap currently has focus */
hasFocus: ShallowRef<boolean>;
/** Whether focus trap is paused */
isPaused: ShallowRef<boolean>;
/** Activate the focus trap */
activate: (opts?: ActivateOptions) => void;
/** Deactivate the focus trap */
deactivate: (opts?: DeactivateOptions) => void;
/** Pause the focus trap */
pause: Fn;
/** Unpause the focus trap */
unpause: Fn;
}
interface UseFocusTrapOptions extends Options {
/** Activate focus trap immediately */
immediate?: boolean;
}
// focus-trap options
interface Options {
onActivate?: (focusTrapInstance: FocusTrap) => void;
onDeactivate?: (focusTrapInstance: FocusTrap) => void;
onPause?: (focusTrapInstance: FocusTrap) => void;
onUnpause?: (focusTrapInstance: FocusTrap) => void;
onPostActivate?: (focusTrapInstance: FocusTrap) => void;
onPostDeactivate?: (focusTrapInstance: FocusTrap) => void;
checkCanFocusTrap?: (focusTrapContainers: HTMLElement[]) => Promise<void>;
checkCanReturnFocus?: (triggerElement: HTMLElement) => Promise<void>;
initialFocus?: string | HTMLElement | (() => HTMLElement | string) | false;
fallbackFocus?: string | HTMLElement | (() => HTMLElement | string);
escapeDeactivates?: boolean | ((e: KeyboardEvent) => boolean);
clickOutsideDeactivates?: boolean | ((e: MouseEvent | TouchEvent) => boolean);
returnFocusOnDeactivate?: boolean;
setReturnFocus?: HTMLElement | string | ((nodeFocusedBeforeActivation: HTMLElement) => HTMLElement | string);
allowOutsideClick?: boolean | ((e: MouseEvent | TouchEvent) => boolean);
preventScroll?: boolean;
tabbableOptions?: TabbableOptions;
}
interface ActivateOptions {
onActivate?: (focusTrapInstance: FocusTrap) => void;
}
interface DeactivateOptions {
onDeactivate?: (focusTrapInstance: FocusTrap) => void;
checkCanReturnFocus?: (trigger: HTMLElement) => Promise<void>;
}Usage Examples:
import { useFocusTrap } from "@vueuse/integrations/useFocusTrap";
import { ref } from 'vue';
// Basic focus trap
const modalRef = ref<HTMLElement>();
const { activate, deactivate, hasFocus } = useFocusTrap(modalRef);
// Show modal with focus trap
const showModal = () => {
// Show modal UI
activate();
};
const closeModal = () => {
deactivate();
// Hide modal UI
};
// Multiple containers
const containers = [
ref<HTMLElement>(),
ref<HTMLElement>()
];
const { activate, deactivate } = useFocusTrap(containers, {
immediate: false,
escapeDeactivates: true,
clickOutsideDeactivates: true
});
// Advanced configuration
const { activate, deactivate, pause, unpause } = useFocusTrap(modalRef, {
initialFocus: '#first-input',
fallbackFocus: '#cancel-button',
onActivate: () => console.log('Focus trap activated'),
onDeactivate: () => console.log('Focus trap deactivated'),
escapeDeactivates: (e) => {
// Custom logic for escape key
return !e.shiftKey;
},
clickOutsideDeactivates: false
});
// Temporarily pause focus trap
const handleOverlay = () => {
pause();
// Handle overlay interaction
setTimeout(unpause, 1000);
};Declarative focus trap component wrapper.
/**
* Declarative focus trap component
*/
const UseFocusTrap = defineComponent({
name: 'UseFocusTrap',
props: {
/** HTML tag to render */
as: {
type: [String, Object] as PropType<string | Component>,
default: 'div'
},
/** Focus trap options */
options: {
type: Object as PropType<UseFocusTrapOptions>,
default: () => ({})
}
},
slots: {
default: (props: {
hasFocus: boolean;
isPaused: boolean;
activate: (opts?: ActivateOptions) => void;
deactivate: (opts?: DeactivateOptions) => void;
pause: Fn;
unpause: Fn;
}) => any;
}
});Component Usage:
<template>
<UseFocusTrap
as="div"
:options="focusTrapOptions"
v-slot="{ hasFocus, activate, deactivate }"
>
<div v-if="isModalOpen" class="modal">
<h2>Modal Title</h2>
<input ref="firstInput" placeholder="First input" />
<input placeholder="Second input" />
<button @click="closeModal">Close</button>
</div>
</UseFocusTrap>
</template>
<script setup>
import { UseFocusTrap } from "@vueuse/integrations/useFocusTrap";
import { ref } from 'vue';
const isModalOpen = ref(false);
const firstInput = ref<HTMLInputElement>();
const focusTrapOptions = {
immediate: true,
initialFocus: () => firstInput.value,
escapeDeactivates: true
};
const closeModal = () => {
isModalOpen.value = false;
};
</script>Install with Tessl CLI
npx tessl i tessl/npm-vueuse--integrations