قائمة اختيار مع بحث
حقل نص يصفّي قائمة أثناء الكتابة. يعرض الصفوف الظاهرة فقط بعد 200 خيار، ويستدعي loadMore لجلب الصفحة التالية.
client - الحجم لم يُقَس بعد
قائمة اختيار مع بحث
client - الحجم لم يُقَس بعدحقل نص يصفّي قائمة أثناء الكتابة. يعرض الصفوف الظاهرة فقط بعد 200 خيار، ويستدعي loadMore لجلب الصفحة التالية.
جرّبه
size
invalid
fullWidth
placement
visibleRows
hasMore
loadMoreThreshold
status
الشيفرة
combobox.tsx
import { Combobox } from 'topclassui/combobox'
<div className="grid justify-items-start gap-1">
<label htmlFor="combobox-playground" className="text-sm font-medium text-body">Framework</label>
<Combobox id="combobox-playground" items={FRAMEWORKS} placeholder="Search frameworks" />
</div>تجري التصفية على الخيارات المحمّلة حتى الآن؛ وتصل الصفحة التالية عبر hasMore وloadMore.
كل الحالات، بدون تمرير الفأرة
4 حالاتأو استورده من الحزمة
topclassui/combobox
import { Combobox } from 'topclassui/combobox'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
itemsيأخذ{ value, label, disabled? }: تُطابق الكتابة مع التسمية، ويستلم النموذج القيمة. أبقِ المصفوفة نفسها بين مرات العرض.- المكوّن يطلب ولا يجلب أبدًا: مع
hasMoreيستدعيloadMoreحين تقترب القائمة المفتوحة من نهايتها بمقدارloadMoreThresholdصفوف (10)، وأنت تضيف الصفحة التالية إلىitems. statusيخبر بما هو قادم:loadingيرسم مؤشر تحميل مكان السهم، وloading-moreيكتب سطرًا تحت الخيارات.virtualizeقيمتهautoافتراضيًا: بعد 200 خيار تُركَّب الصفوف الظاهرة وقليل حولها فقط، ويبقى التمييز قادرًا على الوصول إلى كل خيار.- البحث بلا نتيجة يعرض
emptyFallback، أو تسمية "لا نتائج" من المزوّد. وmaxLengthيحدّ البحث بـ256 حرفًا افتراضيًا.
basic
basic.tsx
import { Combobox } from 'topclassui/combobox'
const FRAMEWORKS = [
{ value: 'next', label: 'Next.js' },
{ value: 'astro', label: 'Astro' },
{ value: 'remix', label: 'Remix' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'gatsby', label: 'Gatsby' },
{ value: 'vite', label: 'Vite' },
{ value: 'qwik', label: 'Qwik City' },
{ value: 'solid', label: 'SolidStart' },
{ value: 'angular', label: 'Angular' },
]
/** Named by a label that points at its id. Typing filters by "contains"; the arrows move, Enter chooses. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-1">
<label htmlFor="combobox-basic" className="text-sm font-medium text-body">Framework</label>
<Combobox id="combobox-basic" items={FRAMEWORKS} placeholder="Search frameworks" />
</div>
)
}
form
form.tsx
import { Button } from 'topclassui/button'
import { Combobox } from 'topclassui/combobox'
const FRAMEWORKS = [
{ value: 'next', label: 'Next.js' },
{ value: 'astro', label: 'Astro' },
{ value: 'remix', label: 'Remix' },
]
/** The value, not the label, is what the form receives: framework=next, before any script runs and after. */
export default function Demo() {
return (
<form className="grid justify-items-start gap-3">
<label htmlFor="combobox-form" className="text-sm font-medium text-body">Framework</label>
<Combobox id="combobox-form" name="framework" items={FRAMEWORKS} defaultValue="next" />
<Button type="submit">Save</Button>
</form>
)
}
sizes
sizes.tsx
import { Combobox } from 'topclassui/combobox'
const FRUITS = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
{ value: 'date', label: 'Date' },
]
/** The five steps of the field scale, each an Input wide at its own type step. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-3">
<Combobox aria-label="Fruit, extra small" size="xs" items={FRUITS} defaultValue="apple" />
<Combobox aria-label="Fruit, small" size="sm" items={FRUITS} defaultValue="banana" />
<Combobox aria-label="Fruit, medium" size="md" items={FRUITS} defaultValue="cherry" />
<Combobox aria-label="Fruit, large" size="lg" items={FRUITS} defaultValue="date" />
<Combobox aria-label="Fruit, extra large" size="xl" items={FRUITS} />
</div>
)
}
load-more
Pages asked for: 0, loaded: 1
load-more.tsx
'use client'
import { useCallback, useState } from 'react'
import { Combobox } from 'topclassui/combobox'
const PAGE = 50
const LAST = 4
/** One page of the server's answer, made here so the demo needs no network. */
function pageOf(page: number) {
return Array.from({ length: PAGE }, (_, index) => {
const n = page * PAGE + index + 1
return { value: `r${String(n)}`, label: `Result ${String(n)}` }
})
}
/** The component asks and never fetches: loadMore is called once per page as the open list nears its end, and the
* page holds what has arrived. Four pages of fifty, each after a short wait. */
export default function Demo() {
const [items, setItems] = useState(() => pageOf(0))
const [asked, setAsked] = useState(0)
const [loading, setLoading] = useState(false)
const pages = items.length / PAGE
const loadMore = useCallback(() => {
setAsked((n) => n + 1)
setLoading(true)
return new Promise<void>((resolve) => {
setTimeout(() => {
setItems((current) => [...current, ...pageOf(current.length / PAGE)])
setLoading(false)
resolve()
}, 300)
})
}, [])
return (
<div className="grid justify-items-start gap-1">
<label htmlFor="combobox-load-more" className="text-sm font-medium text-body">Search results</label>
<Combobox
id="combobox-load-more"
items={items}
hasMore={pages < LAST}
loadMore={loadMore}
status={loading ? 'loading-more' : 'idle'}
/>
<p data-testid="asked" className="text-xs text-muted">{`Pages asked for: ${String(asked)}, loaded: ${String(pages)}`}</p>
</div>
)
}
status
status.tsx
import { Combobox } from 'topclassui/combobox'
const FIRST_PAGE = [
{ value: 'a', label: 'Aleppo' },
{ value: 'b', label: 'Beirut' },
{ value: 'c', label: 'Cairo' },
]
/** loading: a spinner in place of the caret, and the list says Loading until the first options arrive. loading-more:
* the options so far, and a line under them. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-3">
<Combobox aria-label="City, loading" status="loading" items={[]} />
<Combobox aria-label="City, loading more" status="loading-more" items={FIRST_PAGE} />
</div>
)
}
ten-thousand
ten-thousand.tsx
import { Combobox } from 'topclassui/combobox'
/** Ten thousand options, built once at module scope so the array is stable between renders. */
const OPTIONS = Array.from({ length: 10_000 }, (_, index) => ({
value: String(index + 1),
label: `Option ${String(index + 1)}`,
}))
/** Windowed: about twenty rows are mounted whatever the scroll, and the highlight still reaches every one of them. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-1">
<label htmlFor="combobox-ten-thousand" className="text-sm font-medium text-body">Ten thousand options</label>
<Combobox id="combobox-ten-thousand" name="option" items={OPTIONS} />
</div>
)
}
long-content
long-content.tsx
import { Combobox } from 'topclassui/combobox'
const WORD = 'Supercalifragilistic'.repeat(15)
const LONG = [
{ value: 'word', label: WORD },
{ value: 'sentence', label: 'A label long enough to run past the width of the input and end in an ellipsis' },
{ value: 'short', label: 'Short' },
]
/** One 300-character word chosen: the input scrolls its text, each row ends in an ellipsis, and nothing widens. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-3">
<Combobox aria-label="Long, own width" items={LONG} defaultValue="word" />
<Combobox aria-label="Long, full width" fullWidth items={LONG} defaultValue="sentence" />
</div>
)
}
بالعربية، من اليمين إلى اليسار
rtl.tsx
import { TopClassProvider } from 'topclassui/provider'
import { Combobox } from 'topclassui/combobox'
const CITIES = [
{ value: 'amman', label: 'عمّان' },
{ value: 'irbid', label: 'إربد' },
{ value: 'zarqa', label: 'الزرقاء' },
{ value: 'aqaba', label: 'العقبة' },
{ value: 'madaba', label: 'مادبا' },
]
/** Arabic, inside TopClassProvider as the root layout would mount it. The caret sits at the left, the list opens at
* the right edge of the input, and the tick ends each row at its left. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="grid justify-items-start gap-1">
<label htmlFor="combobox-rtl" className="text-sm font-medium text-body">المدينة</label>
<Combobox id="combobox-rtl" name="city" items={CITIES} defaultValue="amman" />
</div>
</TopClassProvider>
)
}
الخصائص
لوحة المفاتيح وإمكانية الوصول
- الاسم مطلوب:
idلتسمية<label htmlFor>، أوaria-label، أوaria-labelledby. الحقل هو<input role="combobox">. - يبقى التركيز في الحقل بينما يتحرك التمييز في القائمة، كما في نمط القائمة القابلة للكتابة من ARIA Authoring Practices.
- مفتاح Escape على الحقل المغلق يمسح القيمة، فيرسل النموذج بعدها قيمة فارغة.
من اليمين إلى اليسار
يقع السهم وعلامة الاختيار على اليسار، وbottom-start تحاذي القائمة مع الحافة اليمنى للحقل. ArrowLeft وArrowRight يحرّكان المؤشر داخل النص، في العربية أيضًا.
الخادم والمتصفح
مكوّن متصفح: يُعرض على الخادم ثم يُرطَّب في المتصفح.
الملفات: index.tsx, client.tsx.
على الشارة: client - الحجم لم يُقَس بعد.