نوارابزار صفحه (PageToolbar)
جزء سطح پایین ردیف فیلتر و اقدامی که ListPage میسازد — فیلترها در ابتدای خط، اقدامها در انتهای خط، یک اندازه و عرضهای ذاتی
معرفی
PageToolbar ردیف فیلتر و اقدامی است که قالب ListPage (و DashboardPage) از propهای
خودش میسازد: جستوجو و فیلترها در ابتدای خط (راست)، اقدامهای صفحه در انتهای خط (چپ)، ردیف دوم برای فیلترهای فعال، و جدول یا فهرستی که فیلتر میشود زیر آن. همهٔ قاعدههای
این ردیف را خودش اجرا میکند، پس صفحه هیچکدام را نمینویسد:
- یک اندازه برای همهٔ کنترلها (
sm، 30 پیکسل؛ فیلدها هم که پیشفرضشانmdاست)؛ - کنترلهایی که بهاندازهٔ محتوایشاناند و در ردیف کش نمیآیند؛
- جستوجو با عرض نامدار؛
- فاصلهٔ 8 پیکسل بین کنترلها و 16 پیکسل تا جدول (توکنهای هندسهٔ صفحه)؛
- شکستن مرتب روی صفحهٔ باریک.
PageToolbar روی FilterBar ساخته شده است. در صفحهٔ فهرست آن را خودتان رندر نکنید:
ListPage میسازدش؛ مستقیم فقط درون یک CustomPage. FilterBar جزء سطح پایینتری است برای ردیف فیلتری که نوارابزار
صفحه نیست.
در صفحهٔ فهرست: ListPage
صفحهٔ فهرست ListPage است، که این نوارابزار را از search، filters، primaryAction
و secondaryActions خودش میسازد. PageToolbar را مستقیم فقط درون CustomPage به
کار ببرید.
چه زمانی استفاده کنیم:
- درون محتوای یک
CustomPage(صفحهای که در هیچ قالبی نمیگنجد و DS-GAP دارد): جستوجو، فیلترها و اقدامهای بالای محتوایی که فیلتر میشود (یک نقشه، یک خط زمانی)، با آن محتوا بهعنوان فرزند نوارابزار. - صفحهٔ فهرستی با جستوجو یا فیلتر (منشنها، منابع، هشدارها، کاربران) نه: آن
ListPageاست.
چه زمانی استفاده نکنیم:
- صفحهٔ فهرست، با یا بدون جستوجو و فیلتر:
ListPage(قالب اقدام اصلی را خودش در نوارابزار یا سرِ صفحه میگذارد). - ردیف فیلتر داخل یک کارت:
FilterBarباControlSizeProvider. فیلترهای زیاد صفحه: سایدبار فیلتر (CustomPage filterPanel). - فرم: فیلدهای فرم اندازه و چیدمان خودشان را دارند.
استفاده
PageToolbar فقط از مسیر جدای خودش وارد میشود و فقط درون محتوای یک CustomPage به کار میرود — صفحهای که در هیچ
قالبی جا نمیشود، اینجا نقشهٔ گفتوگوها به تفکیک استان. صفحهٔ فهرست این را نمینویسد: ListPage نوارابزارش را از
search، filters و اقدامهایش میسازد.
'use client'
import * as React from 'react'
import { Button, DataTableFacetedFilter, PageSection, SearchInput } from '@partodata/ui'
import { Icons } from '@partodata/ui/icons'
import { PageToolbar } from '@partodata/ui/page-toolbar'
import { CustomPage } from '@partodata/ui/templates'
type Platform = 'instagram' | 'telegram'
const PLATFORMS: { value: Platform; label: string }[] = [
{ value: 'instagram', label: 'اینستاگرام' },
{ value: 'telegram', label: 'تلگرام' },
]
// The map the toolbar filters (the product's own component).
declare function ConversationMap(props: { q: string; platforms: Platform[] }): React.ReactElement
declare function createAlert(): void
export function ConversationMapScreen() {
const [q, setQ] = React.useState('')
const [platforms, setPlatforms] = React.useState<Platform[]>([])
const filtered = q !== '' || platforms.length > 0
const clear = () => {
setQ('')
setPlatforms([])
}
return (
<CustomPage dsGap="DS-GAP-21: نقشهٔ گفتوگوها به تفکیک استان" title="نقشهٔ گفتوگوها">
<PageSection>
<PageToolbar
search={
<SearchInput
placeholder="جستوجو در گفتوگوها"
aria-label="جستوجو در گفتوگوها"
value={q}
onChange={(e) => setQ(e.target.value)}
onClear={() => setQ('')}
/>
}
filters={
<DataTableFacetedFilter
title="پلتفرم"
options={PLATFORMS}
selected={platforms}
onSelectedChange={setPlatforms}
/>
}
// PageToolbar's own API: the clear button shows while a filter is active (ListPage takes `filtered` instead).
onClearFilters={filtered ? clear : undefined}
primaryAction={
<Button onClick={createAlert} iconStart={<Icons.plus />}>
ساخت هشدار
</Button>
}
>
<ConversationMap q={q} platforms={platforms} />
</PageToolbar>
</PageSection>
</CustomPage>
)
}کنترلهای داخل search، filters و actions را بدون size و بدون کلاس عرض بنویسید: اندازه و عرض را
نوارابزار تعیین میکند. کنترلها میتوانند از @partodata/ui یا از مسیرهای جدا بیایند؛ نوارابزار به هر دو
میرسد.
حالتها و انواع
جای اقدام اصلی
| صفحه | اقدام اصلی کجاست |
|---|---|
صفحهای با PageToolbar (جستوجو یا فیلتر) | primaryAction در PageToolbar |
صفحهای بدون PageToolbar (بی جستوجو و فیلتر) | primaryAction در PageHeader |
در هر صفحه فقط یک اقدام اصلی هست و یک جا برای آن: primaryAction، یک Button بدون variant که نوارابزار همیشه
آخرِ خط (چپ در RTL) رندر میکند. اقدامهای دیگر در actions میآیند و ثانویاند: دکمهٔ بدون variant در
actions، مثل همهجا، همان default خنثی است، پس نوارابزار دو دکمهٔ اصلی پیدا نمیکند و اقدام اصلی هیچوقت اول
نمینشیند. برای ظاهر دیگر variant را صریح بنویسید (outline، ghost)؛ variant="primary" هیچوقت در actions نمیآید. در محیط
توسعه، دکمهٔ اصلی دوم (مثلاً variant="primary" در actions) یا اقدام اصلی در PageHeader یا ردیف breadcrumb همان
صفحه (درون PageContainer صفحه یا درست بالای آن) هشدار میدهد، و قانون ESLint parto/page-primary-action همینها را در کد نشان میدهد. اقدامهای بیشتر از سه تا
را در یک DropdownMenu با دکمهٔ فقطآیکون جمع کنید.
اقدامی که کاربر اجازهاش را ندارد پنهان نمیشود: همانجا غیرفعال با دلیلش میماند، با
GatedAction دور Button (<GatedAction allowed={canCreate} reason="…">)؛ نه
canCreate && <Button/>. false و null هیچ جایی در خط نمیگیرند؛ دکمهٔ اصلی را به actions نبرید، آنجا ثانوی
میشود.
دکمهٔ نمایش ستونها (DataTableColumnVisibilityToggle) و دکمهٔ خروجی (DataTableExportButton) اقدام ثانویاند:
همیشه در actions، نه در filters.
<PageToolbar
search={
<SearchInput
placeholder="جستوجو در گفتوگوها"
aria-label="جستوجو در گفتوگوها"
value={q}
onChange={(e) => setQ(e.target.value)}
onClear={() => setQ('')}
/>
}
actions={
<Button variant="default" onClick={shareMap}>
اشتراکگذاری
</Button>
}
primaryAction={
<Button onClick={createAlert} iconStart={<Icons.plus />}>
ساخت هشدار
</Button>
}
>
<ConversationMap q={q} platforms={platforms} />
</PageToolbar>عرض جستوجو
searchWidth یکی از سه عرض نامدار را انتخاب میکند: narrow (192)، default (256) یا wide (384 پیکسل).
عدد دلخواه نمیپذیرد. عرض جستوجو فقط از searchWidth میآید: کنترلی که در search میگذارید (SearchInput،
یا Autocomplete برای جستوجو با پیشنهاد) جای جستوجو را پر میکند، و کلاس عرض روی آن نمیتواند پهنترش کند.
فیلترهای فعال و پاککردن
activeFilters: چیپهای فیلتر فعال (FilterChip) در ردیف دوم، فقط برای فیلتری که ماشهٔ خودش را در همین نوار ندارد (کلیک روی نمودار). وقتی خالی باشد ردیف رندر نمیشود. چیپهای سایدبار فیلتر هرگز اینجا نمیآیند.onClearFilters: دکمهٔ «پاک کردن فیلترها» را در انتهای فیلترها نشان میدهد؛ وقتی chip فعال هست، در انتهای ردیف chipها (تا به خطی جدا بین کنترلها و chipها نشکند). درPageToolbarآن را فقط وقتی فیلتری فعال است بدهید:onClearFilters={filtered ? clear : undefined}. (ListPageاین را خودش تصمیم میگیرد: آنجاfilteredو همیشهonClearFilters={clear}؛ شکل بالا درListPageخطای نوع است.)search،filters،actionsوactiveFiltersمقدارfalseرا مثل خالی میگیرند، پس الگوی{cond && <X />}جای خالی نمیگذارد.
محتوای زیر نوارابزار
محتوایی که نوارابزار فیلترش میکند فرزند PageToolbar است و 16 پیکسل زیر کنترلها قرار میگیرد
(--layout-toolbar-gap). اندازهٔ نوارابزار به آن نمیرسد.
صفحهٔ باریک
فیلترها در فضای خودشان به چند خط میشکنند و اقدامها در خط اول و در انتهای خط میمانند؛ پس فعال شدن یک فیلتر و آمدن دکمهٔ پاککردن، اقدام اصلی را جابهجا نمیکند. فقط وقتی حتی جستوجو کنار اقدامها جا نشود (صفحهٔ موبایل)، اقدامها به خط بعد و انتهای آن میروند. جستوجو هرگز از عرض صفحه پهنتر نمیشود.
راهنمای استفاده
بکنید
- صفحهٔ فهرست همیشه
ListPageاست و نوارابزارش را ازsearchوfiltersخودش میسازد؛PageToolbarرا فقط درون محتوای یکCustomPageبگذارید، بهعنوان اولین بلوک بخش و با محتوایی که فیلتر میکند بهعنوان فرزندش. - جستوجو را در
search، فیلترها را به ترتیب اهمیت درfilters، اقدام اصلی را درprimaryActionو بقیهٔ اقدامها را باvariant="default"درactionsبنویسید. - برای فیلترهای شمارشی
DataTableFacetedFilterو برای بازهٔ زمانیDateRangePickerرا به کار ببرید.
نکنید
- به کنترلهای داخل نوارابزار
size،w-*یاautoWidthندهید؛ نوارابزار آنها را همقد و همشکل میکند. - اقدام اصلی صفحه را هم در
PageHeaderو هم درPageToolbarنگذارید، و آن را بهجایprimaryActionدرactionsننویسید (آنجا ثانوی رندر میشود). - نوارابزار را داخل
Cardنگذارید و خودتان باdivوflexیا باFilterBarنوارابزار نسازید. - کنترلها را فرزند
PageToolbarنکنید؛ فرزند فقط محتوایی است که فیلتر میشود. - اقدامی را که کاربر اجازهاش را ندارد پنهان نکنید:
GatedAction.
Props
PageToolbar
className و style نمیپذیرد: چیدمان مال خود نوارابزار است، و اگر از کد بدون نوع (JavaScript ساده یا گسترش یک
شیء any) هم برسند کنار گذاشته میشوند. ویژگیهای دیگر div (id، aria-*، data-*) به عنصر ریشه میرسند.
دسترسیپذیری
- ترتیب DOM همان ترتیب خواندن است: جستوجو، فیلترها، پاککردن، اقدامهای ثانوی، اقدام اصلی، ردیف فیلترهای فعال و در آخر جدول؛ ترتیب Tab هم همین است.
- وقتی دکمهٔ پاککردن یا دکمهٔ حذف یک چیپ با صفحهکلید (یا فناوری کمکی) فعال میشود و از صفحه میرود، تمرکز گم نمیشود: به چیپ بعدی، اگر نبود به چیپ قبلی، و اگر ردیف چیپها رفت به جستوجو (یا اولین فیلتر) میرود. با کلیک ماوس یا لمس تمرکز جابهجا نمیشود تا روی موبایل صفحهکلید باز نشود.
- ردیف فیلترهای فعال
role="group"و برچسب «فیلترهای فعال» دارد. - دکمهٔ فقطآیکون در
actionsمربعی به ارتفاع ردیف است و بهaria-labelنیاز دارد. SearchInputنقشsearchو برچسب قابل خواندن دارد؛aria-labelیاplaceholderمعنادار بدهید.
کامپوننتهای مرتبط
FilterBar— جزء سطح پایینی کهPageToolbarروی آن ساخته شده است؛ برای ردیف فیلتر داخل پنل یا کارت، نه برای نوارابزار صفحه.ControlSizeProvider— اگر ردیف کنترلی غیر از نوارابزار صفحه دارید و میخواهید یک اندازه داشته باشد.DataTable— جدول صفحهٔ فهرست، فرزندListPage(نهPageToolbar)؛DataTableFacetedFilterفیلتر شمارشی همین نوارابزار است.PageHeader— سری که قالبها میسازند؛ صفحهای بی جستوجو و فیلتر اقدام اصلیاش را بهprimaryActionقالبش میدهد.- بلاک جستوجوی پست و صفحهٔ «منشنها» در قالب شروع — صفحههای کامل با همین
نوارابزار؛ صفحهٔ «منشنها» یک
ListPageاست که نوارابزار را خودش میسازد.
سربرگ صفحه (PageHeader)
جزء سطح پایینی که سرِ صفحهٔ هر قالب صفحه را میسازد — عنوان (تنها h1 صفحه)، توضیح، راه بازگشت، اقدام اصلی و اقدامهای دیگر؛ صفحهٔ محصول آن را نمینویسد، قالبش رندرش میکند.
حالت بلوک (PageState)
حالتهای بارگذاری، خطا و خالیِ یک بلوک از صفحه — اسکلتی همشکل محتوا، خطا با «تلاش مجدد» و حالت خالی، همه بهجای بلوک