@blakesteve/roster 5.13.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 +187 -41
- package/dist/components/molecules/Carousel/Carousel.cjs +1 -1
- package/dist/components/molecules/Carousel/Carousel.d.ts +65 -3
- package/dist/components/molecules/Carousel/Carousel.js +376 -225
- package/dist/components/molecules/Carousel/carousel-math.cjs +1 -1
- package/dist/components/molecules/Carousel/carousel-math.d.ts +16 -0
- package/dist/components/molecules/Carousel/carousel-math.js +63 -44
- package/dist/components/molecules/Carousel/carousel-selection.cjs +1 -0
- package/dist/components/molecules/Carousel/carousel-selection.d.ts +28 -0
- package/dist/components/molecules/Carousel/carousel-selection.js +23 -0
- 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/internal/dev.cjs +1 -0
- package/dist/internal/dev.d.ts +10 -0
- package/dist/internal/dev.js +10 -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.
|
|
@@ -1861,8 +1862,8 @@ Things to know:
|
|
|
1861
1862
|
|
|
1862
1863
|
### Carousel
|
|
1863
1864
|
|
|
1864
|
-
A sideways row: a row of cards, a selection strip, or a
|
|
1865
|
-
view. It is a real scroll container with CSS scroll snap, so touch, trackpad,
|
|
1865
|
+
A sideways row: a row of cards, a selection strip (`selectable`, below), or a
|
|
1866
|
+
gallery of one per view. It is a real scroll container with CSS scroll snap, so touch, trackpad,
|
|
1866
1867
|
Shift with the wheel and keyboard focus all scroll it without script, and every
|
|
1867
1868
|
item is in the server render. No clones, no virtualization, no loop, and
|
|
1868
1869
|
nothing moves on its own.
|
|
@@ -1895,22 +1896,142 @@ Script adds what a mouse is missing:
|
|
|
1895
1896
|
|
|
1896
1897
|
A vertical wheel over the row scrolls the page, and sideways overscroll is
|
|
1897
1898
|
contained. Under reduced motion every scroll the component makes is instant.
|
|
1898
|
-
`ref` gives `scrollToIndex(i)`, `scrollPrev()` and `
|
|
1899
|
+
`ref` gives `scrollToIndex(i)`, `scrollPrev()`, `scrollNext()` and `focus()`;
|
|
1899
1900
|
`onIndexChange` reports the first item wholly in view. For a gallery,
|
|
1900
1901
|
`showPosition` shows "2 of 5", which is also each item's name and is announced
|
|
1901
1902
|
politely when it changes.
|
|
1902
1903
|
|
|
1903
|
-
|
|
1904
|
+
#### Selection mode
|
|
1904
1905
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1906
|
+
For a strip of choices, one of which is picked, pass `selectable`. The row
|
|
1907
|
+
becomes a horizontal listbox: one tab stop, the keys moving focus between
|
|
1908
|
+
options, and one option selected. A row of buttons with `aria-pressed` is a tab
|
|
1909
|
+
stop per button (300 options, 300 Tab presses), and a screen reader hears a
|
|
1910
|
+
list of unrelated buttons. Without `selectable` nothing changes.
|
|
1908
1911
|
|
|
1909
|
-
|
|
1910
|
-
|
|
1912
|
+
```tsx
|
|
1913
|
+
<Carousel
|
|
1914
|
+
aria-label="Choose a record"
|
|
1915
|
+
aria-describedby="how"
|
|
1916
|
+
selectable
|
|
1917
|
+
selectedKey={chosen}
|
|
1918
|
+
onSelectionChange={setChosen}
|
|
1919
|
+
onActivate={play}
|
|
1920
|
+
clearable
|
|
1921
|
+
itemWidth={150}
|
|
1922
|
+
>
|
|
1923
|
+
{records.map((r) => <RecordCell key={r.id} record={r} selected={r.id === chosen} />)}
|
|
1924
|
+
</Carousel>
|
|
1925
|
+
```
|
|
1926
|
+
|
|
1927
|
+
- **Tab** lands on the selected option, or the first when nothing is. Leaving
|
|
1928
|
+
the row hands the tab stop back to the selected option.
|
|
1929
|
+
- **Left and Right** move by one, without wrapping; **Home and End** go to the
|
|
1930
|
+
ends; **Page Up and Page Down** move by as many options as are wholly in
|
|
1931
|
+
view.
|
|
1932
|
+
- **Type-ahead:** typing jumps to the next option whose name starts with what
|
|
1933
|
+
was typed. A pause of 700ms starts over, and the same letter again steps on
|
|
1934
|
+
to the next option starting with it.
|
|
1935
|
+
- **Enter or Space selects. Selection doesn't follow focus:** arrowing only
|
|
1936
|
+
moves focus, so whatever a selection sets off (a pan, a fetch, an
|
|
1937
|
+
announcement) happens once, on purpose.
|
|
1938
|
+
- **Enter, a click or a tap on the selected option** calls `onActivate`, the
|
|
1939
|
+
step after choosing: open it, read it, play it. A click or tap on any other
|
|
1940
|
+
option selects it.
|
|
1941
|
+
- **Escape** clears the selection when `clearable` is set, and calls
|
|
1942
|
+
`onSelectionChange(null)`.
|
|
1943
|
+
|
|
1944
|
+
Moving focus with the keys scrolls the option into view, smoothly, or at once
|
|
1945
|
+
under reduced motion; a click doesn't move the row. A selection doesn't scroll
|
|
1946
|
+
the row at all, so a `scrollToIndex` of your own (centering the choice, say)
|
|
1947
|
+
isn't undone. The keyboard-mode tracker is mounted, so the ring shows in Safari
|
|
1948
|
+
after a click too (see [Focus](#focus)).
|
|
1949
|
+
|
|
1950
|
+
**Identity is the child's `key`.** `selectedKey`, `defaultSelectedKey` and both
|
|
1951
|
+
callbacks deal in it, as React stores it: a string. Keys, not positions,
|
|
1952
|
+
because a consumer that filters the row changes every position after the first
|
|
1953
|
+
option it hides. A development build warns about a child without a key.
|
|
1954
|
+
Controlled (`selectedKey` with `onSelectionChange`) or uncontrolled
|
|
1955
|
+
(`defaultSelectedKey`); where focus is stays internal, and `focus()` on the
|
|
1956
|
+
`ref` moves focus onto the tab stop, from a find field's Down Arrow, say.
|
|
1957
|
+
|
|
1958
|
+
**Names.** An option is named by its content, or by `getOptionLabel(key,
|
|
1959
|
+
index)`, which is also the text type-ahead matches. Name the listbox with
|
|
1960
|
+
`aria-label` or `aria-labelledby`, and put a sentence saying how it works in
|
|
1961
|
+
`aria-describedby`.
|
|
1962
|
+
|
|
1963
|
+
**Nothing interactive inside an option.** An option is one choice, reached with
|
|
1964
|
+
the arrow keys; a link or a button inside it is a tab stop the keys skip, and
|
|
1965
|
+
a screen reader can't reach it as anything but text. Put actions outside the
|
|
1966
|
+
row, or behind `onActivate`. A development build warns when an option holds
|
|
1967
|
+
something that takes focus.
|
|
1968
|
+
|
|
1969
|
+
**Filtering.** Render fewer children and `aria-setsize` and `aria-posinset`
|
|
1970
|
+
describe what's shown. The tab stop stays on its option if that's still shown;
|
|
1971
|
+
otherwise it moves to the nearest option still shown, in the order before the
|
|
1972
|
+
change, after it if there is one, else before it. A selection made elsewhere
|
|
1973
|
+
moves the tab stop to the selected option while focus is outside the row, and
|
|
1974
|
+
leaves it where the keys put it while focus is inside.
|
|
1975
|
+
|
|
1976
|
+
**The selected look is yours.** Roster draws the focus ring, an outline 2px out,
|
|
1977
|
+
so a selected look drawn with a ring or a border sits inside it. Draw the
|
|
1978
|
+
selection in the child, from `selectedKey`, or on the option with
|
|
1979
|
+
`itemClassName` and the `aria-selected:` variant.
|
|
1980
|
+
|
|
1981
|
+
**Scale.** Every option is in the DOM, for the keys and for screen readers;
|
|
1982
|
+
draw expensive content lazily inside it. Options are memoized and the keys,
|
|
1983
|
+
clicks and focus are one listener each on the row. Measured at 1,000 options
|
|
1984
|
+
in Chromium: mounting takes about 30ms longer than the default mode, a key
|
|
1985
|
+
moves focus within a millisecond, and the frame after it lands in about 7ms.
|
|
1986
|
+
|
|
1987
|
+
### Tabs
|
|
1988
|
+
|
|
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.
|
|
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:
|
|
1911
2032
|
|
|
1912
2033
|
```tsx
|
|
1913
|
-
<
|
|
2034
|
+
<Tabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
|
|
1914
2035
|
```
|
|
1915
2036
|
|
|
1916
2037
|
The count is part of the name, "Queue, 12 items" rather than "Queue12" or
|
|
@@ -1931,29 +2052,29 @@ labels={{ count: (n) => (n === 1 ? "1 game" : `${n.toLocaleString()} games`) }}
|
|
|
1931
2052
|
shorten the mark, pass `labels.countMark` (`(n) => "12K"`); the name stays
|
|
1932
2053
|
whole, since a screen reader has no width to save.
|
|
1933
2054
|
- **The mark is drawn from the tab's own text color:** its number in that
|
|
1934
|
-
color on a 12% tint of it. So it follows the tab through every state,
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
|
1938
2059
|
palette, which on the selected tab would be a second chip sitting on the
|
|
1939
2060
|
pill.
|
|
1940
|
-
- **The pill follows,** re-measuring when a count widens its tab or
|
|
1941
|
-
before it.
|
|
2061
|
+
- **The liquid pill follows,** re-measuring when a count widens its tab or
|
|
2062
|
+
one before it.
|
|
1942
2063
|
|
|
1943
|
-
**The panel.** `
|
|
1944
|
-
with it by construction: its `id` is the tab's `panelId`, and
|
|
1945
|
-
`aria-labelledby` points at the tab through `
|
|
1946
|
-
|
|
1947
|
-
|
|
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:
|
|
1948
2069
|
|
|
1949
2070
|
```tsx
|
|
1950
2071
|
// One panel: every tab carries the same panelId, and `tab` is the selected one.
|
|
1951
|
-
<
|
|
1952
|
-
<
|
|
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>
|
|
1953
2074
|
|
|
1954
2075
|
// A panel per tab: each its own panelId, and every panel gets activeTab.
|
|
1955
|
-
<
|
|
1956
|
-
<
|
|
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>
|
|
1957
2078
|
```
|
|
1958
2079
|
|
|
1959
2080
|
Per tab, a panel that isn't selected stays in the page, hidden, so every tab's
|
|
@@ -1965,6 +2086,22 @@ can reach what they just selected; it leaves the tab order once its content
|
|
|
1965
2086
|
holds a link, a button or a field, rechecked as the content changes. Pass
|
|
1966
2087
|
`tabIndex` to decide it yourself.
|
|
1967
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
|
+
|
|
1968
2105
|
### Components that render links
|
|
1969
2106
|
|
|
1970
2107
|
`Breadcrumbs` renders a plain `<a>` by default, which is right for a static
|
|
@@ -2015,8 +2152,8 @@ Dialog or Sheet), alongside `Pagination`, the loading system and DataTable's
|
|
|
2015
2152
|
instead, and 5.8.0 adds the ones that were missing: MultiSelect's
|
|
2016
2153
|
`countLabel`, DataTable's `emptyMessage` and MediaPreview's `newTabLabel`, next
|
|
2017
2154
|
to the existing `dismissLabel`, `placeholder`, `removeLabel`, `prevLabel` and
|
|
2018
|
-
the like. `LiquidTabs` and `LiquidNav` take `labels` for the words a
|
|
2019
|
-
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
|
|
2020
2157
|
whose word changes with the count can say so. An entry passed as `undefined`
|
|
2021
2158
|
(a catalog missing a key) keeps its English rather than leaving a control
|
|
2022
2159
|
unnamed.
|
|
@@ -2123,13 +2260,13 @@ function App() {
|
|
|
2123
2260
|
| `Breadcrumbs` | Navigation trail; pass `linkComponent` to keep navigation client-side |
|
|
2124
2261
|
| `CheckboxGroup` | Checkbox set reporting one `string[]`; groups, columns, scroll, error |
|
|
2125
2262
|
| `CallToAction` | Prominent hero-style CTA block |
|
|
2126
|
-
| `Carousel` | Sideways row of items on native scroll snap; mouse drag, paging arrows, no autoplay |
|
|
2263
|
+
| `Carousel` | Sideways row of items on native scroll snap; mouse drag, paging arrows, no autoplay; a selection mode for a strip of choices (see [Selection mode](#selection-mode)) |
|
|
2127
2264
|
| `DescriptionList` | Label and value pairs as a real `<dl>`: inline, stacked, or split |
|
|
2128
2265
|
| `EmptyState` | Zero-data placeholder with icon and action slot; `headingLevel` sets its title's `h1` to `h6` |
|
|
2129
2266
|
| `ErrorState` | Error display with retry action |
|
|
2130
|
-
| `LiquidNav` |
|
|
2131
|
-
| `LiquidTabs` |
|
|
2132
|
-
| `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` |
|
|
2133
2270
|
| `MatchupCard` | Head-to-head comparison card |
|
|
2134
2271
|
| `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
|
|
2135
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)) |
|
|
@@ -2142,6 +2279,8 @@ function App() {
|
|
|
2142
2279
|
| `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
|
|
2143
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)) |
|
|
2144
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 |
|
|
2145
2284
|
| `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
|
|
2146
2285
|
|
|
2147
2286
|
### Organisms
|
|
@@ -2307,6 +2446,13 @@ npm run check:prefix
|
|
|
2307
2446
|
npm run check:classes-emit
|
|
2308
2447
|
```
|
|
2309
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
|
+
|
|
2310
2456
|
Three guards are worth knowing about, because the bugs they catch cannot be
|
|
2311
2457
|
reproduced from inside this repo.
|
|
2312
2458
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("react/jsx-runtime"),Be=require("react"),$e=require("../../atoms/Button/Button.cjs"),T=require("../../../lib/utils.cjs"),ze=require("../../../lib/labels.cjs"),He=require("../../../internal/motion.cjs"),d=require("./carousel-math.cjs"),U=require("./carousel-layout.cjs"),Ue=require("../../../internal/key-focus.cjs");function Ve(o){const a=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(o){for(const f in o)if(f!=="default"){const x=Object.getOwnPropertyDescriptor(o,f);Object.defineProperty(a,f,x.get?x:{enumerable:!0,get:()=>o[f]})}}return a.default=o,Object.freeze(a)}const n=Ve(Be),We={position:(o,a)=>`${o} of ${a}`},Xe=o=>Math.min(3e3,500+o*.35),Ge='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"])',V=o=>typeof o=="number"?`${o}px`:o;function se(o){const a=parseFloat(o);return Number.isFinite(a)?a:0}const Je="rst:hidden rst:[@media(any-hover:hover)]:inline-flex",Ke="rst:hidden rst:[@media(any-hover:hover)]:flex";function oe({direction:o}){return c.jsx("svg",{"aria-hidden":"true",focusable:"false",viewBox:"0 0 20 20",className:"rst:h-5 rst:w-5",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:c.jsx("path",{d:o==="prev"?"M12.5 4.5 7 10l5.5 5.5":"M7.5 4.5 13 10l-5.5 5.5"})})}const le={overflowing:null,canScrollPrev:!1,canScrollNext:!1,index:0},Qe=n.forwardRef(function({"aria-label":a,"aria-labelledby":f,children:x,itemWidth:I,perView:C,gap:W=12,gutter:X=16,bleed:ce=!1,snap:R="start",snapStrictness:ie="mandatory",oneAtATime:ae=!1,arrows:j="hover",arrowPlacement:M="controls",renderArrows:L,prevLabel:ue,nextLabel:de,prevIcon:fe,nextIcon:ge,arrowButtonProps:ve,fade:pe=!1,scrollbar:me=!1,showPosition:G=!1,labels:be,onIndexChange:J,onScrollStateChange:K,itemClassName:xe,listClassName:he,className:ye,style:Se,...we},Ne){Ue.useKeyFocus();const Q=ze.withLabels(We,be),p=n.useRef(null),Y=`${n.useId()}-list`,Z=n.Children.toArray(x),E=Z.length,[l,Te]=n.useState(le),ee=n.useRef(le),[Ie,Le]=n.useState(""),[Ee,Pe]=n.useState(!1),O=n.useRef({onIndexChange:J,onScrollStateChange:K});n.useEffect(()=>{O.current={onIndexChange:J,onScrollStateChange:K}});const g=n.useCallback(()=>{const e=p.current;if(!e)return null;const t=e.getBoundingClientRect(),r=getComputedStyle(e),s=t.left+e.clientLeft-e.scrollLeft;return{items:Array.from(e.children).map(u=>{const b=u.getBoundingClientRect();return{start:b.left-s,end:b.right-s}}),scrollLeft:e.scrollLeft,clientWidth:e.clientWidth,scrollWidth:e.scrollWidth,paddingStart:se(r.scrollPaddingInlineStart||r.scrollPaddingLeft),paddingEnd:se(r.scrollPaddingInlineEnd||r.scrollPaddingRight),align:R}},[R]),h=n.useCallback(()=>{const e=g();if(!e)return;const t={overflowing:d.overflows(e),canScrollPrev:d.canScrollPrev(e),canScrollNext:d.canScrollNext(e),index:e.items.length?d.currentIndex(e):0},r=ee.current;r.overflowing===t.overflowing&&r.canScrollPrev===t.canScrollPrev&&r.canScrollNext===t.canScrollNext&&r.index===t.index||(ee.current=t,Te(t),r.index!==t.index&&O.current.onIndexChange?.(t.index),(r.canScrollPrev!==t.canScrollPrev||r.canScrollNext!==t.canScrollNext)&&O.current.onScrollStateChange?.({canScrollPrev:t.canScrollPrev,canScrollNext:t.canScrollNext}))},[g]),ke=JSON.stringify([W,X,I,C]);n.useLayoutEffect(()=>{const e=p.current;if(!e)return;h();let t=0;const r=()=>{t||(t=requestAnimationFrame(()=>{t=0,h()}))};e.addEventListener("scroll",r,{passive:!0});const s=typeof ResizeObserver>"u"?null:new ResizeObserver(()=>h()),i=()=>{s?.observe(e),Array.from(e.children).forEach(b=>s?.observe(b))};i();const u=typeof MutationObserver>"u"?null:new MutationObserver(()=>{i(),h()});return u?.observe(e,{childList:!0}),()=>{e.removeEventListener("scroll",r),s?.disconnect(),u?.disconnect(),t&&cancelAnimationFrame(t)}},[h,E,ke]),n.useLayoutEffect(()=>{const e=p.current;if(e&&(Pe(!e.querySelector(Ge)),f)){const t=f.split(/\s+/).map(r=>document.getElementById(r)?.textContent?.trim()??"").filter(Boolean).join(" ");Le(t)}},[x,f]);const y=n.useRef(null),v=n.useCallback(()=>{const e=p.current,t=y.current;t&&(y.current=null,window.clearTimeout(t.timer),e?.removeEventListener("scrollend",t.done),e&&(e.style.scrollSnapType="",delete e.dataset.settling))},[]),m=n.useCallback((e,t)=>{const r=p.current;if(!r)return;if(v(),t!=="smooth"||He.prefersReducedMotion()||Math.abs(r.scrollLeft-e)<1){r.style.scrollSnapType="none",r.scrollTo({left:e,behavior:"instant"}),r.style.scrollSnapType="";return}r.style.scrollSnapType="none",r.dataset.settling="";const i=r.scrollLeft,u=()=>{Math.abs(r.scrollLeft-i)<.5||v()};y.current={timer:window.setTimeout(v,Xe(Math.abs(r.scrollLeft-e))),done:u,target:e},r.addEventListener("scrollend",u),r.scrollTo({left:e,behavior:"smooth"})},[v]),P=n.useCallback(e=>{const t=g();if(!t)return;const r=y.current?{...t,scrollLeft:y.current.target}:t;m(d.pageTarget(r,e),"smooth")},[g,m]);n.useImperativeHandle(Ne,()=>({scrollToIndex:(e,t)=>{const r=g();if(!r||!r.items.length)return;const s=Math.min(Math.max(0,Math.round(e)),r.items.length-1);m(d.snapPosition(r,s),t?.behavior??"smooth")},scrollPrev:()=>P(-1),scrollNext:()=>P(1)}),[g,P,m]),n.useEffect(()=>v,[v]);const D=n.useRef(null),F=n.useRef(null),_=n.useRef(null);n.useLayoutEffect(()=>{const e=_.current;if(!e)return;const t=e==="prev"?D.current:F.current,r=e==="prev"?F.current:D.current,s=e==="prev"?!l.canScrollPrev:!l.canScrollNext,i=document.activeElement;s&&(i!==t&&i!==document.body&&i!==null||r&&!r.disabled&&r.focus())},[l.canScrollPrev,l.canScrollNext]);const S=n.useRef(null),w=n.useRef(!1),Ce=e=>{e.pointerType!=="mouse"||e.button!==0||l.overflowing&&(v(),w.current=!1,S.current={pointerId:e.pointerId,x0:e.clientX,scroll0:e.currentTarget.scrollLeft,dragging:!1})},Re=e=>{const t=S.current;if(!t||e.pointerId!==t.pointerId)return;const r=e.currentTarget,s=e.clientX-t.x0;if(!t.dragging){if(Math.abs(s)<=d.CAROUSEL_DRAG_THRESHOLD)return;t.dragging=!0,r.dataset.dragging="",r.style.scrollSnapType="none",window.getSelection?.()?.removeAllRanges();try{r.setPointerCapture(e.pointerId)}catch{}}r.scrollLeft=t.scroll0-s},A=e=>{const t=S.current;if(!t||e.pointerId!==t.pointerId||(S.current=null,!t.dragging))return;const r=e.currentTarget;delete r.dataset.dragging,w.current=e.type==="pointerup",window.setTimeout(()=>{w.current=!1},0);const s=g();s?m(d.snapPosition(s,d.nearestIndex(s)),"smooth"):r.style.scrollSnapType=""},je=e=>{!w.current||e.detail===0||(w.current=!1,e.preventDefault(),e.stopPropagation())},Me=e=>{l.overflowing&&e.preventDefault()},Oe=e=>{const t=e.currentTarget,r=e.target;if(r===t||S.current?.dragging)return;let s=!1;try{s=r.matches(":focus-visible, [data-key-focus]")}catch{s=!1}if(!s)return;const i=r.closest("[data-carousel-item]");if(!i||i.parentElement!==t)return;const u=g();if(!u)return;const b=Array.prototype.indexOf.call(t.children,i);m(d.snapPosition(u,b),"instant")},[De,Fe]=n.useState(0);n.useEffect(()=>{const e=window.setTimeout(()=>Fe(l.index),150);return()=>window.clearTimeout(e)},[l.index]);const te=a??Ie,re=e=>te?`${e} ${te}`:e,q=j!=="none"&&l.overflowing!==!1,_e=j==="hover"?Je:"rst:inline-flex",ne=e=>{const t=e==="prev",{className:r,...s}=ve??{};return c.jsx($e.Button,{type:"button",variant:"outline",colorScheme:"neutral",size:"icon",...s,"aria-label":t?ue??re("Previous"):de??re("Next"),"aria-controls":Y,disabled:t?!l.canScrollPrev:!l.canScrollNext,onClick:()=>P(t?-1:1),ref:t?D:F,onFocus:()=>{_.current=e},onBlur:i=>{i.relatedTarget&&(_.current=null)},"data-carousel-arrow":e,className:T.cn("rst:size-11 rst:shrink-0 rst:rounded-full",M==="overlay"&&!L&&"rst:bg-[var(--roster-popover-bg)] rst:elevation-raised rst:hover:bg-[var(--roster-popover-bg)] rst:disabled:invisible",_e,r),children:t?fe??c.jsx(oe,{direction:"prev"}):ge??c.jsx(oe,{direction:"next"})})},k=q?ne("prev"):null,B=q?ne("next"):null,N=G&&E?c.jsx("p",{"aria-live":"polite",className:"rst:font-ui rst:m-0 rst:text-sm rst:tabular-nums rst:opacity-75","data-carousel-position":"",children:Q.position(De+1,E)}):null;let $=null;L?$=L({prev:k,next:B,position:N}):M==="controls"&&(k||N)&&($=c.jsxs("div",{className:T.cn("rst:mb-2 rst:items-center rst:justify-end rst:gap-2",!N&&j==="hover"?Ke:"rst:flex"),"data-carousel-controls":"",children:[N&&c.jsx("div",{className:"rst:mr-auto",children:N}),k,B]}));const z=pe&&l.overflowing&&(l.canScrollPrev||l.canScrollNext)?`linear-gradient(to right, ${l.canScrollPrev?"transparent, black 48px":"black"}, ${l.canScrollNext?"black calc(100% - 48px), transparent":"black"})`:void 0,H=I===void 0&&C!==void 0,Ae={"--rst-cv-gap":V(W),"--rst-cv-gutter":V(X),...H?U.perViewVars(C):{},...z?{maskImage:z,WebkitMaskImage:z}:{}},qe=I!==void 0?{width:V(I)}:H?{width:U.FLUID_ITEM_WIDTH}:void 0;return c.jsxs("div",{className:T.cn("rst:min-w-0",ye),style:Se,...we,children:[$,c.jsxs("div",{className:"rst:relative",children:[c.jsx("ul",{ref:p,id:Y,role:"list","aria-label":a,"aria-labelledby":a?void 0:f,tabIndex:Ee&&l.overflowing?0:void 0,"data-overflowing":l.overflowing?"":void 0,onPointerDown:Ce,onPointerMove:Re,onPointerUp:A,onPointerCancel:A,onLostPointerCapture:A,onClickCapture:je,onDragStart:Me,onFocus:Oe,style:Ae,className:T.cn("rst:m-0 rst:flex rst:list-none rst:overflow-x-auto rst:overflow-y-hidden","rst:gap-(--rst-cv-gap) rst:px-(--rst-cv-gutter) rst:py-1 rst:scroll-px-(--rst-cv-gutter)","rst:overscroll-x-contain",ie==="mandatory"?"rst:snap-x rst:snap-mandatory":"rst:snap-x rst:snap-proximity",ce&&"rst:-mx-(--rst-cv-gutter)",!me&&"rst:[scrollbar-width:none] rst:[&::-webkit-scrollbar]:hidden",H&&U.FLUID_CLASSES,"rst:data-overflowing:cursor-grab rst:data-dragging:cursor-grabbing rst:data-dragging:select-none","rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-inset",he),children:Z.map((e,t)=>c.jsx("li",{"data-carousel-item":"","aria-label":G?Q.position(t+1,E):void 0,style:qe,className:T.cn("rst:shrink-0",R==="start"?"rst:snap-start":"rst:snap-center",ae&&"rst:snap-always",xe),children:e},n.isValidElement(e)&&e.key!=null?e.key:t))}),!L&&M==="overlay"&&q&&c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:start-1 rst:flex rst:items-center",children:c.jsx("div",{className:"rst:pointer-events-auto",children:k})}),c.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:end-1 rst:flex rst:items-center",children:c.jsx("div",{className:"rst:pointer-events-auto",children:B})})]})]})]})});exports.Carousel=Qe;
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react/jsx-runtime"),Ee=require("react"),Te=require("../../atoms/Button/Button.cjs"),_=require("../../../lib/utils.cjs"),Ie=require("../../../lib/labels.cjs"),Ne=require("../../../internal/motion.cjs"),v=require("./carousel-math.cjs"),ct=require("./carousel-layout.cjs"),at=require("./carousel-selection.cjs"),Ce=require("../../../internal/key-focus.cjs"),Re=require("../../../internal/dev.cjs");function Pe(l){const c=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(l){for(const u in l)if(u!=="default"){const x=Object.getOwnPropertyDescriptor(l,u);Object.defineProperty(c,u,x.get?x:{enumerable:!0,get:()=>l[u]})}}return c.default=l,Object.freeze(c)}const s=Pe(Ee),Le={position:(l,c)=>`${l} of ${c}`},je=l=>Math.min(3e3,500+l*.35),ut='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"])',dt=l=>typeof l=="number"?`${l}px`:l;function Lt(l){const c=parseFloat(l);return Number.isFinite(c)?c:0}const Oe="rst:hidden rst:[@media(any-hover:hover)]:inline-flex",De="rst:hidden rst:[@media(any-hover:hover)]:flex";function jt({direction:l}){return a.jsx("svg",{"aria-hidden":"true",focusable:"false",viewBox:"0 0 20 20",className:"rst:h-5 rst:w-5",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:a.jsx("path",{d:l==="prev"?"M12.5 4.5 7 10l5.5 5.5":"M7.5 4.5 13 10l-5.5 5.5"})})}const Me="rst:cursor-pointer rst:in-data-dragging:cursor-grabbing rst:outline-hidden rst:focus-visible:outline-solid rst:focus-visible:outline-2 rst:focus-visible:outline-offset-2 rst:focus-visible:outline-ring",Ae=s.memo(function({child:c,index:u,count:x,selected:C,active:k,label:P,style:q,className:F}){return a.jsx("li",{role:"option","aria-selected":C,"aria-setsize":x,"aria-posinset":u+1,"aria-label":P,tabIndex:k?0:-1,"data-carousel-item":"","data-selected":C?"":void 0,style:q,className:F,children:c})});function _e(l){const c=[];return s.Children.forEach(l,u=>{if(u==null||typeof u=="boolean")return;const x=s.isValidElement(u)&&u.key!==null;c.push({child:u,key:x?String(u.key):`#${c.length}`,keyed:x})}),c}const qe={active:null,keys:[],sig:"",selected:null,init:!1},W=(l,c)=>Array.prototype.indexOf.call(l.children,c),Ot={overflowing:null,canScrollPrev:!1,canScrollNext:!1,index:0},Fe=s.forwardRef(function({"aria-label":c,"aria-labelledby":u,"aria-describedby":x,children:C,itemWidth:k,perView:P,gap:q=12,gutter:F=16,bleed:Dt=!1,snap:G="start",snapStrictness:Mt="mandatory",oneAtATime:At=!1,arrows:X="hover",arrowPlacement:J="controls",renderArrows:$,prevLabel:_t,nextLabel:qt,prevIcon:Ft,nextIcon:$t,arrowButtonProps:Bt,fade:zt=!1,scrollbar:Ht=!1,showPosition:ft=!1,labels:Vt,onIndexChange:gt,onScrollStateChange:pt,selectable:p=!1,selectedKey:vt,defaultSelectedKey:Kt=null,onSelectionChange:Ut,onActivate:mt,clearable:Wt=!1,getOptionLabel:Y,itemClassName:Gt,listClassName:Xt,className:Jt,style:Yt,...Qt},Zt){Ce.useKeyFocus();const yt=Ie.withLabels(Le,Vt),h=s.useRef(null),xt=`${s.useId()}-list`,ht=s.Children.toArray(C),w=p?_e(C):null,L=w?w.length:ht.length,bt=vt!==void 0,[te,ee]=s.useState(Kt),d=bt?vt:te,[re,St]=s.useState(!1),m=w?w.map(t=>t.key):[],kt=m.join("\0"),[E,B]=s.useState(qe);let T=E.active;if(p&&(!E.init||E.sig!==kt||E.selected!==d)){const t=new Set(m);E.init&&E.selected!==d&&!re&&d!==null&&t.has(d)&&(T=d),(T===null||!t.has(T))&&(T=at.survivingNeighbor(E.keys,t,T)??(d!==null&&t.has(d)?d:m[0]??null)),B({active:T,keys:m,sig:kt,selected:d,init:!0})}const[f,ne]=s.useState(Ot),wt=s.useRef(Ot),[se,oe]=s.useState(""),[ie,le]=s.useState(!1),Q=s.useRef({onIndexChange:gt,onScrollStateChange:pt});s.useEffect(()=>{Q.current={onIndexChange:gt,onScrollStateChange:pt}});const y=s.useCallback(()=>{const t=h.current;if(!t)return null;const e=t.getBoundingClientRect(),r=getComputedStyle(t),n=e.left+t.clientLeft-t.scrollLeft;return{items:Array.from(t.children).map(i=>{const g=i.getBoundingClientRect();return{start:g.left-n,end:g.right-n}}),scrollLeft:t.scrollLeft,clientWidth:t.clientWidth,scrollWidth:t.scrollWidth,paddingStart:Lt(r.scrollPaddingInlineStart||r.scrollPaddingLeft),paddingEnd:Lt(r.scrollPaddingInlineEnd||r.scrollPaddingRight),align:G}},[G]),j=s.useCallback(()=>{const t=y();if(!t)return;const e={overflowing:v.overflows(t),canScrollPrev:v.canScrollPrev(t),canScrollNext:v.canScrollNext(t),index:t.items.length?v.currentIndex(t):0},r=wt.current;r.overflowing===e.overflowing&&r.canScrollPrev===e.canScrollPrev&&r.canScrollNext===e.canScrollNext&&r.index===e.index||(wt.current=e,ne(e),r.index!==e.index&&Q.current.onIndexChange?.(e.index),(r.canScrollPrev!==e.canScrollPrev||r.canScrollNext!==e.canScrollNext)&&Q.current.onScrollStateChange?.({canScrollPrev:e.canScrollPrev,canScrollNext:e.canScrollNext}))},[y]),ce=JSON.stringify([q,F,k,P]);s.useLayoutEffect(()=>{const t=h.current;if(!t)return;j();let e=0;const r=()=>{e||(e=requestAnimationFrame(()=>{e=0,j()}))};t.addEventListener("scroll",r,{passive:!0});const n=typeof ResizeObserver>"u"?null:new ResizeObserver(()=>j()),o=()=>{n?.observe(t),Array.from(t.children).forEach(g=>n?.observe(g))};o();const i=typeof MutationObserver>"u"?null:new MutationObserver(()=>{o(),j()});return i?.observe(t,{childList:!0}),()=>{t.removeEventListener("scroll",r),n?.disconnect(),i?.disconnect(),e&&cancelAnimationFrame(e)}},[j,L,ce]),s.useLayoutEffect(()=>{const t=h.current;if(t&&(le(!t.querySelector(ut)),u)){const e=u.split(/\s+/).map(r=>document.getElementById(r)?.textContent?.trim()??"").filter(Boolean).join(" ");oe(e)}},[C,u]);const I=s.useRef(null),N=s.useCallback(()=>{const t=h.current,e=I.current;e&&(I.current=null,window.clearTimeout(e.timer),t?.removeEventListener("scrollend",e.done),t&&(t.style.scrollSnapType="",delete t.dataset.settling))},[]),b=s.useCallback((t,e)=>{const r=h.current;if(!r)return;if(N(),e!=="smooth"||Ne.prefersReducedMotion()||Math.abs(r.scrollLeft-t)<1){r.style.scrollSnapType="none",r.scrollTo({left:t,behavior:"instant"}),r.style.scrollSnapType="";return}r.style.scrollSnapType="none",r.dataset.settling="";const o=r.scrollLeft,i=()=>{Math.abs(r.scrollLeft-o)<.5||N()};I.current={timer:window.setTimeout(N,je(Math.abs(r.scrollLeft-t))),done:i,target:t},r.addEventListener("scrollend",i),r.scrollTo({left:t,behavior:"smooth"})},[N]),z=s.useCallback(t=>{const e=y();if(!e)return;const r=I.current?{...e,scrollLeft:I.current.target}:e;b(v.pageTarget(r,t),"smooth")},[y,b]);s.useImperativeHandle(Zt,()=>({scrollToIndex:(t,e)=>{const r=y();if(!r||!r.items.length)return;const n=Math.min(Math.max(0,Math.round(t)),r.items.length-1);b(v.snapPosition(r,n),e?.behavior??"smooth")},scrollPrev:()=>z(-1),scrollNext:()=>z(1),focus:()=>{const t=h.current;if(!t)return;const e=p?t.querySelector(':scope > [tabindex="0"]'):t.querySelector(ut)??(t.tabIndex===0?t:null);if(!e)return;e.focus({preventScroll:!0});const r=e.closest("[data-carousel-item]"),n=y(),o=r&&r.parentElement===t?W(t,r):-1,i=n&&o>=0?v.revealTarget(n,o,I.current?.target??n.scrollLeft):null;i!==null&&b(i,"smooth")}}),[y,z,b,p]),s.useEffect(()=>N,[N]);const Z=s.useRef(null),tt=s.useRef(null),et=s.useRef(null);s.useLayoutEffect(()=>{const t=et.current;if(!t)return;const e=t==="prev"?Z.current:tt.current,r=t==="prev"?tt.current:Z.current,n=t==="prev"?!f.canScrollPrev:!f.canScrollNext,o=document.activeElement;n&&(o!==e&&o!==document.body&&o!==null||r&&!r.disabled&&r.focus())},[f.canScrollPrev,f.canScrollNext]);const R=s.useRef(null),O=s.useRef(!1),ae=t=>{t.pointerType!=="mouse"||t.button!==0||f.overflowing&&(N(),O.current=!1,R.current={pointerId:t.pointerId,x0:t.clientX,scroll0:t.currentTarget.scrollLeft,dragging:!1})},ue=t=>{const e=R.current;if(!e||t.pointerId!==e.pointerId)return;const r=t.currentTarget,n=t.clientX-e.x0;if(!e.dragging){if(Math.abs(n)<=v.CAROUSEL_DRAG_THRESHOLD)return;e.dragging=!0,r.dataset.dragging="",r.style.scrollSnapType="none",window.getSelection?.()?.removeAllRanges();try{r.setPointerCapture(t.pointerId)}catch{}}r.scrollLeft=e.scroll0-n},rt=t=>{const e=R.current;if(!e||t.pointerId!==e.pointerId||(R.current=null,!e.dragging))return;const r=t.currentTarget;delete r.dataset.dragging,O.current=t.type==="pointerup",window.setTimeout(()=>{O.current=!1},0);const n=y();n?b(v.snapPosition(n,v.nearestIndex(n)),"smooth"):r.style.scrollSnapType=""},de=t=>{!O.current||t.detail===0||(O.current=!1,t.preventDefault(),t.stopPropagation())},fe=t=>{f.overflowing&&t.preventDefault()},ge=t=>{if(p){ve(t);return}const e=t.currentTarget,r=t.target;if(r===e||R.current?.dragging)return;let n=!1;try{n=r.matches(":focus-visible, [data-key-focus]")}catch{n=!1}if(!n)return;const o=r.closest("[data-carousel-item]");if(!o||o.parentElement!==e)return;const i=y();if(!i)return;const g=Array.prototype.indexOf.call(e.children,o);b(v.snapPosition(i,g),"instant")},H=t=>{bt||ee(t),Ut?.(t)},pe=t=>{const e=y();if(!e)return;const r=v.revealTarget(e,t,I.current?.target??e.scrollLeft);r!==null&&b(r,"smooth")},ve=t=>{const e=t.currentTarget,r=t.target;if(r.parentElement!==e)return;const n=W(e,r),o=m[n];if(o===void 0||(B(g=>g.active===o?g:{...g,active:o}),St(!0),R.current?.dragging))return;let i=!1;try{i=r.matches(":focus-visible, [data-key-focus]")}catch{i=!1}i&&pe(n)},me=t=>{const e=t.relatedTarget;e&&t.currentTarget.contains(e)||(St(!1),d!==null&&m.includes(d)&&B(r=>r.active===d?r:{...r,active:d}))},ye=t=>{const e=t.currentTarget,r=t.target.closest("[data-carousel-item]");if(!r||r.parentElement!==e)return;const n=m[W(e,r)];n!==void 0&&(B(o=>o.active===n?o:{...o,active:n}),n===d?mt?.(n):H(n))},D=s.useRef({text:"",at:-1/0}),xe=t=>{const e=t.currentTarget,r=t.target;if(r.parentElement!==e||t.nativeEvent.isComposing||t.ctrlKey||t.metaKey||t.altKey)return;const n=m.length-1,o=W(e,r),i=m[o];if(i===void 0)return;const g=S=>{t.preventDefault();const A=Math.min(n,Math.max(0,S));A!==o&&e.children[A].focus({preventScroll:!0})},Rt=t.timeStamp-D.current.at<at.TYPEAHEAD_RESET_MS&&D.current.text!=="";switch(t.key.length!==1&&(D.current={text:"",at:-1/0}),t.key){case"ArrowRight":return g(o+1);case"ArrowLeft":return g(o-1);case"Home":return g(0);case"End":return g(n);case"PageDown":case"PageUp":{const S=y(),A=S?v.pageStep(S):1;return g(t.key==="PageDown"?o+A:o-A)}case"Enter":t.preventDefault(),i===d?mt?.(i):H(i);return;case" ":if(Rt)break;t.preventDefault(),i!==d&&H(i);return;case"Escape":if(!Wt||d===null)return;t.preventDefault(),t.stopPropagation(),H(null);return}if(t.key.length!==1)return;const Pt=(Rt?D.current.text:"")+t.key.toLowerCase();D.current={text:Pt,at:t.timeStamp},t.preventDefault();const we=S=>Y?Y(m[S],S):e.children[S]?.textContent??"",lt=at.typeaheadIndex(m.length,o,Pt,we);lt>=0&<!==o&&e.children[lt].focus({preventScroll:!0})},V=s.useRef({keys:!1,interactive:!1});s.useEffect(()=>{if(!p||!Re.isDevelopment())return;const t=h.current;!V.current.keys&&w?.some(e=>!e.keyed)&&(V.current.keys=!0,console.warn("Roster Carousel: selection mode needs a key on every child; it's the option's identity.")),!V.current.interactive&&t?.querySelector(`[data-carousel-item] :is(${ut})`)&&(V.current.interactive=!0,console.warn("Roster Carousel: an option holds something that takes focus. In selection mode the keys can't reach it; put it outside the row, or use onActivate."))});const[he,be]=s.useState(0);s.useEffect(()=>{const t=window.setTimeout(()=>be(f.index),150);return()=>window.clearTimeout(t)},[f.index]);const Et=c??se,Tt=t=>Et?`${t} ${Et}`:t,nt=X!=="none"&&f.overflowing!==!1,Se=X==="hover"?Oe:"rst:inline-flex",It=t=>{const e=t==="prev",{className:r,...n}=Bt??{};return a.jsx(Te.Button,{type:"button",variant:"outline",colorScheme:"neutral",size:"icon",...n,"aria-label":e?_t??Tt("Previous"):qt??Tt("Next"),"aria-controls":xt,disabled:e?!f.canScrollPrev:!f.canScrollNext,onClick:()=>z(e?-1:1),ref:e?Z:tt,onFocus:()=>{et.current=t},onBlur:o=>{o.relatedTarget&&(et.current=null)},"data-carousel-arrow":t,className:_.cn("rst:size-11 rst:shrink-0 rst:rounded-full",J==="overlay"&&!$&&"rst:bg-[var(--roster-popover-bg)] rst:elevation-raised rst:hover:bg-[var(--roster-popover-bg)] rst:disabled:invisible",Se,r),children:e?Ft??a.jsx(jt,{direction:"prev"}):$t??a.jsx(jt,{direction:"next"})})},K=nt?It("prev"):null,st=nt?It("next"):null,M=ft&&L?a.jsx("p",{"aria-live":"polite",className:"rst:font-ui rst:m-0 rst:text-sm rst:tabular-nums rst:opacity-75","data-carousel-position":"",children:yt.position(he+1,L)}):null;let ot=null;$?ot=$({prev:K,next:st,position:M}):J==="controls"&&(K||M)&&(ot=a.jsxs("div",{className:_.cn("rst:mb-2 rst:items-center rst:justify-end rst:gap-2",!M&&X==="hover"?De:"rst:flex"),"data-carousel-controls":"",children:[M&&a.jsx("div",{className:"rst:mr-auto",children:M}),K,st]}));const it=zt&&f.overflowing&&(f.canScrollPrev||f.canScrollNext)?`linear-gradient(to right, ${f.canScrollPrev?"transparent, black 48px":"black"}, ${f.canScrollNext?"black calc(100% - 48px), transparent":"black"})`:void 0,U=k===void 0&&P!==void 0,ke={"--rst-cv-gap":dt(q),"--rst-cv-gutter":dt(F),...U?ct.perViewVars(P):{},...it?{maskImage:it,WebkitMaskImage:it}:{}},Nt=s.useMemo(()=>k!==void 0?{width:dt(k)}:U?{width:ct.FLUID_ITEM_WIDTH}:void 0,[k,U]),Ct=_.cn("rst:shrink-0",G==="start"?"rst:snap-start":"rst:snap-center",At&&"rst:snap-always",p&&Me,Gt);return a.jsxs("div",{className:_.cn("rst:min-w-0",Jt),style:Yt,...Qt,"aria-describedby":p?void 0:x,children:[ot,a.jsxs("div",{className:"rst:relative",children:[a.jsx("ul",{ref:h,id:xt,role:p?"listbox":"list","aria-label":c,"aria-labelledby":c?void 0:u,"aria-orientation":p?"horizontal":void 0,"aria-describedby":p?x:void 0,tabIndex:!p&&ie&&f.overflowing?0:void 0,"data-overflowing":f.overflowing?"":void 0,onPointerDown:ae,onPointerMove:ue,onPointerUp:rt,onPointerCancel:rt,onLostPointerCapture:rt,onClickCapture:de,onClick:p?ye:void 0,onKeyDown:p?xe:void 0,onBlur:p?me:void 0,onDragStart:fe,onFocus:ge,style:ke,className:_.cn("rst:m-0 rst:flex rst:list-none rst:overflow-x-auto rst:overflow-y-hidden","rst:gap-(--rst-cv-gap) rst:px-(--rst-cv-gutter) rst:py-1 rst:scroll-px-(--rst-cv-gutter)","rst:overscroll-x-contain",Mt==="mandatory"?"rst:snap-x rst:snap-mandatory":"rst:snap-x rst:snap-proximity",Dt&&"rst:-mx-(--rst-cv-gutter)",!Ht&&"rst:[scrollbar-width:none] rst:[&::-webkit-scrollbar]:hidden",U&&ct.FLUID_CLASSES,"rst:data-overflowing:cursor-grab rst:data-dragging:cursor-grabbing rst:data-dragging:select-none","rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-inset",Xt),children:w?w.map((t,e)=>a.jsx(Ae,{child:t.child,index:e,count:L,selected:t.key===d,active:t.key===T,label:Y?.(t.key,e),style:Nt,className:Ct},t.key)):ht.map((t,e)=>a.jsx("li",{"data-carousel-item":"","aria-label":ft?yt.position(e+1,L):void 0,style:Nt,className:Ct,children:t},s.isValidElement(t)&&t.key!=null?t.key:e))}),!$&&J==="overlay"&&nt&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:start-1 rst:flex rst:items-center",children:a.jsx("div",{className:"rst:pointer-events-auto",children:K})}),a.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:end-1 rst:flex rst:items-center",children:a.jsx("div",{className:"rst:pointer-events-auto",children:st})})]})]})]})});exports.Carousel=Fe;
|
|
@@ -17,6 +17,13 @@ export interface CarouselHandle {
|
|
|
17
17
|
scrollPrev: () => void;
|
|
18
18
|
/** What the next arrow does: on one page. */
|
|
19
19
|
scrollNext: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Moves focus into the row. In selection mode, onto the option a Tab would
|
|
22
|
+
* land on: the selected one, or where the keys last left off. Otherwise onto
|
|
23
|
+
* the first item that can take focus, or the list when it's the tab stop.
|
|
24
|
+
* Scrolls it into view.
|
|
25
|
+
*/
|
|
26
|
+
focus: () => void;
|
|
20
27
|
}
|
|
21
28
|
export interface CarouselArrowsRender {
|
|
22
29
|
/** The previous arrow, or `null` while the row fits and there is nothing to page. */
|
|
@@ -35,7 +42,17 @@ export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>
|
|
|
35
42
|
"aria-label"?: string;
|
|
36
43
|
/** The id of the heading that names the row. */
|
|
37
44
|
"aria-labelledby"?: string;
|
|
38
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* In selection mode, describes the listbox: say how it works, as "Enter
|
|
47
|
+
* selects. Enter again opens it." Otherwise it describes the outer wrapper,
|
|
48
|
+
* as it always has.
|
|
49
|
+
*/
|
|
50
|
+
"aria-describedby"?: string;
|
|
51
|
+
/**
|
|
52
|
+
* One child per item. Each is wrapped in the list item the row snaps to. In
|
|
53
|
+
* selection mode each child is an option, and its React `key` is its
|
|
54
|
+
* identity: give every child one.
|
|
55
|
+
*/
|
|
39
56
|
children: React.ReactNode;
|
|
40
57
|
/**
|
|
41
58
|
* Fixed mode: every item is this wide (a number is px). The row overflows
|
|
@@ -117,14 +134,53 @@ export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>
|
|
|
117
134
|
canScrollPrev: boolean;
|
|
118
135
|
canScrollNext: boolean;
|
|
119
136
|
}) => void;
|
|
137
|
+
/**
|
|
138
|
+
* Selection mode: the row becomes a listbox of options, one of which is
|
|
139
|
+
* selected. One tab stop, the arrow keys, Home, End and the Page keys move
|
|
140
|
+
* between options, type-ahead jumps to one, and Enter or Space selects.
|
|
141
|
+
* Selection doesn't follow focus: arrowing only moves focus. See the README
|
|
142
|
+
* for when to use it. Off by default, and off changes nothing.
|
|
143
|
+
*
|
|
144
|
+
* **Nothing interactive inside an option.** An option is one choice; a link
|
|
145
|
+
* or a button inside it would be a second tab stop the keys can't reach.
|
|
146
|
+
* Put actions outside the row (or use `onActivate`). A development build
|
|
147
|
+
* warns when an option holds something that takes focus.
|
|
148
|
+
*/
|
|
149
|
+
selectable?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Selection mode, controlled: the selected option's `key`, as React stores
|
|
152
|
+
* it (a string), or `null` for none.
|
|
153
|
+
*/
|
|
154
|
+
selectedKey?: string | null;
|
|
155
|
+
/** Selection mode, uncontrolled: the option selected at first. */
|
|
156
|
+
defaultSelectedKey?: string | null;
|
|
157
|
+
/**
|
|
158
|
+
* Selection mode: an option was selected (Enter, Space, a click or a tap on
|
|
159
|
+
* one that isn't), with its key; or the selection was cleared (Escape, with
|
|
160
|
+
* `clearable`), with `null`.
|
|
161
|
+
*/
|
|
162
|
+
onSelectionChange?: (key: string | null) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Selection mode: Enter, or a click or a tap, on the option that's already
|
|
165
|
+
* selected. The second step after choosing, such as opening it.
|
|
166
|
+
*/
|
|
167
|
+
onActivate?: (key: string) => void;
|
|
168
|
+
/** Selection mode: Escape clears the selection. */
|
|
169
|
+
clearable?: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Selection mode: each option's accessible name, which is also the text
|
|
172
|
+
* type-ahead matches. Without it an option is named by its content, and
|
|
173
|
+
* type-ahead reads its text.
|
|
174
|
+
*/
|
|
175
|
+
getOptionLabel?: (key: string, index: number) => string;
|
|
120
176
|
/** Classes for each list item. */
|
|
121
177
|
itemClassName?: string;
|
|
122
178
|
/** Classes for the scrolling list. `className` is the outer wrapper. */
|
|
123
179
|
listClassName?: string;
|
|
124
180
|
}
|
|
125
181
|
/**
|
|
126
|
-
* A sideways row of items: a row of cards, a selection strip, a
|
|
127
|
-
* per view.
|
|
182
|
+
* A sideways row of items: a row of cards, a selection strip (`selectable`), a
|
|
183
|
+
* gallery of one per view.
|
|
128
184
|
*
|
|
129
185
|
* Native first. The row is a real scroll container with CSS scroll snap, so
|
|
130
186
|
* touch, trackpad, Shift with the wheel and focus all scroll it without
|
|
@@ -148,6 +204,12 @@ export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>
|
|
|
148
204
|
*
|
|
149
205
|
* Renders a `ul` named by `aria-label` or `aria-labelledby`, one `li` per
|
|
150
206
|
* child. There is no carousel role and no slide role: nothing rotates.
|
|
207
|
+
*
|
|
208
|
+
* For a strip of choices, one of which is picked, use `selectable`: the row
|
|
209
|
+
* becomes a horizontal listbox with one tab stop, roving focus over its
|
|
210
|
+
* options, and a selection that changes only on Enter, Space or a tap. A row
|
|
211
|
+
* of buttons with `aria-pressed` is as many tab stops as there are buttons,
|
|
212
|
+
* and a screen reader hears a list of unrelated buttons.
|
|
151
213
|
*/
|
|
152
214
|
declare const Carousel: React.ForwardRefExoticComponent<CarouselProps & React.RefAttributes<CarouselHandle>>;
|
|
153
215
|
export { Carousel };
|