نافذة منبثقة
لوحة مثبّتة بجانب مشغّلها لرابط أو حقل أو إجراء. تنقلب وتتقلّص إلى المساحة المتاحة بدلًا من أن تفيض.
client - الحجم لم يُقَس بعد
نافذة منبثقة
client - الحجم لم يُقَس بعدلوحة مثبّتة بجانب مشغّلها لرابط أو حقل أو إجراء. تنقلب وتتقلّص إلى المساحة المتاحة بدلًا من أن تفيض.
جرّبه
placement
delay
closeDelay
الشيفرة
popover.tsx
import { Button } from 'topclassui/button'
import { PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger } from 'topclassui/popover'
<PopoverRoot>
<PopoverTrigger openOnHover render={<Button variant="outline">Share</Button>} />
<PopoverPopup>
<PopoverTitle>Share this page</PopoverTitle>
<PopoverDescription>Anyone with the link can read it. Only your team can edit.</PopoverDescription>
<Button size="sm" className="self-start">Copy link</Button>
</PopoverPopup>
</PopoverRoot>التلميح نص فقط: أبناؤه مكتوبون كنص. كل ما فيه رابط أو عنصر تحكم هو نافذة منبثقة.
كل الحالات، بدون تمرير الفأرة
5 حالاتأو استورده من الحزمة
topclassui/popover
import { PopoverRoot, PopoverClose, PopoverDescription, PopoverPopup, PopoverTitle, PopoverTrigger } from 'topclassui/popover'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
- ستة أجزاء بأسماء مسطّحة ولا يوجد
Popoverوحده:PopoverRootوPopoverTriggerوPopoverPopupوPopoverTitleوPopoverDescriptionوPopoverClose. placementعلىPopoverPopup: اثنتا عشرة قيمة، وbottomافتراضيًا. تفتح على الجهة الأخرى حين لا تتسع جهتها، وتبقى على بعد 8 بكسل داخل الشاشة.- عرض النافذة 280 بكسل ولا يتجاوز ارتفاعها المساحة المتاحة، وبعد ذلك تتمرّر داخلها. لعرض آخر، مرّر صنفًا.
PopoverCloseبلا محتوى هو زر الإغلاق، وأنت تحدّد مكانه.openOnHoverعلى المشغّل يتيح للمؤشر فتحها أيضًا، بعدdelay(300 مللي ثانية) وحتىcloseDelay(0)، وكلاهما محصور بين 0 و10000.- ضع
formداخلPopoverPopup، لاrender={<form />}عليها، وأعطهactionأوonSubmit.
basic
basic.tsx
import { Button } from 'topclassui/button'
import { PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger } from 'topclassui/popover'
/** The drawing: a title, a line of text and one action, under the trigger. The label sits
* inside the Button passed to render, so a server render keeps it whole. */
export default function Demo() {
return (
<PopoverRoot>
<PopoverTrigger render={<Button variant="outline">Share</Button>} />
<PopoverPopup>
<PopoverTitle>Share this page</PopoverTitle>
<PopoverDescription>Anyone with the link can read it. Only your team can edit.</PopoverDescription>
<Button size="sm" className="self-start">Copy link</Button>
</PopoverPopup>
</PopoverRoot>
)
}
with-close
with-close.tsx
import { Button } from 'topclassui/button'
import {
PopoverClose, PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger,
} from 'topclassui/popover'
/** A PopoverClose with nothing in it is the x. The caller places it: here in a row with the
* title, which wraps beside it and never runs under it. A touch screen reader has no Escape
* key, so a popover that holds more than a line of text carries one. */
export default function Demo() {
return (
<PopoverRoot>
<PopoverTrigger render={<Button variant="outline">Notifications</Button>} />
<PopoverPopup>
<div className="flex items-start justify-between gap-2">
<PopoverTitle>Notifications are paused</PopoverTitle>
<PopoverClose />
</div>
<PopoverDescription>You will get one summary tomorrow at nine instead.</PopoverDescription>
</PopoverPopup>
</PopoverRoot>
)
}
hover
hover.tsx
import { Button } from 'topclassui/button'
import { PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger } from 'topclassui/popover'
/** openOnHover: the pointer opens it after delay and it stays closeDelay after the pointer
* leaves, long enough to reach the link inside. A press and the keyboard still open it, and
* a tooltip could not hold this link: anything with a link or a control is a Popover. */
export default function Demo() {
return (
<PopoverRoot>
<PopoverTrigger openOnHover delay={100} closeDelay={200} render={<Button variant="ghost">Billing details</Button>} />
<PopoverPopup placement="bottom-start">
<PopoverTitle>How billing works</PopoverTitle>
<PopoverDescription>Only the seats in use are billed.</PopoverDescription>
<Button variant="link" size="sm" className="self-start" render={<a href="/docs/billing" />}>Read the guide</Button>
</PopoverPopup>
</PopoverRoot>
)
}
with-form
with-form.tsx
'use client'
import { useState, type SubmitEvent } from 'react'
import { Button } from 'topclassui/button'
import { Input } from 'topclassui/input'
import { PopoverClose, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger } from 'topclassui/popover'
/** A field in a popover. The form goes inside the popup, never on it: role="dialog" on a form
* fails axe. A form needs an action or an onSubmit, or the browser reloads the page: here
* onSubmit closes the popover. autoFocus puts the first focus in the field. */
export default function Demo() {
const [open, setOpen] = useState(false)
const save = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault()
setOpen(false)
}
return (
<PopoverRoot open={open} onOpenChange={setOpen}>
<PopoverTrigger render={<Button variant="outline">Rename</Button>} />
<PopoverPopup>
<PopoverTitle>Rename this project</PopoverTitle>
<form className="grid gap-3" onSubmit={save}>
<div className="grid gap-1">
<label htmlFor="demo-popover-name" className="font-medium text-body">Name</label>
<Input id="demo-popover-name" name="name" defaultValue="Website redesign" required fullWidth autoFocus />
</div>
<div className="flex flex-wrap justify-end gap-2">
<PopoverClose render={<Button variant="outline" size="sm">Cancel</Button>} />
<Button type="submit" size="sm">Save</Button>
</div>
</form>
</PopoverPopup>
</PopoverRoot>
)
}
modal
modal.tsx
import { Button } from 'topclassui/button'
import {
PopoverClose, PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger,
} from 'topclassui/popover'
/** modal: the page behind takes no press and does not scroll, and no scrim is drawn. Tab stays
* in the popup because a PopoverClose is inside it: without one Base UI does not hold the focus. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<PopoverRoot modal>
<PopoverTrigger render={<Button variant="outline">Delete draft</Button>} />
<PopoverPopup>
<div className="flex items-start justify-between gap-2">
<PopoverTitle>Delete this draft?</PopoverTitle>
<PopoverClose />
</div>
<PopoverDescription>The draft and its comments are removed. This cannot be undone.</PopoverDescription>
<div className="flex flex-wrap justify-end gap-2">
<PopoverClose render={<Button variant="outline" size="sm">Keep it</Button>} />
<PopoverClose render={<Button tone="danger" size="sm">Delete</Button>} />
</div>
</PopoverPopup>
</PopoverRoot>
<Button variant="ghost">Behind the panel</Button>
</div>
)
}
placements
placements.tsx
import { Button } from 'topclassui/button'
import { PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger } from 'topclassui/popover'
import type { PopoverPlacement } from 'topclassui/popover'
const PLACES: PopoverPlacement[] = [
'top-start', 'top', 'top-end',
'start-top', 'start', 'start-bottom',
'end-top', 'end', 'end-bottom',
'bottom-start', 'bottom', 'bottom-end',
]
/** All twelve placements, one outline Button each. The label names the placement and the
* popup is titled by the same word. start and end are the inline sides, so they mirror. */
export default function Demo() {
return (
<div className="grid max-w-[31.5rem] grid-cols-[repeat(auto-fill,minmax(min(100%,8rem),1fr))] gap-3 p-4 sm:p-12">
{PLACES.map((place) => (
<PopoverRoot key={place}>
<PopoverTrigger render={<Button variant="outline" size="sm">{place}</Button>} />
<PopoverPopup placement={place}>
<PopoverTitle>{place}</PopoverTitle>
<PopoverDescription>It flips to the other side where this one has no room.</PopoverDescription>
</PopoverPopup>
</PopoverRoot>
))}
</div>
)
}
long-content
long-content.tsx
import { Button } from 'topclassui/button'
import { PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger } from 'topclassui/popover'
const LINES = Array.from({ length: 60 }, (_, index) => index + 1)
/** Sixty lines, taller than any screen. The popup shrinks to the room beside its trigger and
* scrolls inside itself (R-10.5-1). While it scrolls it is a tab stop, so the keyboard reaches
* the last line of a text that holds no control. */
export default function Demo() {
return (
<PopoverRoot>
<PopoverTrigger render={<Button variant="outline">Change log</Button>} />
<PopoverPopup>
<PopoverTitle>Change log</PopoverTitle>
<PopoverDescription>Sixty changes. Scroll to read them all.</PopoverDescription>
<ol className="grid gap-1">
{LINES.map((line) => <li key={line}>Change {line}: a fix that shipped this week.</li>)}
</ol>
</PopoverPopup>
</PopoverRoot>
)
}
بالعربية، من اليمين إلى اليسار
rtl.tsx
import { TopClassProvider } from 'topclassui/provider'
import { Button } from 'topclassui/button'
import {
PopoverClose, PopoverDescription, PopoverPopup, PopoverRoot, PopoverTitle, PopoverTrigger,
} from 'topclassui/popover'
/** Arabic, inside TopClassProvider as the root layout would mount it. The popup is portaled
* into the body, so it takes its direction and its language from the provider: start opens on
* the right, end on the left, bottom-start lines up the right edges, 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">
<PopoverRoot>
<PopoverTrigger render={<Button variant="outline">مشاركة</Button>} />
<PopoverPopup placement="start">
<div className="flex items-start justify-between gap-2">
<PopoverTitle>مشاركة هذه الصفحة</PopoverTitle>
<PopoverClose />
</div>
<PopoverDescription>يمكن لأي شخص لديه الرابط قراءتها. فريقك فقط يستطيع التعديل.</PopoverDescription>
</PopoverPopup>
</PopoverRoot>
<PopoverRoot>
<PopoverTrigger render={<Button variant="outline">التفاصيل</Button>} />
<PopoverPopup placement="end">
<PopoverTitle>كيف تعمل الفوترة</PopoverTitle>
<PopoverDescription>تُحتسب المقاعد المستخدمة فقط.</PopoverDescription>
</PopoverPopup>
</PopoverRoot>
<PopoverRoot>
<PopoverTrigger render={<Button variant="outline">الإعدادات</Button>} />
<PopoverPopup placement="bottom-start">
<PopoverTitle>إعدادات الإشعارات</PopoverTitle>
<PopoverDescription>يصلك ملخص واحد كل صباح.</PopoverDescription>
</PopoverPopup>
</PopoverRoot>
</div>
</TopClassProvider>
)
}
الخصائص
PopoverRoot
PopoverClose
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
PopoverDescription
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
PopoverPopup
PopoverTitle
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
PopoverTrigger
لوحة المفاتيح وإمكانية الوصول
PopoverTitleيسمّي النافذة وPopoverDescriptionيصفها. إن لم يكن لها عنوان، مرّرaria-labelإلىPopoverPopup.- قارئ الشاشة على شاشة لمس لا يملك مفتاح Escape: أعطِ النافذة التي تحمل أكثر من سطر
PopoverClose. - ما دامت النافذة تتمرّر فهي محطة Tab، فتصل لوحة المفاتيح إلى نص طويل لا عنصر تحكم فيه.
من اليمين إلى اليسار
start تفتح على اليمين وend على اليسار، وbottom-start تحاذي الحواف اليمنى. تأخذ النافذة اتجاهها من أقرب TopClassProvider.
الخادم والمتصفح
مكوّن متصفح: يُعرض على الخادم ثم يُرطَّب في المتصفح.
الملفات: index.tsx, client.tsx.
على الشارة: client - الحجم لم يُقَس بعد.