@blakesteve/roster 5.13.0 → 5.14.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 CHANGED
@@ -1861,8 +1861,8 @@ Things to know:
1861
1861
 
1862
1862
  ### Carousel
1863
1863
 
1864
- A sideways row: a row of cards, a selection strip, or a gallery of one per
1865
- view. It is a real scroll container with CSS scroll snap, so touch, trackpad,
1864
+ A sideways row: a row of cards, a selection strip (`selectable`, below), or a
1865
+ gallery of one per view. It is a real scroll container with CSS scroll snap, so touch, trackpad,
1866
1866
  Shift with the wheel and keyboard focus all scroll it without script, and every
1867
1867
  item is in the server render. No clones, no virtualization, no loop, and
1868
1868
  nothing moves on its own.
@@ -1895,11 +1895,94 @@ Script adds what a mouse is missing:
1895
1895
 
1896
1896
  A vertical wheel over the row scrolls the page, and sideways overscroll is
1897
1897
  contained. Under reduced motion every scroll the component makes is instant.
1898
- `ref` gives `scrollToIndex(i)`, `scrollPrev()` and `scrollNext()`;
1898
+ `ref` gives `scrollToIndex(i)`, `scrollPrev()`, `scrollNext()` and `focus()`;
1899
1899
  `onIndexChange` reports the first item wholly in view. For a gallery,
1900
1900
  `showPosition` shows "2 of 5", which is also each item's name and is announced
1901
1901
  politely when it changes.
1902
1902
 
1903
+ #### Selection mode
1904
+
1905
+ For a strip of choices, one of which is picked, pass `selectable`. The row
1906
+ becomes a horizontal listbox: one tab stop, the keys moving focus between
1907
+ options, and one option selected. A row of buttons with `aria-pressed` is a tab
1908
+ stop per button (300 options, 300 Tab presses), and a screen reader hears a
1909
+ list of unrelated buttons. Without `selectable` nothing changes.
1910
+
1911
+ ```tsx
1912
+ <Carousel
1913
+ aria-label="Choose a record"
1914
+ aria-describedby="how"
1915
+ selectable
1916
+ selectedKey={chosen}
1917
+ onSelectionChange={setChosen}
1918
+ onActivate={play}
1919
+ clearable
1920
+ itemWidth={150}
1921
+ >
1922
+ {records.map((r) => <RecordCell key={r.id} record={r} selected={r.id === chosen} />)}
1923
+ </Carousel>
1924
+ ```
1925
+
1926
+ - **Tab** lands on the selected option, or the first when nothing is. Leaving
1927
+ the row hands the tab stop back to the selected option.
1928
+ - **Left and Right** move by one, without wrapping; **Home and End** go to the
1929
+ ends; **Page Up and Page Down** move by as many options as are wholly in
1930
+ view.
1931
+ - **Type-ahead:** typing jumps to the next option whose name starts with what
1932
+ was typed. A pause of 700ms starts over, and the same letter again steps on
1933
+ to the next option starting with it.
1934
+ - **Enter or Space selects. Selection doesn't follow focus:** arrowing only
1935
+ moves focus, so whatever a selection sets off (a pan, a fetch, an
1936
+ announcement) happens once, on purpose.
1937
+ - **Enter, a click or a tap on the selected option** calls `onActivate`, the
1938
+ step after choosing: open it, read it, play it. A click or tap on any other
1939
+ option selects it.
1940
+ - **Escape** clears the selection when `clearable` is set, and calls
1941
+ `onSelectionChange(null)`.
1942
+
1943
+ Moving focus with the keys scrolls the option into view, smoothly, or at once
1944
+ under reduced motion; a click doesn't move the row. A selection doesn't scroll
1945
+ the row at all, so a `scrollToIndex` of your own (centering the choice, say)
1946
+ isn't undone. The keyboard-mode tracker is mounted, so the ring shows in Safari
1947
+ after a click too (see [Focus](#focus)).
1948
+
1949
+ **Identity is the child's `key`.** `selectedKey`, `defaultSelectedKey` and both
1950
+ callbacks deal in it, as React stores it: a string. Keys, not positions,
1951
+ because a consumer that filters the row changes every position after the first
1952
+ option it hides. A development build warns about a child without a key.
1953
+ Controlled (`selectedKey` with `onSelectionChange`) or uncontrolled
1954
+ (`defaultSelectedKey`); where focus is stays internal, and `focus()` on the
1955
+ `ref` moves focus onto the tab stop, from a find field's Down Arrow, say.
1956
+
1957
+ **Names.** An option is named by its content, or by `getOptionLabel(key,
1958
+ index)`, which is also the text type-ahead matches. Name the listbox with
1959
+ `aria-label` or `aria-labelledby`, and put a sentence saying how it works in
1960
+ `aria-describedby`.
1961
+
1962
+ **Nothing interactive inside an option.** An option is one choice, reached with
1963
+ the arrow keys; a link or a button inside it is a tab stop the keys skip, and
1964
+ a screen reader can't reach it as anything but text. Put actions outside the
1965
+ row, or behind `onActivate`. A development build warns when an option holds
1966
+ something that takes focus.
1967
+
1968
+ **Filtering.** Render fewer children and `aria-setsize` and `aria-posinset`
1969
+ describe what's shown. The tab stop stays on its option if that's still shown;
1970
+ otherwise it moves to the nearest option still shown, in the order before the
1971
+ change, after it if there is one, else before it. A selection made elsewhere
1972
+ moves the tab stop to the selected option while focus is outside the row, and
1973
+ leaves it where the keys put it while focus is inside.
1974
+
1975
+ **The selected look is yours.** Roster draws the focus ring, an outline 2px out,
1976
+ so a selected look drawn with a ring or a border sits inside it. Draw the
1977
+ selection in the child, from `selectedKey`, or on the option with
1978
+ `itemClassName` and the `aria-selected:` variant.
1979
+
1980
+ **Scale.** Every option is in the DOM, for the keys and for screen readers;
1981
+ draw expensive content lazily inside it. Options are memoized and the keys,
1982
+ clicks and focus are one listener each on the row. Measured at 1,000 options
1983
+ in Chromium: mounting takes about 30ms longer than the default mode, a key
1984
+ moves focus within a millisecond, and the frame after it lands in about 7ms.
1985
+
1903
1986
  ### LiquidTabs
1904
1987
 
1905
1988
  A tab strip with a sliding pill, on the WAI tabs pattern: one tab stop, arrows
@@ -2123,7 +2206,7 @@ function App() {
2123
2206
  | `Breadcrumbs` | Navigation trail; pass `linkComponent` to keep navigation client-side |
2124
2207
  | `CheckboxGroup` | Checkbox set reporting one `string[]`; groups, columns, scroll, error |
2125
2208
  | `CallToAction` | Prominent hero-style CTA block |
2126
- | `Carousel` | Sideways row of items on native scroll snap; mouse drag, paging arrows, no autoplay |
2209
+ | `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
2210
  | `DescriptionList` | Label and value pairs as a real `<dl>`: inline, stacked, or split |
2128
2211
  | `EmptyState` | Zero-data placeholder with icon and action slot; `headingLevel` sets its title's `h1` to `h6` |
2129
2212
  | `ErrorState` | Error display with retry action |
@@ -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&&lt!==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
- /** One child per item. Each is wrapped in the list item the row snaps to. */
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 gallery of one
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 };