feat(forms): add useMaskedField and useMaskedInput composables for input masking

This commit is contained in:
2026-06-09 13:54:52 +07:00
parent 6de7c72fb3
commit 07937e26db
426 changed files with 12981 additions and 311 deletions
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PopperAnchorProps } from '../popper';
/**
* The element the popup is positioned against, typically wrapping the Input and Trigger.
* Acts as the Popper anchor and the boundary used for the blur-to-close heuristic.
*/
export interface ComboboxAnchorProps extends PopperAnchorProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PopperArrowProps } from '../popper';
/**
* An optional arrow that visually points from the popup back to the anchor. Renders only
* while the combobox is open. Place inside ComboboxContent.
*/
export type ComboboxArrowProps = PopperArrowProps;
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* A button that clears the current search term and refocuses the input. Typically shown
* as an "x" inside the field while the user is typing.
*/
export interface ComboboxCancelProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { ComboboxContentImplEmits, ComboboxContentImplProps } from './ComboboxContentImpl.vue';
/**
* The popup listbox that holds the options. Mounts only while open (via Presence) and
* positions itself relative to the anchor. Place the Viewport, Items, and Empty inside it.
*/
export type ComboboxContentProps = ComboboxContentImplProps;
export type ComboboxContentEmits = ComboboxContentImplEmits;
</script>
@@ -4,6 +4,10 @@ import type { FocusScopeEmits } from '../focus-scope';
import type { PopperContentProps } from '../popper';
import type { PrimitiveProps } from '../primitive';
/**
* Internal implementation of the content popup: wires up focus scoping, dismiss-on-outside,
* Popper positioning, and the screen-reader result announcer. Use ComboboxContent instead.
*/
export interface ComboboxContentImplProps extends PrimitiveProps, /* @vue-ignore */ Partial<PopperContentProps> {
/** Position strategy. @default 'popper' */
position?: 'inline' | 'popper';
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* Fallback content shown when the current search term matches no items. Renders only when
* the filtered count is zero, unless `always` is set.
*/
export interface ComboboxEmptyProps extends PrimitiveProps {
/** Render even when items exist but none are filtered out. */
always?: boolean;
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* Groups related items under a shared ComboboxLabel. Hides itself automatically when none
* of its items survive the current filter.
*/
export interface ComboboxGroupProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* The text field users type into to filter options. Owns the search term, ARIA combobox
* semantics, and keyboard navigation (arrows, Home/End, Enter to select, Escape to close).
*/
export interface ComboboxInputProps extends PrimitiveProps {
/** Disable the input. */
disabled?: boolean;
@@ -2,6 +2,10 @@
import type { PrimitiveProps } from '../primitive';
import type { AcceptableValue } from './utils';
/**
* A single selectable option in the list. Registers itself for filtering and keyboard
* navigation, toggles selection on click, and highlights on pointer move.
*/
export interface ComboboxItemProps<T extends AcceptableValue = AcceptableValue> extends PrimitiveProps {
/** Item value. Selected/registered identity. */
value: T;
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* Marks the selected state of its parent ComboboxItem, e.g. a checkmark. Renders only when
* that item is selected.
*/
export interface ComboboxItemIndicatorProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* An accessible label for a ComboboxGroup. Its id is referenced by the group's
* `aria-labelledby`, so place it as a direct child of ComboboxGroup.
*/
export interface ComboboxLabelProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PortalProps } from '../teleport';
/**
* Teleports the ComboboxContent into another part of the DOM (defaults to `body`) to escape
* overflow/stacking-context clipping. Wrap ComboboxContent with it.
*/
export interface ComboboxPortalProps extends PortalProps {}
</script>
+7 -3
View File
@@ -2,6 +2,13 @@
import type { Direction } from '../config-provider';
import type { AcceptableValue, ComboboxFilterFunction, ComboboxFilterItem } from './utils';
/**
* An autocomplete / typeahead input that filters a list of options as the user types.
* Combine a text input with a popup listbox, supporting single or multiple selection,
* custom filtering, and full keyboard navigation. Reach for it when users must pick from
* a large or searchable set of options; for a small fixed list a plain Select is simpler.
* Wraps everything in a Popper and provides shared state to every other Combobox part.
*/
export interface ComboboxRootProps<T extends AcceptableValue = AcceptableValue> {
/** Controlled selected value. Use `v-model`. */
modelValue?: T | T[];
@@ -69,8 +76,6 @@ const {
by,
} = defineProps<ComboboxRootProps<T>>();
const emit = defineEmits<ComboboxRootEmits<T>>();
const config = useConfig();
const direction = computed(() => dir ?? config.dir.value);
@@ -203,7 +208,6 @@ function isSelected(v: T): boolean {
function commitValue(next: T | T[] | undefined) {
value.value = next;
emit('update:modelValue', next);
}
function onValueChange(v: T) {
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* A purely visual divider between items or groups inside the popup. Decorative and hidden
* from assistive technology.
*/
export interface ComboboxSeparatorProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* A button, usually a chevron next to the input, that toggles the popup open and closed.
* Optional: typing in the Input also opens the list.
*/
export interface ComboboxTriggerProps extends PrimitiveProps {
/** Disable the trigger independently from the root. */
disabled?: boolean;
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* The scrollable region inside ComboboxContent that holds the items. Provides the overflow
* container that keeps the highlighted item scrolled into view.
*/
export interface ComboboxViewportProps extends PrimitiveProps {}
</script>
+119
View File
@@ -0,0 +1,119 @@
<script setup lang="ts">
import { ref } from 'vue';
import {
ComboboxAnchor,
ComboboxCancel,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxLabel,
ComboboxPortal,
ComboboxRoot,
ComboboxTrigger,
ComboboxViewport,
} from '@robonen/primitives';
interface Framework {
value: string;
label: string;
group: 'JavaScript' | 'Native';
}
const frameworks: Framework[] = [
{ value: 'vue', label: 'Vue', group: 'JavaScript' },
{ value: 'react', label: 'React', group: 'JavaScript' },
{ value: 'svelte', label: 'Svelte', group: 'JavaScript' },
{ value: 'solid', label: 'Solid', group: 'JavaScript' },
{ value: 'angular', label: 'Angular', group: 'JavaScript' },
{ value: 'swiftui', label: 'SwiftUI', group: 'Native' },
{ value: 'compose', label: 'Jetpack Compose', group: 'Native' },
{ value: 'flutter', label: 'Flutter', group: 'Native' },
];
const selected = ref<string>();
function labelFor(value: string | undefined) {
return frameworks.find(f => f.value === value)?.label ?? '';
}
const groups = ['JavaScript', 'Native'] as const;
</script>
<template>
<div class="flex w-full max-w-xs flex-col gap-3">
<ComboboxRoot
v-model="selected"
:display-value="labelFor"
class="relative"
>
<ComboboxAnchor
class="flex items-center gap-1 rounded-lg border border-(--border) bg-(--bg-inset) px-2 py-1.5 focus-within:border-(--accent) focus-within:ring-2 focus-within:ring-(--ring)"
>
<ComboboxInput
placeholder="Search a framework..."
open-on-click
class="min-w-0 flex-1 bg-transparent px-1 text-sm text-(--fg) outline-none placeholder:text-(--fg-subtle)"
/>
<ComboboxCancel
class="grid size-5 place-items-center rounded text-(--fg-subtle) hover:bg-(--bg-subtle) hover:text-(--fg)"
>
<span aria-hidden="true" class="text-xs"></span>
</ComboboxCancel>
<ComboboxTrigger
class="grid size-5 place-items-center rounded text-(--fg-muted) hover:bg-(--bg-subtle) hover:text-(--fg) data-[state=open]:rotate-180"
>
<span aria-hidden="true" class="text-xs"></span>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxPortal>
<ComboboxContent
:side-offset="6"
class="z-50 w-(--popper-anchor-width) overflow-hidden rounded-lg border border-(--border) bg-(--bg-elevated) shadow-lg"
>
<ComboboxViewport class="max-h-60 p-1">
<ComboboxEmpty class="px-3 py-6 text-center text-sm text-(--fg-subtle)">
No frameworks found.
</ComboboxEmpty>
<ComboboxGroup
v-for="group in groups"
:key="group"
class="mb-1 last:mb-0"
>
<ComboboxLabel
class="px-2 py-1 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)"
>
{{ group }}
</ComboboxLabel>
<ComboboxItem
v-for="framework in frameworks.filter(f => f.group === group)"
:key="framework.value"
:value="framework.value"
:text-value="framework.label"
class="flex cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-sm text-(--fg) outline-none data-[highlighted]:bg-(--accent) data-[highlighted]:text-(--accent-fg) data-[disabled]:opacity-50"
>
<span>{{ framework.label }}</span>
<ComboboxItemIndicator>
<span aria-hidden="true"></span>
</ComboboxItemIndicator>
</ComboboxItem>
</ComboboxGroup>
</ComboboxViewport>
</ComboboxContent>
</ComboboxPortal>
</ComboboxRoot>
<p class="text-sm text-(--fg-muted)">
Selected:
<span class="font-medium text-(--fg)">{{ selected ? labelFor(selected) : 'none' }}</span>
</p>
</div>
</template>