@blakesteve/roster 5.14.0 → 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -37
- package/dist/components/molecules/LiquidTabs/LiquidCount.d.ts +2 -2
- package/dist/components/molecules/LiquidTabs/LiquidTabPanel.cjs +1 -1
- package/dist/components/molecules/LiquidTabs/LiquidTabPanel.d.ts +10 -49
- package/dist/components/molecules/LiquidTabs/LiquidTabPanel.js +5 -28
- package/dist/components/molecules/LiquidTabs/LiquidTabs.cjs +1 -1
- package/dist/components/molecules/LiquidTabs/LiquidTabs.d.ts +13 -40
- package/dist/components/molecules/LiquidTabs/LiquidTabs.js +5 -95
- package/dist/components/molecules/LiquidTabs/liquid-count.d.ts +7 -5
- package/dist/components/molecules/LiquidTabs/liquid-tab-id.cjs +1 -1
- package/dist/components/molecules/LiquidTabs/liquid-tab-id.d.ts +4 -9
- package/dist/components/molecules/LiquidTabs/liquid-tab-id.js +3 -4
- package/dist/components/molecules/Tabs/TabPanel.cjs +1 -0
- package/dist/components/molecules/Tabs/TabPanel.d.ts +53 -0
- package/dist/components/molecules/Tabs/TabPanel.js +32 -0
- package/dist/components/molecules/Tabs/Tabs.cjs +1 -0
- package/dist/components/molecules/Tabs/Tabs.d.ts +118 -0
- package/dist/components/molecules/Tabs/Tabs.js +145 -0
- package/dist/components/molecules/Tabs/tab-id.cjs +1 -0
- package/dist/components/molecules/Tabs/tab-id.d.ts +13 -0
- package/dist/components/molecules/Tabs/tab-id.js +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/meta.json +3 -3
- package/dist/roster.cjs +1 -1
- package/dist/roster.css +1 -1
- package/dist/roster.js +116 -110
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -268,9 +268,9 @@ changes nothing.
|
|
|
268
268
|
| token | default | reshapes |
|
|
269
269
|
| ----- | ------- | -------- |
|
|
270
270
|
| `--roster-radius-sm` | `0.25rem` | `Button` `xs`, `Checkbox` `sm` and `md`, `InlineCode`, `Link`, `MultiSelect`'s chip dismiss, the dismiss and reveal buttons in `Alert`, `Toast` and `PasswordInput` |
|
|
271
|
-
| `--roster-radius-md` | `0.375rem` | `Button`, `Input`, `Textarea`, `Select`, `MultiSelect`, `Badge`, `Avatar` and its popover, `Checkbox` `lg`, `CheckboxGroup`'s panel, `Disclosure`, `Dialog`'s and `Sheet`'s close buttons, `Navbar`'s links and menu, popup panels, `Table` |
|
|
272
|
-
| `--roster-radius-lg` | `0.5rem` | `Alert`, `Toast`, `Tooltip`, `CallToAction`, `EmptyState`, `ErrorState`, `
|
|
273
|
-
| `--roster-radius-xl` | `0.75rem` | `Card`, `
|
|
271
|
+
| `--roster-radius-md` | `0.375rem` | `Button`, `Input`, `Textarea`, `Select`, `MultiSelect`, `Badge`, `Avatar` and its popover, `Checkbox` `lg`, `CheckboxGroup`'s panel, `Disclosure`, `Dialog`'s and `Sheet`'s close buttons, `Navbar`'s links and menu, popup panels, `Table`, a plain `Tabs`' focus ring |
|
|
272
|
+
| `--roster-radius-lg` | `0.5rem` | `Alert`, `Toast`, `Tooltip`, `CallToAction`, `EmptyState`, `ErrorState`, `Tabs`' liquid variant, `MatchupCard`, `Navbar`'s mobile panel, `Skeleton` blocks (unless `--roster-skeleton-radius` is set) |
|
|
273
|
+
| `--roster-radius-xl` | `0.75rem` | `Card`, the liquid `Tabs`' `pill` strip |
|
|
274
274
|
| `--roster-radius-2xl` | `1rem` | `Dialog`, the top of `Sheet` |
|
|
275
275
|
| `--roster-radius-3xl` | `1.5rem` | nothing yet; wired for the first component that uses it |
|
|
276
276
|
| `--roster-border-width` | `1px` | every 1px edge: borders, `divide-y`, the ring `Select`'s trigger and the anchored panels draw their edge with, and the rules in `Accordion` and `LabeledDivider` |
|
|
@@ -477,11 +477,12 @@ Replace the rule with the token:
|
|
|
477
477
|
:root { --roster-primary-600-ink: #10142e; }
|
|
478
478
|
```
|
|
479
479
|
|
|
480
|
-
`LiquidTabs` followed later: its active label
|
|
481
|
-
which defaults to `--roster-primary-500-ink`,
|
|
482
|
-
fill. A rule naming `rst:text-white` on a tab
|
|
483
|
-
`[role="tab"][aria-selected="true"]` still does. If
|
|
484
|
-
`--roster-lt-pill`, set `--roster-lt-text-active` to
|
|
480
|
+
`LiquidTabs` followed later (now `Tabs`' liquid variant): its active label
|
|
481
|
+
reads `--roster-lt-text-active`, which defaults to `--roster-primary-500-ink`,
|
|
482
|
+
the ink of the pill's default fill. A rule naming `rst:text-white` on a tab
|
|
483
|
+
stops matching; a rule on `[role="tab"][aria-selected="true"]` still does. If
|
|
484
|
+
you repaint the pill with `--roster-lt-pill`, set `--roster-lt-text-active` to
|
|
485
|
+
match it.
|
|
485
486
|
|
|
486
487
|
**If you pass a foreground through `className`, add the hover modifier.** Solid
|
|
487
488
|
variants now carry `hover:text-*`, and `tailwind-merge` resolves conflicts per
|
|
@@ -565,7 +566,7 @@ a click. A small tracker marks those elements: keyboard mode starts on a navigat
|
|
|
565
566
|
arrows, Escape, Enter, Space, Home, End, PageUp, PageDown) and ends on pointer
|
|
566
567
|
input, and it marks only what Chromium's own `:focus-visible` already matches,
|
|
567
568
|
so Chromium's rings are unchanged. It runs while a component that moves focus
|
|
568
|
-
is mounted (Menu, the select family, Dialog, Sheet, RadioGroup,
|
|
569
|
+
is mounted (Menu, the select family, Dialog, Sheet, RadioGroup, Tabs,
|
|
569
570
|
Slider and the rest), on one set of document listeners.
|
|
570
571
|
|
|
571
572
|
Your own components' rings come from your Tailwind, not Roster's, so they
|
|
@@ -1382,7 +1383,7 @@ What it promises:
|
|
|
1382
1383
|
sideways at 320px. Nothing animates as an item leaves.
|
|
1383
1384
|
|
|
1384
1385
|
Counts for a tab need no API: the app holds the items, so it's
|
|
1385
|
-
`items.length`, passed as a `
|
|
1386
|
+
`items.length`, passed as a `Tabs` tab's `count`. `MediaPreview` is the
|
|
1386
1387
|
thumbnail to use in an item: when the image can't render (an expired link, a
|
|
1387
1388
|
format the browser can't draw), it falls back to a link to the original in the
|
|
1388
1389
|
same footprint.
|
|
@@ -1983,17 +1984,54 @@ clicks and focus are one listener each on the row. Measured at 1,000 options
|
|
|
1983
1984
|
in Chromium: mounting takes about 30ms longer than the default mode, a key
|
|
1984
1985
|
moves focus within a millisecond, and the frame after it lands in about 7ms.
|
|
1985
1986
|
|
|
1986
|
-
###
|
|
1987
|
+
### Tabs
|
|
1987
1988
|
|
|
1988
|
-
A tab strip
|
|
1989
|
-
that wrap, Home and End, and `activation="manual"` for tabs that
|
|
1990
|
-
switch.
|
|
1989
|
+
A controlled tab strip, on the WAI tabs pattern in every variant: one tab
|
|
1990
|
+
stop, arrows that wrap, Home and End, and `activation="manual"` for tabs that
|
|
1991
|
+
are costly to switch. Every tab is `type="button"`, so a strip inside a form
|
|
1992
|
+
never submits it.
|
|
1991
1993
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
+
```tsx
|
|
1995
|
+
import { Tabs } from "@blakesteve/roster";
|
|
1996
|
+
|
|
1997
|
+
<Tabs aria-label="Project" tabs={[{ id: "overview", label: "Overview" }, …]} activeTab={tab} onChange={setTab} />
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
**Plain, by default.** The selected tab is underlined, 2px, on a 1px rule
|
|
2001
|
+
that runs the strip's width. The other tabs are muted and darken under the
|
|
2002
|
+
pointer, with a lighter underline as a hint. The weight never changes, so
|
|
2003
|
+
selecting a tab never moves the row. Keyboard focus is Roster's ring. The
|
|
2004
|
+
underline is a border, so it stays in forced colors, and only the selected
|
|
2005
|
+
tab draws one there. Its colors are tokens, each set in light and dark:
|
|
2006
|
+
|
|
2007
|
+
| token | light | dark | paints |
|
|
2008
|
+
| ----- | ----- | ---- | ------ |
|
|
2009
|
+
| `--roster-tabs-indicator` | `primary-500` | `primary-400` | the selected tab's underline |
|
|
2010
|
+
| `--roster-tabs-indicator-hover` | `gray-300` | `gray-600` | the underline under the pointer |
|
|
2011
|
+
| `--roster-tabs-border` | `gray-200` | `gray-800` | the rule along the strip |
|
|
2012
|
+
| `--roster-tabs-text` | `gray-500` | `gray-400` | a tab at rest |
|
|
2013
|
+
| `--roster-tabs-text-hover` | `gray-900` | `gray-100` | a tab under the pointer |
|
|
2014
|
+
| `--roster-tabs-text-active` | `gray-900` | `gray-100` | the selected tab |
|
|
2015
|
+
|
|
2016
|
+
On Roster's own surfaces the underline clears 3:1 against the page (6.37:1 in
|
|
2017
|
+
light, 7.31:1 in dark), a label at rest 4.5:1 (5.99:1 and 7.83:1), and a
|
|
2018
|
+
count's number 4.5:1 against its mark (5.07:1 at the least). `fullWidth` gives
|
|
2019
|
+
every tab an equal share of the row, and `size="lg"` makes each at least 44px
|
|
2020
|
+
tall.
|
|
2021
|
+
|
|
2022
|
+
**Liquid.** `variant="liquid"` draws the sliding pill instead:
|
|
2023
|
+
`liquidStyle="pill"` (the default), a floating pill in a padded strip, or
|
|
2024
|
+
`"filled"`, where the selected tab fills its cell. The pill follows
|
|
2025
|
+
`activeTab`, never the click, and moves in two phases, a stretch and a
|
|
2026
|
+
settle, or not at all under reduced motion. Its colors are the
|
|
2027
|
+
`--roster-lt-*` tokens. `liquidStyle` is a type error without
|
|
2028
|
+
`variant="liquid"`.
|
|
2029
|
+
|
|
2030
|
+
**Counts.** A tab takes `count`, in every variant, and so does a `LiquidNav` link, shown
|
|
2031
|
+
in a small mark after its label:
|
|
1994
2032
|
|
|
1995
2033
|
```tsx
|
|
1996
|
-
<
|
|
2034
|
+
<Tabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
|
|
1997
2035
|
```
|
|
1998
2036
|
|
|
1999
2037
|
The count is part of the name, "Queue, 12 items" rather than "Queue12" or
|
|
@@ -2014,29 +2052,29 @@ labels={{ count: (n) => (n === 1 ? "1 game" : `${n.toLocaleString()} games`) }}
|
|
|
2014
2052
|
shorten the mark, pass `labels.countMark` (`(n) => "12K"`); the name stays
|
|
2015
2053
|
whole, since a screen reader has no width to save.
|
|
2016
2054
|
- **The mark is drawn from the tab's own text color:** its number in that
|
|
2017
|
-
color on a 12% tint of it. So it follows the tab through every state,
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2055
|
+
color on a 12% tint of it. So it follows the tab through every state, every
|
|
2056
|
+
variant and both themes, and through any palette set on the tabs' tokens,
|
|
2057
|
+
and its number clears 4.5:1 in every state on Roster's own (4.88:1 at the
|
|
2058
|
+
least, on the liquid pill). `Badge` and `Pill` each draw from a fixed
|
|
2021
2059
|
palette, which on the selected tab would be a second chip sitting on the
|
|
2022
2060
|
pill.
|
|
2023
|
-
- **The pill follows,** re-measuring when a count widens its tab or
|
|
2024
|
-
before it.
|
|
2061
|
+
- **The liquid pill follows,** re-measuring when a count widens its tab or
|
|
2062
|
+
one before it.
|
|
2025
2063
|
|
|
2026
|
-
**The panel.** `
|
|
2027
|
-
with it by construction: its `id` is the tab's `panelId`, and
|
|
2028
|
-
`aria-labelledby` points at the tab through `
|
|
2029
|
-
|
|
2030
|
-
|
|
2064
|
+
**The panel.** `TabPanel` is the `role="tabpanel"` a tab controls, in every
|
|
2065
|
+
variant, paired with it by construction: its `id` is the tab's `panelId`, and
|
|
2066
|
+
its `aria-labelledby` points at the tab through `tabId`, so the panel takes the
|
|
2067
|
+
tab's name, count included. Give the `Tabs` an `id` to pair it with. One panel
|
|
2068
|
+
that swaps its content, or a panel per tab:
|
|
2031
2069
|
|
|
2032
2070
|
```tsx
|
|
2033
2071
|
// One panel: every tab carries the same panelId, and `tab` is the selected one.
|
|
2034
|
-
<
|
|
2035
|
-
<
|
|
2072
|
+
<Tabs id="lists" tabs={tabs.map((t) => ({ ...t, panelId: "lists-panel" }))} activeTab={list} onChange={setList} />
|
|
2073
|
+
<TabPanel tabsId="lists" id="lists-panel" tab={list}>…</TabPanel>
|
|
2036
2074
|
|
|
2037
2075
|
// A panel per tab: each its own panelId, and every panel gets activeTab.
|
|
2038
|
-
<
|
|
2039
|
-
<
|
|
2076
|
+
<TabPanel tabsId="views" id="views-board" tab="board" activeTab={view}>…</TabPanel>
|
|
2077
|
+
<TabPanel tabsId="views" id="views-list" tab="list" activeTab={view} keepMounted>…</TabPanel>
|
|
2040
2078
|
```
|
|
2041
2079
|
|
|
2042
2080
|
Per tab, a panel that isn't selected stays in the page, hidden, so every tab's
|
|
@@ -2048,6 +2086,22 @@ can reach what they just selected; it leaves the tab order once its content
|
|
|
2048
2086
|
holds a link, a button or a field, rechecked as the content changes. Pass
|
|
2049
2087
|
`tabIndex` to decide it yourself.
|
|
2050
2088
|
|
|
2089
|
+
**From `LiquidTabs`.** `LiquidTabs` is deprecated, and keeps working,
|
|
2090
|
+
unchanged: it renders `Tabs` with the liquid variant, the same markup, styles
|
|
2091
|
+
and test ids it always has. To move, rename it and add the variant; a
|
|
2092
|
+
`variant="filled"` moves to `liquidStyle`:
|
|
2093
|
+
|
|
2094
|
+
```tsx
|
|
2095
|
+
<LiquidTabs … /> → <Tabs variant="liquid" … />
|
|
2096
|
+
<LiquidTabs variant="filled" … /> → <Tabs variant="liquid" liquidStyle="filled" … />
|
|
2097
|
+
<LiquidTabPanel … /> → <TabPanel … />
|
|
2098
|
+
liquidTabId(id, tab) → tabId(id, tab)
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
Nothing else changes: the props, ids, keys, pill and `data-testid` hooks are
|
|
2102
|
+
the same. `LiquidTabsLabels` is now `TabsLabels`, and the old name still works.
|
|
2103
|
+
`LiquidNav`, the liquid strip as a `nav` of links, is unchanged.
|
|
2104
|
+
|
|
2051
2105
|
### Components that render links
|
|
2052
2106
|
|
|
2053
2107
|
`Breadcrumbs` renders a plain `<a>` by default, which is right for a static
|
|
@@ -2098,8 +2152,8 @@ Dialog or Sheet), alongside `Pagination`, the loading system and DataTable's
|
|
|
2098
2152
|
instead, and 5.8.0 adds the ones that were missing: MultiSelect's
|
|
2099
2153
|
`countLabel`, DataTable's `emptyMessage` and MediaPreview's `newTabLabel`, next
|
|
2100
2154
|
to the existing `dismissLabel`, `placeholder`, `removeLabel`, `prevLabel` and
|
|
2101
|
-
the like. `LiquidTabs` and `LiquidNav` take `labels` for the words a
|
|
2102
|
-
says. Words that take a number or a name are functions, so a language
|
|
2155
|
+
the like. `Tabs`, `LiquidTabs` and `LiquidNav` take `labels` for the words a
|
|
2156
|
+
count says. Words that take a number or a name are functions, so a language
|
|
2103
2157
|
whose word changes with the count can say so. An entry passed as `undefined`
|
|
2104
2158
|
(a catalog missing a key) keeps its English rather than leaving a control
|
|
2105
2159
|
unnamed.
|
|
@@ -2210,9 +2264,9 @@ function App() {
|
|
|
2210
2264
|
| `DescriptionList` | Label and value pairs as a real `<dl>`: inline, stacked, or split |
|
|
2211
2265
|
| `EmptyState` | Zero-data placeholder with icon and action slot; `headingLevel` sets its title's `h1` to `h6` |
|
|
2212
2266
|
| `ErrorState` | Error display with retry action |
|
|
2213
|
-
| `LiquidNav` |
|
|
2214
|
-
| `LiquidTabs` |
|
|
2215
|
-
| `LiquidTabPanel` |
|
|
2267
|
+
| `LiquidNav` | The liquid tab strip as a `nav` of links; takes a router's link component |
|
|
2268
|
+
| `LiquidTabs` | Deprecated: `Tabs` with `variant="liquid"`, which it renders (see [Tabs](#tabs)) |
|
|
2269
|
+
| `LiquidTabPanel` | Deprecated: the old name of `TabPanel` |
|
|
2216
2270
|
| `MatchupCard` | Head-to-head comparison card |
|
|
2217
2271
|
| `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
|
|
2218
2272
|
| `Menu` | A button's menu of actions: icons, shortcuts, checkbox and radio items, links, groups, submenus; the full keyboard pattern, 44px targets (see [Menu](#menu)) |
|
|
@@ -2225,6 +2279,8 @@ function App() {
|
|
|
2225
2279
|
| `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
|
|
2226
2280
|
| `Slider` | A value along a track, starting in the middle or not set on request; named snap-point chips, a visible number, the full keyboard, 44px targets (see [Slider](#slider)) |
|
|
2227
2281
|
| `RangeSlider` | A low and a high on one track: two thumbs that meet and never cross, each its own slider to the keyboard (see [RangeSlider](#rangeslider)) |
|
|
2282
|
+
| `Tabs` | Tab strip, plain (an underline) or liquid (a sliding pill); arrow keys, one tab stop, counts, 44px `lg` size (see [Tabs](#tabs)) |
|
|
2283
|
+
| `TabPanel` | The panel a `Tabs` tab controls, named for its tab by construction; one swapping panel or one per tab |
|
|
2228
2284
|
| `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
|
|
2229
2285
|
|
|
2230
2286
|
### Organisms
|
|
@@ -2390,6 +2446,13 @@ npm run check:prefix
|
|
|
2390
2446
|
npm run check:classes-emit
|
|
2391
2447
|
```
|
|
2392
2448
|
|
|
2449
|
+
In CI the Storybook projects run inside Playwright's own image for the locked
|
|
2450
|
+
version (`mcr.microsoft.com/playwright:v<version>-noble` in `ci.yml`), so no
|
|
2451
|
+
browser or system library is installed from Ubuntu's mirrors. Bumping
|
|
2452
|
+
`playwright` means bumping that tag too: `src/ci-workflow.test.ts` fails until
|
|
2453
|
+
they match. Every job and test step has a time limit, so a stall fails with
|
|
2454
|
+
GitHub's "has timed out" message instead of running for hours.
|
|
2455
|
+
|
|
2393
2456
|
Three guards are worth knowing about, because the bugs they catch cannot be
|
|
2394
2457
|
reproduced from inside this repo.
|
|
2395
2458
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { TabsLabels } from './liquid-count';
|
|
3
3
|
/**
|
|
4
4
|
* A tab's or a link's label, with its count beside it when it has one.
|
|
5
5
|
*
|
|
@@ -25,5 +25,5 @@ import { LiquidTabsLabels } from './liquid-count';
|
|
|
25
25
|
export declare function LiquidCount({ label, count, labels }: {
|
|
26
26
|
label: ReactNode;
|
|
27
27
|
count: number | undefined;
|
|
28
|
-
labels:
|
|
28
|
+
labels: TabsLabels;
|
|
29
29
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("react/jsx-runtime"),i=require("../Tabs/TabPanel.cjs");function t(e){return n.jsx(i.TabPanel,{...e})}exports.LiquidTabPanel=t;
|
|
@@ -1,52 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export interface LiquidTabPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, "id" | "role" | "hidden" | "children"> {
|
|
3
|
-
/** The `id` of the `LiquidTabs` this panel belongs to (its `id` prop). */
|
|
4
|
-
tabsId: string;
|
|
5
|
-
/** This panel's `id`: the `panelId` its tab carries, or every tab, for one panel that swaps its content. */
|
|
6
|
-
id: string;
|
|
7
|
-
/**
|
|
8
|
-
* The tab that names this panel. For one panel that swaps its content, the
|
|
9
|
-
* selected tab: the panel's name follows the selection. For a panel per tab,
|
|
10
|
-
* its own tab.
|
|
11
|
-
*/
|
|
12
|
-
tab: string;
|
|
13
|
-
/**
|
|
14
|
-
* For a panel per tab: the selected tab. The panel shows when it is `tab`,
|
|
15
|
-
* and is hidden otherwise. Leave it out for one panel that swaps its
|
|
16
|
-
* content, which always shows.
|
|
17
|
-
*/
|
|
18
|
-
activeTab?: string;
|
|
19
|
-
/**
|
|
20
|
-
* For a panel per tab: keep the content mounted while the panel is hidden,
|
|
21
|
-
* so what's in it (a half-filled form, a scroll position) survives a switch
|
|
22
|
-
* away and back. By default a hidden panel renders no content. The panel
|
|
23
|
-
* element itself always stays, hidden, so its tab's `aria-controls` points
|
|
24
|
-
* at something.
|
|
25
|
-
*/
|
|
26
|
-
keepMounted?: boolean;
|
|
27
|
-
children?: ReactNode;
|
|
28
|
-
}
|
|
1
|
+
import { TabPanelProps } from '../Tabs/TabPanel';
|
|
29
2
|
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* <LiquidTabs id="views" tabs={[{ id: "open", label: "Open", panelId: "views-panel" }, …]} … />
|
|
36
|
-
* <LiquidTabPanel tabsId="views" id="views-panel" tab={view}>…</LiquidTabPanel>
|
|
37
|
-
* ```
|
|
38
|
-
*
|
|
39
|
-
* Two ways to lay it out. **One panel that swaps its content:** every tab
|
|
40
|
-
* carries the same `panelId`, and `tab` is the selected tab. **A panel per
|
|
41
|
-
* tab:** each tab has its own `panelId`, each panel its own `tab`, and every
|
|
42
|
-
* panel gets `activeTab`, so only the selected one shows.
|
|
3
|
+
* @deprecated Renamed `TabPanelProps`; the props are the same.
|
|
4
|
+
*/
|
|
5
|
+
export type LiquidTabPanelProps = TabPanelProps;
|
|
6
|
+
/**
|
|
7
|
+
* The panel a tab controls, named for its tab by construction.
|
|
43
8
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* renders, and it can change as content loads, so the panel starts reachable
|
|
48
|
-
* (`tabIndex={0}`) and leaves the tab order once its content holds something
|
|
49
|
-
* that can be Tabbed to, rechecking as the content changes. Pass `tabIndex`
|
|
50
|
-
* to decide it yourself.
|
|
9
|
+
* @deprecated Renamed `TabPanel`, which this renders, for `Tabs` of every
|
|
10
|
+
* variant and for `LiquidTabs`. Replace the name; nothing else changes. This
|
|
11
|
+
* name keeps working.
|
|
51
12
|
*/
|
|
52
|
-
export declare function LiquidTabPanel(
|
|
13
|
+
export declare function LiquidTabPanel(props: LiquidTabPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function w({ tabsId: d, id: s, tab: t, activeTab: n, keepMounted: l = !1, tabIndex: b, children: u, ...c }) {
|
|
7
|
-
const r = m(null), a = n === void 0 || n === t, [f, h] = v(!1);
|
|
8
|
-
return x(() => {
|
|
9
|
-
const e = r.current;
|
|
10
|
-
if (!e) return;
|
|
11
|
-
const o = () => h(!!e.querySelector(L));
|
|
12
|
-
if (o(), typeof MutationObserver > "u") return;
|
|
13
|
-
const i = new MutationObserver(o);
|
|
14
|
-
return i.observe(e, { childList: !0, subtree: !0, attributes: !0, attributeFilter: ["disabled", "tabindex", "href", "contenteditable"] }), () => i.disconnect();
|
|
15
|
-
}, []), /* @__PURE__ */ p(
|
|
16
|
-
"div",
|
|
17
|
-
{
|
|
18
|
-
...c,
|
|
19
|
-
ref: r,
|
|
20
|
-
role: "tabpanel",
|
|
21
|
-
id: s,
|
|
22
|
-
"aria-labelledby": y(d, t),
|
|
23
|
-
hidden: !a,
|
|
24
|
-
tabIndex: b ?? (f ? void 0 : 0),
|
|
25
|
-
"data-tab-panel": "",
|
|
26
|
-
children: a || l ? u : null
|
|
27
|
-
}
|
|
28
|
-
);
|
|
2
|
+
import { jsx as e } from "react/jsx-runtime";
|
|
3
|
+
import { TabPanel as i } from "../Tabs/TabPanel.js";
|
|
4
|
+
function t(r) {
|
|
5
|
+
return /* @__PURE__ */ e(i, { ...r });
|
|
29
6
|
}
|
|
30
7
|
export {
|
|
31
|
-
|
|
8
|
+
t as LiquidTabPanel
|
|
32
9
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),u=require("../Tabs/Tabs.cjs");function r({variant:i="pill",...e}){return t.jsx(u.Tabs,{...e,variant:"liquid",liquidStyle:i})}exports.LiquidTabs=r;
|
|
@@ -1,29 +1,7 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
1
|
import { LiquidSize, LiquidVariant } from './liquid-pill';
|
|
3
|
-
import {
|
|
4
|
-
export type {
|
|
5
|
-
export
|
|
6
|
-
id: string;
|
|
7
|
-
/** Static label, or a render function that receives whether this tab is active. */
|
|
8
|
-
label: ReactNode | ((isActive: boolean) => ReactNode);
|
|
9
|
-
/**
|
|
10
|
-
* The `id` of the panel this tab controls, which sets `aria-controls`. Give
|
|
11
|
-
* every tab the same id when one panel swaps its content.
|
|
12
|
-
*/
|
|
13
|
-
panelId?: string;
|
|
14
|
-
/**
|
|
15
|
-
* A number in a small mark beside the label, read as part of the tab's
|
|
16
|
-
* name: "Open, 12 items" (`labels.count` says it).
|
|
17
|
-
*
|
|
18
|
-
* **Zero shows**, as "0" and "0 items": a count that vanished at zero would
|
|
19
|
-
* change the tab's width and move every tab after it, and an empty list is
|
|
20
|
-
* worth saying. To hide it, pass `undefined` for zero. `undefined` means no
|
|
21
|
-
* count and no mark. A negative or fractional count isn't shown at all,
|
|
22
|
-
* never rounded or clamped. Large counts aren't abbreviated: 12,345 shows
|
|
23
|
-
* as "12,345" unless `labels.countMark` writes it shorter.
|
|
24
|
-
*/
|
|
25
|
-
count?: number;
|
|
26
|
-
}
|
|
2
|
+
import { TabItem, TabsLabels } from '../Tabs/Tabs';
|
|
3
|
+
export type { TabItem };
|
|
4
|
+
export type { LiquidTabsLabels } from './liquid-count';
|
|
27
5
|
export interface LiquidTabsProps {
|
|
28
6
|
tabs: TabItem[];
|
|
29
7
|
/** The `id` of the currently active tab. */
|
|
@@ -64,25 +42,20 @@ export interface LiquidTabsProps {
|
|
|
64
42
|
/** Extra classes applied to the outer container. */
|
|
65
43
|
className?: string;
|
|
66
44
|
/** Every word a count says, with English defaults: see `TabItem.count`. */
|
|
67
|
-
labels?: Partial<
|
|
45
|
+
labels?: Partial<TabsLabels>;
|
|
68
46
|
}
|
|
69
47
|
/**
|
|
70
48
|
* A controlled tab strip with a liquid sliding indicator.
|
|
71
49
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* shows.
|
|
76
|
-
*
|
|
77
|
-
* Until script has measured the pill, the active tab paints the pill's color
|
|
78
|
-
* itself, so server-rendered HTML shows the selection too.
|
|
79
|
-
*
|
|
80
|
-
* The pill follows `activeTab`, never the click, so a change the consumer
|
|
81
|
-
* vetoes, or one that waits on navigation, never leaves it on the wrong tab.
|
|
50
|
+
* @deprecated Use `Tabs` with `variant="liquid"`, which renders exactly this:
|
|
51
|
+
* the same markup, styles, keys and pill. `variant` becomes `liquidStyle`, and
|
|
52
|
+
* every other prop is the same:
|
|
82
53
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <LiquidTabs variant="filled" … /> // before
|
|
56
|
+
* <Tabs variant="liquid" liquidStyle="filled" … /> // after
|
|
57
|
+
* ```
|
|
85
58
|
*
|
|
86
|
-
*
|
|
59
|
+
* `LiquidTabs` keeps working, unchanged.
|
|
87
60
|
*/
|
|
88
|
-
export declare function LiquidTabs({
|
|
61
|
+
export declare function LiquidTabs({ variant, ...rest }: LiquidTabsProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,99 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { LiquidCount as z } from "./LiquidCount.js";
|
|
7
|
-
import { shownCount as G, countName as J, LIQUID_COUNT_LABELS as M } from "./liquid-count.js";
|
|
8
|
-
import { withLabels as V } from "../../../lib/labels.js";
|
|
9
|
-
import { useKeyFocus as X } from "../../../internal/key-focus.js";
|
|
10
|
-
function re({
|
|
11
|
-
tabs: l,
|
|
12
|
-
activeTab: r,
|
|
13
|
-
onChange: S,
|
|
14
|
-
variant: n = "pill",
|
|
15
|
-
fullWidth: m = !1,
|
|
16
|
-
size: g = "md",
|
|
17
|
-
activation: h = "automatic",
|
|
18
|
-
id: w,
|
|
19
|
-
"aria-label": C,
|
|
20
|
-
"aria-labelledby": N,
|
|
21
|
-
className: k,
|
|
22
|
-
labels: R
|
|
23
|
-
}) {
|
|
24
|
-
X();
|
|
25
|
-
const f = V(M, R), d = A(null), p = A(null), _ = j(), y = w ?? _;
|
|
26
|
-
Q(d, p, r, l.map((e) => e.id).join("\0"));
|
|
27
|
-
const I = (e) => {
|
|
28
|
-
e !== r && S(e);
|
|
29
|
-
}, [u, L] = F(null), b = l.some((e) => e.id === r), x = u !== null && l.some((e) => e.id === u) ? u : b ? r : l[0]?.id, D = (e) => {
|
|
30
|
-
e.currentTarget.contains(e.relatedTarget) || L(null);
|
|
31
|
-
}, E = (e) => {
|
|
32
|
-
const i = d.current;
|
|
33
|
-
if (!i) return;
|
|
34
|
-
const t = Array.from(
|
|
35
|
-
i.querySelectorAll('[role="tab"]')
|
|
36
|
-
), s = t.indexOf(e.target);
|
|
37
|
-
if (s === -1) return;
|
|
38
|
-
const a = t.length - 1, q = getComputedStyle(i).direction === "rtl", P = q ? "ArrowLeft" : "ArrowRight", T = q ? "ArrowRight" : "ArrowLeft";
|
|
39
|
-
let o;
|
|
40
|
-
if (e.key === P) o = s === a ? 0 : s + 1;
|
|
41
|
-
else if (e.key === T) o = s === 0 ? a : s - 1;
|
|
42
|
-
else if (e.key === "Home") o = 0;
|
|
43
|
-
else if (e.key === "End") o = a;
|
|
44
|
-
else return;
|
|
45
|
-
e.preventDefault(), t[o].focus(), h === "automatic" && I(l[o].id);
|
|
46
|
-
};
|
|
47
|
-
return /* @__PURE__ */ U(
|
|
48
|
-
"div",
|
|
49
|
-
{
|
|
50
|
-
ref: d,
|
|
51
|
-
id: y,
|
|
52
|
-
role: "tablist",
|
|
53
|
-
"aria-label": C,
|
|
54
|
-
"aria-labelledby": N,
|
|
55
|
-
"data-testid": "liquid-tabs",
|
|
56
|
-
style: B,
|
|
57
|
-
className: H(n, m, k),
|
|
58
|
-
onKeyDown: E,
|
|
59
|
-
onBlur: D,
|
|
60
|
-
children: [
|
|
61
|
-
/* @__PURE__ */ c(
|
|
62
|
-
"div",
|
|
63
|
-
{
|
|
64
|
-
ref: p,
|
|
65
|
-
"aria-hidden": !0,
|
|
66
|
-
"data-testid": "liquid-tabs-pill",
|
|
67
|
-
className: O(n),
|
|
68
|
-
style: K
|
|
69
|
-
}
|
|
70
|
-
),
|
|
71
|
-
l.map((e) => {
|
|
72
|
-
const i = e.id === r, t = typeof e.label == "function" ? e.label(i) : e.label;
|
|
73
|
-
return /* @__PURE__ */ c(
|
|
74
|
-
"button",
|
|
75
|
-
{
|
|
76
|
-
type: "button",
|
|
77
|
-
role: "tab",
|
|
78
|
-
id: $(y, e.id),
|
|
79
|
-
"aria-selected": i,
|
|
80
|
-
"aria-controls": e.panelId,
|
|
81
|
-
"aria-label": J(t, e.count, f),
|
|
82
|
-
tabIndex: e.id === x ? 0 : -1,
|
|
83
|
-
"data-tab-id": e.id,
|
|
84
|
-
"data-testid": `liquid-tab-${e.id}`,
|
|
85
|
-
onClick: () => I(e.id),
|
|
86
|
-
onFocus: () => L(e.id),
|
|
87
|
-
className: Y({ variant: n, fullWidth: m, size: g, isActive: i }),
|
|
88
|
-
children: G(e.count) ? /* @__PURE__ */ c(z, { label: t, count: e.count, labels: f }) : t
|
|
89
|
-
},
|
|
90
|
-
e.id
|
|
91
|
-
);
|
|
92
|
-
})
|
|
93
|
-
]
|
|
94
|
-
}
|
|
95
|
-
);
|
|
2
|
+
import { jsx as t } from "react/jsx-runtime";
|
|
3
|
+
import { Tabs as l } from "../Tabs/Tabs.js";
|
|
4
|
+
function e({ variant: i = "pill", ...r }) {
|
|
5
|
+
return /* @__PURE__ */ t(l, { ...r, variant: "liquid", liquidStyle: i });
|
|
96
6
|
}
|
|
97
7
|
export {
|
|
98
|
-
|
|
8
|
+
e as LiquidTabs
|
|
99
9
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Counts on a tab or a nav link: what `LiquidTabs` and `LiquidNav`
|
|
3
|
-
* about them. Internal; the type is exported from `
|
|
2
|
+
* Counts on a tab or a nav link: what `Tabs`, `LiquidTabs` and `LiquidNav`
|
|
3
|
+
* share about them. Internal; the type is exported from `Tabs`.
|
|
4
4
|
*/
|
|
5
5
|
/** Every word a count says. Pass any of them in `labels` to replace it. */
|
|
6
|
-
export interface
|
|
6
|
+
export interface TabsLabels {
|
|
7
7
|
/**
|
|
8
8
|
* What a count says after the label, as part of the tab's accessible name:
|
|
9
9
|
* "12 items", so the tab reads "Open, 12 items". Name your own noun, with
|
|
@@ -19,7 +19,9 @@ export interface LiquidTabsLabels {
|
|
|
19
19
|
*/
|
|
20
20
|
countMark: (count: number) => string;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/** @deprecated Renamed `TabsLabels`; the words are the same. */
|
|
23
|
+
export type LiquidTabsLabels = TabsLabels;
|
|
24
|
+
export declare const LIQUID_COUNT_LABELS: TabsLabels;
|
|
23
25
|
/**
|
|
24
26
|
* Whether a count gets a mark: a whole number, zero or more. `undefined`
|
|
25
27
|
* means no count. A negative or fractional count, or `NaN`, isn't shown at
|
|
@@ -34,4 +36,4 @@ export declare const shownCount: (count: number | undefined) => count is number;
|
|
|
34
36
|
* visually hidden part, so the hidden ", 12 items" reads "Open , 12 items".
|
|
35
37
|
* A label that's a node keeps that content name, which is spoken the same.
|
|
36
38
|
*/
|
|
37
|
-
export declare const countName: (label: unknown, count: number | undefined, labels:
|
|
39
|
+
export declare const countName: (label: unknown, count: number | undefined, labels: TabsLabels) => string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("../Tabs/tab-id.cjs"),e=t.tabId;exports.liquidTabId=e;
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `id` a
|
|
2
|
+
* The `id` a tab renders with, for a panel's `aria-labelledby`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* <div role="tabpanel" id="views-panel" aria-labelledby={liquidTabId("views", view)}>
|
|
7
|
-
* ```
|
|
8
|
-
*
|
|
9
|
-
* The tab's own `id` is used verbatim, so keep it free of whitespace. A module
|
|
10
|
-
* of its own, with no React in it, so a server component can call it too.
|
|
4
|
+
* @deprecated Renamed `tabId`, which is the same function: `Tabs` and
|
|
5
|
+
* `LiquidTabs` give their tabs the same ids. This name keeps working.
|
|
11
6
|
*/
|
|
12
|
-
export declare
|
|
7
|
+
export declare const liquidTabId: (listId: string, tab: string) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("react/jsx-runtime"),t=require("react"),p=require("./tab-id.cjs"),x='a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), iframe, summary, audio[controls], video[controls], [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';function m({tabsId:d,id:l,tab:n,activeTab:a,keepMounted:b=!1,tabIndex:u,children:c,...f}){const r=t.useRef(null),o=a===void 0||a===n,[h,v]=t.useState(!1);return t.useLayoutEffect(()=>{const e=r.current;if(!e)return;const s=()=>v(!!e.querySelector(x));if(s(),typeof MutationObserver>"u")return;const i=new MutationObserver(s);return i.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["disabled","tabindex","href","contenteditable"]}),()=>i.disconnect()},[]),y.jsx("div",{...f,ref:r,role:"tabpanel",id:l,"aria-labelledby":p.tabId(d,n),hidden:!o,tabIndex:u??(h?void 0:0),"data-tab-panel":"",children:o||b?c:null})}exports.TabPanel=m;
|