درج
مربع حوار ينزلق من حافة منطقية، فينعكس في العربية دون أي تعديل في الشيفرة. يُغلق بالسحب أو بزر الإغلاق.
درج
client - الحجم لم يُقَس بعدمربع حوار ينزلق من حافة منطقية، فينعكس في العربية دون أي تعديل في الشيفرة. يُغلق بالسحب أو بزر الإغلاق.
جرّبه
الشيفرة
import { Button } from 'topclassui/button'
import {
<DrawerRoot>
<DrawerTrigger render={<Button variant="outline">Filters</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Filters</DrawerTitle>
<DrawerDescription>Narrow the list of projects.</DrawerDescription>
</DrawerHeader>
<DrawerBody className="grid content-start gap-2.5">
<p>Status</p>
<p>Owner</p>
<p>Updated</p>
</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="outline">Reset</Button>} />
<DrawerClose render={<Button>Apply</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>placement منطقي: start يفتح من اليسار في الإنجليزية ومن اليمين في العربية. لا left ولا right أبدًا.
كل الحالات، بدون تمرير الفأرة
5 حالاتأو استورده من الحزمة
import { DrawerRoot, DrawerBody, DrawerClose, DrawerDescription, DrawerFooter, DrawerHeader, DrawerPopup, DrawerTitle, DrawerTrigger } from 'topclassui/drawer'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
- للوحة تنزلق من حافة الشاشة: عوامل التصفية، التنقّل، قائمة للاختيار منها على الهاتف.
placementعلىDrawerRoot:end(الافتراضي) أوstartأوtopأوbottom.sizeعلىDrawerRootهو أقصى عرض يبلغه درجstartأوend: 256 أو 288 أو 320 أو 384 أو 480 بكسل. درجا الأعلى والأسفل يمتدان بعرض الشاشة ويبلغان تسعة أعشار ارتفاعها.- السحب نحو حافة الدرج يغلقه متى تجاوز نصفه. الإصبع يبدأ من أي مكان في الدرج، والفأرة تبدأ من الرأس.
DrawerBodyيتمرّر ويبقى الرأس والتذييل في مكانهما. ابدأ بـDrawerHeader، فهو يُبعد العنوان عن زر الإغلاق.- على الهاتف يترك الدرج مساحة للنتوء ولمؤشر الشاشة الرئيسية، إن كان وسم viewport يحمل
viewport-fit=cover.
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerDescription, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle,
DrawerTrigger,
} from 'topclassui/drawer'
/** The default: a sheet at the inline end, with a header, a body and a footer. It always ends
* with the x. Each label sits inside the Button passed to render, so a server render keeps it whole. */
export default function Demo() {
return (
<DrawerRoot>
<DrawerTrigger render={<Button variant="outline">Filters</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Filters</DrawerTitle>
<DrawerDescription>Narrow the list of projects.</DrawerDescription>
</DrawerHeader>
<DrawerBody className="grid content-start gap-2.5">
<p>Status</p>
<p>Owner</p>
<p>Updated</p>
</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="outline">Reset</Button>} />
<DrawerClose render={<Button>Apply</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
)
}
import { Button } from 'topclassui/button'
import { DrawerBody, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle, DrawerTrigger } from 'topclassui/drawer'
const LINKS = [['Documentation', '/docs'], ['Components', '/components'], ['Changelog', '/changelog']] as const
/** placement="start": the navigation of a phone. It opens from the left in LTR and from the
* right in RTL, and a swipe toward that edge closes it. Each link is a real anchor. */
export default function Demo() {
return (
<DrawerRoot placement="start">
<DrawerTrigger render={<Button variant="ghost">Menu</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Browse</DrawerTitle>
</DrawerHeader>
<DrawerBody className="grid content-start gap-1">
{LINKS.map(([label, href]) => (
<Button key={href} variant="ghost" tone="neutral" fullWidth className="justify-start" render={<a href={href} />}>
{label}
</Button>
))}
</DrawerBody>
</DrawerPopup>
</DrawerRoot>
)
}
import { Button } from 'topclassui/button'
import { DrawerBody, DrawerClose, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle, DrawerTrigger } from 'topclassui/drawer'
const COUNTRIES = [
'Jordan', 'Egypt', 'Morocco', 'Tunisia', 'Algeria', 'Lebanon', 'Iraq', 'Kuwait', 'Qatar', 'Bahrain', 'Oman', 'Yemen',
'Sudan', 'Libya', 'Palestine', 'Syria', 'Mauritania', 'Somalia', 'Djibouti', 'Comoros', 'Turkey', 'Cyprus', 'Greece',
'Italy',
]
/** placement="bottom": the sheet of a phone. It starts with a grabber, grows to nine tenths of
* the screen and no further, and its list scrolls inside it. A swipe down closes it; so does the x. */
export default function Demo() {
return (
<DrawerRoot placement="bottom">
<DrawerTrigger render={<Button variant="outline">Country</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Choose a country</DrawerTitle>
</DrawerHeader>
<DrawerBody className="grid content-start gap-1">
{COUNTRIES.map((country) => (
<DrawerClose
key={country}
render={<Button variant="ghost" tone="neutral" fullWidth className="justify-start">{country}</Button>}
/>
))}
</DrawerBody>
</DrawerPopup>
</DrawerRoot>
)
}
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerDescription, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle,
DrawerTrigger,
} from 'topclassui/drawer'
/** placement="top": a sheet that comes down from the top edge. A swipe up closes it. */
export default function Demo() {
return (
<DrawerRoot placement="top">
<DrawerTrigger render={<Button variant="soft">What is new</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Version 2.4</DrawerTitle>
<DrawerDescription>Released this week.</DrawerDescription>
</DrawerHeader>
<DrawerBody>Tables can now pin their first column, and every menu takes a link.</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button>Got it</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
)
}
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle, DrawerTrigger,
} from 'topclassui/drawer'
import type { DrawerRootProps } from 'topclassui/drawer'
const WIDTHS: Array<[NonNullable<DrawerRootProps['size']>, string]> = [
['xs', '256 px'],
['sm', '288 px'],
['md', '320 px'],
['lg', '384 px'],
['xl', '480 px'],
]
/** The five widths of a sheet at the start or the end, and custom, which sets none: its className
* gives the sheet one. Each is the widest it grows; on a narrower screen every one fills it. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
{WIDTHS.map(([size, width]) => (
<DrawerRoot key={size} size={size}>
<DrawerTrigger render={<Button variant="outline">{size}</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Size {size}</DrawerTitle>
</DrawerHeader>
<DrawerBody>This sheet grows to {width} and no wider.</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button>Done</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
))}
<DrawerRoot size="custom">
<DrawerTrigger render={<Button variant="outline">custom</Button>} />
<DrawerPopup className="max-w-xl">
<DrawerHeader>
<DrawerTitle>Size custom</DrawerTitle>
</DrawerHeader>
<DrawerBody>This sheet takes its width from the className it was given.</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button>Done</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
</div>
)
}
import { Button } from 'topclassui/button'
import { Input } from 'topclassui/input'
import {
DrawerBody, DrawerClose, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle, DrawerTrigger,
} from 'topclassui/drawer'
/** A form in a bottom sheet. One form with display contents wraps the three bars, so the submit
* button in the footer belongs to it. On a phone the keyboard covers the lower half of the screen:
* a bottom sheet scrolls the field that has the focus clear of it. */
export default function Demo() {
return (
<DrawerRoot placement="bottom">
<DrawerTrigger render={<Button variant="outline">Invite</Button>} />
<DrawerPopup>
<form className="contents">
<DrawerHeader>
<DrawerTitle>Invite a teammate</DrawerTitle>
</DrawerHeader>
<DrawerBody className="grid content-start gap-1">
<label htmlFor="demo-invite-name" className="font-medium">Name</label>
<Input id="demo-invite-name" name="name" autoComplete="name" required fullWidth />
<label htmlFor="demo-invite-email" className="mt-3 font-medium">Email</label>
<Input id="demo-invite-email" name="email" type="email" autoComplete="email" required fullWidth />
</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="outline">Cancel</Button>} />
<Button type="submit">Send</Button>
</DrawerFooter>
</form>
</DrawerPopup>
</DrawerRoot>
)
}
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerDescription, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle,
DrawerTrigger,
} from 'topclassui/drawer'
const ENTRIES = Array.from({ length: 60 }, (_, index) => index + 1)
/** Sixty rows: the body scrolls inside the sheet, and the header and the footer keep their
* places above and below it (R-10.5-1). At its end the page behind does not take over the scroll. */
export default function Demo() {
return (
<DrawerRoot>
<DrawerTrigger render={<Button variant="outline">Activity</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Activity</DrawerTitle>
<DrawerDescription>Sixty entries. Scroll to read them all.</DrawerDescription>
</DrawerHeader>
<DrawerBody className="grid content-start gap-3">
{ENTRIES.map((entry) => (
<p key={entry}>Entry {entry}. A file was changed in the shared folder.</p>
))}
</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="outline">Mark all as read</Button>} />
<DrawerClose render={<Button>Done</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
)
}
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerDescription, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle,
DrawerTrigger,
} from 'topclassui/drawer'
const TITLE = 'Move the quarterly report, its eleven attachments and every comment thread to the shared archive of the finance team'
const WORD = 'a'.repeat(300)
const FILE = 'quarterly-report-final-v2-reviewed-by-legal-and-finance-and-signed-off-by-the-board.pdf'
/** Hostile content: a title of many lines, a word of 300 characters and three long actions.
* The title wraps clear of the x, the word breaks inside the body, and a long action wraps onto
* two lines. Where the bars leave the body no room, the sheet scrolls as one. */
export default function Demo() {
return (
<DrawerRoot>
<DrawerTrigger render={<Button variant="outline">Move to the archive</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>{TITLE}</DrawerTitle>
<DrawerDescription>{FILE}</DrawerDescription>
</DrawerHeader>
<DrawerBody>{WORD}</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="outline">Keep it where it is</Button>} />
<DrawerClose render={<Button variant="outline">Move a copy and keep the original</Button>} />
<DrawerClose render={<Button>Move everything to the archive</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
)
}
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle, DrawerTrigger,
} from 'topclassui/drawer'
/** modal={false}: no scrim, and the page beside the sheet stays in use. It scrolls, its buttons
* take a press, and a screen reader still reads it. Focus is not held: Tab past the last control,
* or a press anywhere outside, closes the sheet. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<DrawerRoot modal={false} size="xs">
<DrawerTrigger render={<Button variant="outline">Outline</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Outline</DrawerTitle>
</DrawerHeader>
<DrawerBody>The page stays live beside this panel, so you can keep working in it.</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button>Done</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
<Button variant="ghost">Behind the panel</Button>
</div>
)
}
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle, DrawerTrigger,
} from 'topclassui/drawer'
/** modal="trap-focus": no scrim, and the pointer keeps the page beside the sheet: it scrolls and
* its buttons take a press. The keyboard and a screen reader stay in the sheet: Tab goes round
* inside it and the page is hidden from them until it closes. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<DrawerRoot modal="trap-focus" size="xs">
<DrawerTrigger render={<Button variant="outline">Inspect</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Inspector</DrawerTitle>
</DrawerHeader>
<DrawerBody>Point at anything on the page beside this panel. The keyboard stays in it.</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button>Done</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
<Button variant="ghost">Behind the panel</Button>
</div>
)
}
import { TopClassProvider } from 'topclassui/provider'
import { Button } from 'topclassui/button'
import {
DrawerBody, DrawerClose, DrawerDescription, DrawerFooter, DrawerHeader, DrawerPopup, DrawerRoot, DrawerTitle,
DrawerTrigger,
} from 'topclassui/drawer'
/** Arabic, inside TopClassProvider as the root layout would mount it. The sheet is portaled into
* the body, so it takes its direction and its language from the provider: start opens from the
* right and end from the left, and each closes by a swipe toward its own edge, on an English page too. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="flex flex-wrap items-center gap-3">
<DrawerRoot placement="start">
<DrawerTrigger render={<Button variant="ghost">القائمة</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>تصفّح</DrawerTitle>
</DrawerHeader>
<DrawerBody>الوثائق والمكوّنات وسجل التغييرات.</DrawerBody>
</DrawerPopup>
</DrawerRoot>
<DrawerRoot>
<DrawerTrigger render={<Button variant="outline">التصفية</Button>} />
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>عوامل التصفية</DrawerTitle>
<DrawerDescription>حدّد المشاريع التي تريد عرضها.</DrawerDescription>
</DrawerHeader>
<DrawerBody>الحالة والمالك وتاريخ التحديث.</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="outline">إعادة ضبط</Button>} />
<DrawerClose render={<Button>تطبيق</Button>} />
</DrawerFooter>
</DrawerPopup>
</DrawerRoot>
</div>
</TopClassProvider>
)
}
الخصائص
DrawerRoot
DrawerBody
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
DrawerClose
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
DrawerDescription
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
DrawerFooter
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
DrawerHeader
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
DrawerPopup
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
DrawerTitle
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
DrawerTrigger
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
لوحة المفاتيح وإمكانية الوصول
- كل درج ينتهي بزر إغلاق لا يمكن إزالته: السحب حركة جرّ، والجرّ يحتاج طريقة ثانية (WCAG 2.5.7).
DrawerTitleيسمّي الدرج. إن لم يكن له عنوان، مرّرaria-labelإلىDrawerPopup.- ما دام
DrawerBodyيتمرّر فهو محطة Tab، فتصل لوحة المفاتيح إلى نص طويل لا عنصر تحكم فيه.
من اليمين إلى اليسار
start يفتح من اليمين وend من اليسار، ويُغلق كل منهما بالسحب نحو حافته. ركّب TopClassProvider: من دونه يتبع السحب اتجاه اليسار إلى اليمين.
الخادم والمتصفح
مكوّن متصفح: يُعرض على الخادم ثم يُرطَّب في المتصفح.
الملفات: index.tsx, client.tsx.
على الشارة: client - الحجم لم يُقَس بعد.