@charcuterie/ui 4.18.0 → 4.18.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"PanelTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgBhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,SAAS,GAAI,2EAQvB,cAAc,KAAG,SA6KnB,CAAA"}
1
+ {"version":3,"file":"PanelTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAiBhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,SAAS,GAAI,2EAQvB,cAAc,KAAG,SAoLnB,CAAA"}
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef } from "react";
4
4
  import { toClassName } from "../toClassName.js";
5
5
  import { TabTrigger } from "./TabTrigger.js";
6
6
  import { toTabListClass } from "./tabStyles.js";
7
+ import { useRevealCurrentTab } from "./useRevealCurrentTab.js";
7
8
  const ORIENTATION_KEYS = {
8
9
  horizontal: {
9
10
  next: "ArrowRight",
@@ -95,6 +96,7 @@ export const PanelTabs = ({ activation = "automatic", activeKey, className, labe
95
96
  * `selectTabIndex` itself and got fixed there.
96
97
  */
97
98
  const shownKey = selection.selectedValue ?? selection.pendingValue;
99
+ const revealRef = useRevealCurrentTab(shownKey, orientation);
98
100
  const selectTab = useCallback((key) => {
99
101
  // Both, because a pointer press moves focus *and* chooses.
100
102
  // Only selecting would leave `automatic`'s effect below to
@@ -146,7 +148,10 @@ export const PanelTabs = ({ activation = "automatic", activeKey, className, labe
146
148
  keyEvent.preventDefault();
147
149
  select(activeValue);
148
150
  }
149
- }, ref: tablistRef, role: "tablist", children: tabs.map((tab, index) => (_jsx(TabTrigger, { id: `${baseId}-tab-${index}`, isSelected: shownKey === tab.key, onSelect: selectTab, panelId: `${baseId}-panel-${index}`, registerFocus: focus.register, orientation: orientation, registerSelection: selection.register, tab: tab,
151
+ }, ref: (element) => {
152
+ tablistRef.current = element;
153
+ revealRef.current = element;
154
+ }, role: "tablist", children: tabs.map((tab, index) => (_jsx(TabTrigger, { id: `${baseId}-tab-${index}`, isSelected: shownKey === tab.key, onSelect: selectTab, panelId: `${baseId}-panel-${index}`, registerFocus: focus.register, orientation: orientation, registerSelection: selection.register, tab: tab,
150
155
  // The roving-tabindex rule read from the core rather
151
156
  // than restated here: exactly one member is in the tab
152
157
  // order, so Tab enters and leaves the bar once while
@@ -1 +1 @@
1
- {"version":3,"file":"PanelTabs.js","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PanelTabs.js","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,cAAc,EACd,cAAc,EACd,eAAe,EACf,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAkB,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,MAAM,gBAAgB,GAGlB;IACF,UAAU,EAAE;QACV,IAAI,EAAE,YAAY;QAClB,QAAQ,EAAE,WAAW;KACtB;IACD,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE;CACrD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EACxB,UAAU,GAAG,WAAW,EACxB,SAAS,EACT,SAAS,EACT,KAAK,EACL,QAAQ,EACR,WAAW,GAAG,YAAY,EAC1B,IAAI,GACW,EAAa,EAAE;IAC9B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,UAAU,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IAE/C,MAAM,WAAW,GAAG,MAAM,CACxB,IAAI,GAAG,EAA6B,CACrC,CAAA;IAED,MAAM,UAAU,GACd,SAAS;QACT,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG;QACxC,IAAI,CAAA;IAEN,MAAM,SAAS,GAAG,eAAe,CAAS;QACxC,QAAQ;QACR,aAAa,EAAE,UAAU;KAC1B,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,cAAc,CAAS;QACnC,WAAW,EAAE,UAAU;KACxB,CAAC,CAAA;IAEF,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAE5B,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,KAAK,CAAA;IAE7C;;;;;;;;;;;;;;OAcG;IACH,MAAM,QAAQ,GACZ,SAAS,CAAC,aAAa,IAAI,SAAS,CAAC,YAAY,CAAA;IACnD,MAAM,SAAS,GAAG,mBAAmB,CACnC,QAAQ,EACR,WAAW,CACZ,CAAA;IAED,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,GAAW,EAAE,EAAE;QACd,2DAA2D;QAC3D,2DAA2D;QAC3D,mCAAmC;QACnC,cAAc,CAAC,GAAG,CAAC,CAAA;QAEnB,MAAM,CAAC,GAAG,CAAC,CAAA;IACb,CAAC,EACD,CAAC,MAAM,EAAE,cAAc,CAAC,CACzB,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,4DAA4D;QAC5D,8DAA8D;QAC9D,2DAA2D;QAC3D,IACE,UAAU,KAAK,WAAW;YAC1B,WAAW,KAAK,IAAI,EACpB,CAAC;YACD,MAAM,CAAC,WAAW,CAAC,CAAA;QACrB,CAAC;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAErC,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAA;QAElC,IACE,CAAC,OAAO;YACR,WAAW,KAAK,IAAI;YACpB,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,EACzC,CAAC;YACD,OAAM;QACR,CAAC;QAED,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,CAAA;IAC/C,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAA;IAEjB,MAAM,IAAI,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAA;IAE1C,OAAO,CACL,eACE,SAAS,EAAE,WAAW,CACpB,qBAAqB,EACrB,SAAS,CACV,aAED,4BACc,KAAK,sBACC,WAAW,EAC7B,SAAS,EAAE,cAAc,CAAC,WAAW,CAAC,EACtC,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;oBACtB,MAAM,QAAQ,GAA+B;wBAC3C,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI;wBACvB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,QAAQ;wBAC/B,GAAG,EAAE,KAAK,CAAC,IAAI;wBACf,IAAI,EAAE,KAAK,CAAC,KAAK;qBAClB,CAAA;oBAED,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;oBAEtC,IAAI,OAAO,EAAE,CAAC;wBACZ,uDAAuD;wBACvD,sDAAsD;wBACtD,yCAAyC;wBACzC,QAAQ,CAAC,cAAc,EAAE,CAAA;wBAEzB,OAAO,EAAE,CAAA;wBAET,OAAM;oBACR,CAAC;oBAED,yDAAyD;oBACzD,wDAAwD;oBACxD,6BAA6B;oBAC7B,IACE,CAAC,QAAQ,CAAC,GAAG,KAAK,OAAO;wBACvB,QAAQ,CAAC,GAAG,KAAK,GAAG,CAAC;wBACvB,WAAW,KAAK,IAAI,EACpB,CAAC;wBACD,QAAQ,CAAC,cAAc,EAAE,CAAA;wBAEzB,MAAM,CAAC,WAAW,CAAC,CAAA;oBACrB,CAAC;gBACH,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;oBACf,UAAU,CAAC,OAAO,GAAG,OAAO,CAAA;oBAC5B,SAAS,CAAC,OAAO,GAAG,OAAO,CAAA;gBAC7B,CAAC,EACD,IAAI,EAAC,SAAS,YAEb,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CACxB,KAAC,UAAU,IACT,EAAE,EAAE,GAAG,MAAM,QAAQ,KAAK,EAAE,EAC5B,UAAU,EAAE,QAAQ,KAAK,GAAG,CAAC,GAAG,EAEhC,QAAQ,EAAE,SAAS,EACnB,OAAO,EAAE,GAAG,MAAM,UAAU,KAAK,EAAE,EACnC,aAAa,EAAE,KAAK,CAAC,QAAQ,EAC7B,WAAW,EAAE,WAAW,EACxB,iBAAiB,EAAE,SAAS,CAAC,QAAQ,EACrC,GAAG,EAAE,GAAG;oBACR,qDAAqD;oBACrD,uDAAuD;oBACvD,qDAAqD;oBACrD,iCAAiC;oBACjC,QAAQ,EAAE,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,EACxC,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE;wBAC7B,IAAI,OAAO,EAAE,CAAC;4BACZ,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;wBACvC,CAAC;6BAAM,CAAC;4BACN,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;wBACjC,CAAC;oBACH,CAAC,IAlBI,GAAG,CAAC,GAAG,CAmBZ,CACH,CAAC,GACE,EAEL,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CACxB,iCACmB,GAAG,MAAM,QAAQ,KAAK,EAAE,EACzC,MAAM,EAAE,QAAQ,KAAK,GAAG,CAAC,GAAG,EAC5B,EAAE,EAAE,GAAG,MAAM,UAAU,KAAK,EAAE,EAE9B,IAAI,EAAC,UAAU,YAEd,GAAG,CAAC,OAAO,IAHP,GAAG,CAAC,GAAG,CAIR,CACP,CAAC,IACE,CACP,CAAA;AACH,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"RoutedTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAG/C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,GAAI,sDAMxB,aAAa,KAAG,SAyBlB,CAAA"}
1
+ {"version":3,"file":"RoutedTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,GAAI,sDAMxB,aAAa,KAAG,SA8BlB,CAAA"}
@@ -3,6 +3,7 @@ import { getNavItemKey, resolveActiveKey, } from "../Nav/navItems.js";
3
3
  import { toClassName } from "../toClassName.js";
4
4
  import { TabLink } from "./TabLink.js";
5
5
  import { toTabListClass } from "./tabStyles.js";
6
+ import { useRevealCurrentTab } from "./useRevealCurrentTab.js";
6
7
  /**
7
8
  * The routed bar: a named `<nav>` of real links wearing the tab
8
9
  * paint.
@@ -20,7 +21,8 @@ import { toTabListClass } from "./tabStyles.js";
20
21
  */
21
22
  export const RoutedTabs = ({ activeHref, className, label, orientation = "horizontal", tabs, }) => {
22
23
  const activeKey = resolveActiveKey(tabs, activeHref);
23
- return (_jsx("nav", { "aria-label": label, className: toClassName(toTabListClass(orientation), className), children: tabs.map((tab) => {
24
+ const containerRef = useRevealCurrentTab(activeKey, orientation);
25
+ return (_jsx("nav", { ref: containerRef, "aria-label": label, className: toClassName(toTabListClass(orientation), className), children: tabs.map((tab) => {
24
26
  const key = getNavItemKey(tab);
25
27
  return (_jsx(TabLink, { isCurrent: key === activeKey, item: tab, orientation: orientation }, key));
26
28
  }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"RoutedTabs.js","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":";AAEA,OAAO,EACL,aAAa,EACb,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAe,CAAA;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/C;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,UAAU,EACV,SAAS,EACT,KAAK,EACL,WAAW,GAAG,YAAY,EAC1B,IAAI,GACU,EAAa,EAAE;IAC7B,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IAEpD,OAAO,CACL,4BACc,KAAK,EACjB,SAAS,EAAE,WAAW,CACpB,cAAc,CAAC,WAAW,CAAC,EAC3B,SAAS,CACV,YAEA,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAChB,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;YAE9B,OAAO,CACL,KAAC,OAAO,IACN,SAAS,EAAE,GAAG,KAAK,SAAS,EAC5B,IAAI,EAAE,GAAG,EAET,WAAW,EAAE,WAAW,IADnB,GAAG,CAER,CACH,CAAA;QACH,CAAC,CAAC,GACE,CACP,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"RoutedTabs.js","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":";AAEA,OAAO,EACL,aAAa,EACb,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAe,CAAA;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,UAAU,EACV,SAAS,EACT,KAAK,EACL,WAAW,GAAG,YAAY,EAC1B,IAAI,GACU,EAAa,EAAE;IAC7B,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IACpD,MAAM,YAAY,GAAG,mBAAmB,CACtC,SAAS,EACT,WAAW,CACZ,CAAA;IAED,OAAO,CACL,cACE,GAAG,EAAE,YAAY,gBACL,KAAK,EACjB,SAAS,EAAE,WAAW,CACpB,cAAc,CAAC,WAAW,CAAC,EAC3B,SAAS,CACV,YAEA,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAChB,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;YAE9B,OAAO,CACL,KAAC,OAAO,IACN,SAAS,EAAE,GAAG,KAAK,SAAS,EAC5B,IAAI,EAAE,GAAG,EAET,WAAW,EAAE,WAAW,IADnB,GAAG,CAER,CACH,CAAA;QACH,CAAC,CAAC,GACE,CACP,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { TabsOrientation } from "./tabItems.ts";
2
+ /** Reveal within the tab bar only; never scroll an ancestor or move focus. */
3
+ export declare function useRevealCurrentTab(currentKey: string | null, orientation: TabsOrientation): import("react").RefObject<HTMLElement | null>;
4
+ //# sourceMappingURL=useRevealCurrentTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRevealCurrentTab.d.ts","sourceRoot":"","sources":["../../src/Tabs/useRevealCurrentTab.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,WAAW,EAAE,eAAe,iDAsF7B"}
@@ -0,0 +1,85 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+ /** Reveal within the tab bar only; never scroll an ancestor or move focus. */
3
+ export function useRevealCurrentTab(currentKey, orientation) {
4
+ const containerRef = useRef(null);
5
+ useLayoutEffect(() => {
6
+ const container = containerRef.current;
7
+ if (!container || currentKey === null)
8
+ return;
9
+ let isActive = true;
10
+ const reveal = () => {
11
+ if (!isActive)
12
+ return;
13
+ // In manual panel mode, the focused tab can differ from the selected tab.
14
+ const focused = container.ownerDocument.activeElement;
15
+ const current = focused instanceof HTMLElement &&
16
+ container.contains(focused) &&
17
+ focused.matches('a, [role="tab"]')
18
+ ? focused
19
+ : container.querySelector('[aria-current="page"], [aria-selected="true"]');
20
+ if (!current)
21
+ return;
22
+ const viewport = container.getBoundingClientRect();
23
+ const target = current.getBoundingClientRect();
24
+ const start = orientation === "horizontal"
25
+ ? target.left
26
+ : target.top;
27
+ const end = orientation === "horizontal"
28
+ ? target.right
29
+ : target.bottom;
30
+ const viewStart = orientation === "horizontal"
31
+ ? viewport.left
32
+ : viewport.top;
33
+ const viewEnd = orientation === "horizontal"
34
+ ? viewport.right
35
+ : viewport.bottom;
36
+ const hasOversize = end - start > viewEnd - viewStart;
37
+ const isRtl = orientation === "horizontal" &&
38
+ getComputedStyle(container).direction === "rtl";
39
+ const delta = hasOversize
40
+ ? isRtl
41
+ ? end - viewEnd
42
+ : start - viewStart
43
+ : start < viewStart
44
+ ? start - viewStart
45
+ : end > viewEnd
46
+ ? end - viewEnd
47
+ : 0;
48
+ if (orientation === "horizontal")
49
+ container.scrollLeft += delta;
50
+ else
51
+ container.scrollTop += delta;
52
+ };
53
+ const frame = { id: 0 };
54
+ const scheduleReveal = () => {
55
+ cancelAnimationFrame(frame.id);
56
+ frame.id = requestAnimationFrame(reveal);
57
+ };
58
+ const sizes = new ResizeObserver(scheduleReveal);
59
+ const observeItems = () => {
60
+ sizes.disconnect();
61
+ sizes.observe(container);
62
+ for (const child of container.children)
63
+ sizes.observe(child);
64
+ reveal();
65
+ };
66
+ const contents = new MutationObserver(observeItems);
67
+ contents.observe(container, {
68
+ childList: true,
69
+ subtree: true,
70
+ characterData: true,
71
+ });
72
+ container.addEventListener("focusin", reveal);
73
+ observeItems();
74
+ void container.ownerDocument.fonts.ready.then(reveal);
75
+ return () => {
76
+ isActive = false;
77
+ cancelAnimationFrame(frame.id);
78
+ sizes.disconnect();
79
+ contents.disconnect();
80
+ container.removeEventListener("focusin", reveal);
81
+ };
82
+ }, [currentKey, orientation]);
83
+ return containerRef;
84
+ }
85
+ //# sourceMappingURL=useRevealCurrentTab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRevealCurrentTab.js","sourceRoot":"","sources":["../../src/Tabs/useRevealCurrentTab.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcuterie/ui",
3
- "version": "4.18.0",
3
+ "version": "4.18.1",
4
4
  "description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -6,12 +6,12 @@ import {
6
6
  } from "@charcuterie/logic"
7
7
  import type { ReactNode } from "react"
8
8
  import { useCallback, useEffect, useRef } from "react"
9
-
10
9
  import { toClassName } from "../toClassName.ts"
11
10
  import type { TabsPanelProps } from "./Tabs.tsx"
12
11
  import { TabTrigger } from "./TabTrigger.tsx"
13
12
  import type { TabsOrientation } from "./tabItems.ts"
14
13
  import { toTabListClass } from "./tabStyles.ts"
14
+ import { useRevealCurrentTab } from "./useRevealCurrentTab.ts"
15
15
 
16
16
  const ORIENTATION_KEYS: Record<
17
17
  TabsOrientation,
@@ -128,6 +128,10 @@ export const PanelTabs = ({
128
128
  */
129
129
  const shownKey =
130
130
  selection.selectedValue ?? selection.pendingValue
131
+ const revealRef = useRevealCurrentTab(
132
+ shownKey,
133
+ orientation,
134
+ )
131
135
 
132
136
  const selectTab = useCallback(
133
137
  (key: string) => {
@@ -214,7 +218,10 @@ export const PanelTabs = ({
214
218
  select(activeValue)
215
219
  }
216
220
  }}
217
- ref={tablistRef}
221
+ ref={(element) => {
222
+ tablistRef.current = element
223
+ revealRef.current = element
224
+ }}
218
225
  role="tablist"
219
226
  >
220
227
  {tabs.map((tab, index) => (
@@ -8,6 +8,7 @@ import { toClassName } from "../toClassName.ts"
8
8
  import { TabLink } from "./TabLink.tsx"
9
9
  import type { TabsLinkProps } from "./Tabs.tsx"
10
10
  import { toTabListClass } from "./tabStyles.ts"
11
+ import { useRevealCurrentTab } from "./useRevealCurrentTab.ts"
11
12
 
12
13
  /**
13
14
  * The routed bar: a named `<nav>` of real links wearing the tab
@@ -32,9 +33,14 @@ export const RoutedTabs = ({
32
33
  tabs,
33
34
  }: TabsLinkProps): ReactNode => {
34
35
  const activeKey = resolveActiveKey(tabs, activeHref)
36
+ const containerRef = useRevealCurrentTab(
37
+ activeKey,
38
+ orientation,
39
+ )
35
40
 
36
41
  return (
37
42
  <nav
43
+ ref={containerRef}
38
44
  aria-label={label}
39
45
  className={toClassName(
40
46
  toTabListClass(orientation),
package/src/Tabs/Tabs.mdx CHANGED
@@ -194,3 +194,11 @@ bar below and the underline stays put until Enter: focus has moved to a tab that
194
194
  selected, which is a state a three-kind model cannot represent at all.
195
195
 
196
196
  <Canvas of={TabsStories.Manual} />
197
+
198
+
199
+ ## Current tabs in a narrow bar
200
+
201
+ An overflowing bar keeps its current tab in view on first render, route changes,
202
+ and reflow. It scrolls only itself, without moving focus or the surrounding page.
203
+ Manual panel activation keeps the focused tab visible even while another panel
204
+ remains selected. A reader can still scroll to other tabs.
@@ -1,13 +1,16 @@
1
1
  import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
2
2
  import type { Meta, StoryObj } from "@storybook/react"
3
3
 
4
+ import { useState } from "react"
4
5
  import { toStoryChoice } from "../argTypes.storyHelpers.ts"
5
6
  import { Badge } from "../Badge/Badge.tsx"
7
+ import { Button } from "../Button/Button.tsx"
6
8
  import {
7
9
  ContainerBoard,
8
10
  StoryCell,
9
11
  StoryGrid,
10
12
  } from "../board.storyHelpers.tsx"
13
+ import { Field } from "../Field/Field.tsx"
11
14
  import { ProgressBar } from "../ProgressBar/ProgressBar.tsx"
12
15
  import { Tabs } from "./Tabs.tsx"
13
16
  import type { TabItem, TabLinkItem } from "./tabItems.ts"
@@ -319,3 +322,73 @@ export const RoutedAllStates: Story = {
319
322
  />
320
323
  ),
321
324
  }
325
+
326
+ const OVERFLOW_ROUTES: TabLinkItem[] = [
327
+ { href: "/sections/activity", label: "Recent activity" },
328
+ { href: "/sections/files", label: "Saved files" },
329
+ { href: "/sections/builds", label: "Prepared builds" },
330
+ { href: "/sections/reports", label: "Reports" },
331
+ ]
332
+ export const CurrentTabVisible: Story = {
333
+ args: { label: "Sections", tabs: BAY_TABS },
334
+ render: () => {
335
+ const [href, setHref] = useState("/sections/reports")
336
+ const [inlineSize, setInlineSize] = useState(240)
337
+ return (
338
+ <div className="flex flex-col gap-4">
339
+ <Field label="Available width">
340
+ <input
341
+ type="number"
342
+ min={100}
343
+ max={500}
344
+ value={inlineSize}
345
+ onChange={(event) =>
346
+ setInlineSize(
347
+ Number(event.currentTarget.value),
348
+ )
349
+ }
350
+ />
351
+ </Field>
352
+ <div className="flex flex-wrap gap-2">
353
+ <Button
354
+ onClick={() => setHref("/sections/activity")}
355
+ >
356
+ Show activity
357
+ </Button>
358
+ <Button
359
+ onClick={() => setHref("/sections/reports")}
360
+ >
361
+ Show reports
362
+ </Button>
363
+ </div>
364
+ <div style={{ inlineSize, maxInlineSize: "100%" }}>
365
+ <Tabs
366
+ label="Current routed section"
367
+ activeHref={href}
368
+ tabs={OVERFLOW_ROUTES}
369
+ />
370
+ </div>
371
+ <div
372
+ dir="rtl"
373
+ style={{ inlineSize: 240, maxInlineSize: "100%" }}
374
+ >
375
+ <Tabs
376
+ label="Current RTL section"
377
+ activeHref="/sections/reports"
378
+ tabs={OVERFLOW_ROUTES}
379
+ />
380
+ </div>
381
+ <div
382
+ style={{ inlineSize: 150, maxInlineSize: "100%" }}
383
+ >
384
+ <Tabs
385
+ label="Current manual panel"
386
+ activation="manual"
387
+ activeKey="disc"
388
+ tabs={BAY_TABS}
389
+ />
390
+ </div>
391
+ </div>
392
+ )
393
+ },
394
+ }
@@ -16,6 +16,7 @@ const {
16
16
  Routed,
17
17
  RoutedAllStates,
18
18
  RoutedAllVariants,
19
+ CurrentTabVisible,
19
20
  } = composeStories(stories)
20
21
 
21
22
  test("selecting a tab wires the panel back to it", async () => {
@@ -450,3 +451,87 @@ test("a routed tab and a panel tab are painted identically", async () => {
450
451
  )
451
452
  }
452
453
  })
454
+
455
+ test("current routed and panel tabs remain visible without moving focus", async () => {
456
+ const { canvas } = await mountStory(CurrentTabVisible)
457
+ const assertVisible = async (
458
+ bar: HTMLElement,
459
+ selector: string,
460
+ ) => {
461
+ await waitFor(() => {
462
+ const selected =
463
+ bar.querySelector<HTMLElement>(selector)
464
+ expect(selected).not.toBeNull()
465
+ const item = selected?.getBoundingClientRect()
466
+ const bounds = bar.getBoundingClientRect()
467
+ expect(
468
+ item?.left ?? -Infinity,
469
+ ).toBeGreaterThanOrEqual(bounds.left - 1)
470
+ expect(item?.right ?? Infinity).toBeLessThanOrEqual(
471
+ bounds.right + 1,
472
+ )
473
+ })
474
+ }
475
+ const routed = expectAgentDrivable(canvas, {
476
+ role: "navigation",
477
+ name: "Current routed section",
478
+ })
479
+ const rtl = expectAgentDrivable(canvas, {
480
+ role: "navigation",
481
+ name: "Current RTL section",
482
+ })
483
+ const panel = expectAgentDrivable(canvas, {
484
+ role: "tablist",
485
+ name: "Current manual panel",
486
+ })
487
+ await assertVisible(routed, '[aria-current="page"]')
488
+ await assertVisible(rtl, '[aria-current="page"]')
489
+ await assertVisible(panel, '[aria-selected="true"]')
490
+ const width = expectAgentDrivable(canvas, {
491
+ role: "spinbutton",
492
+ name: "Available width",
493
+ })
494
+ await userEvent.clear(width)
495
+ await userEvent.type(width, "160")
496
+ await assertVisible(routed, '[aria-current="page"]')
497
+ await expect(width).toHaveFocus()
498
+ const activity = expectAgentDrivable(canvas, {
499
+ role: "button",
500
+ name: "Show activity",
501
+ })
502
+ await userEvent.click(activity)
503
+ await assertVisible(routed, '[aria-current="page"]')
504
+ await expect(activity).toHaveFocus()
505
+ const reports = expectAgentDrivable(canvas, {
506
+ role: "button",
507
+ name: "Show reports",
508
+ })
509
+ await userEvent.click(reports)
510
+ await assertVisible(routed, '[aria-current="page"]')
511
+ await expect(reports).toHaveFocus()
512
+ await userEvent.clear(width)
513
+ await userEvent.type(width, "80")
514
+ await userEvent.click(activity)
515
+ await waitFor(() => {
516
+ const bounds = routed.getBoundingClientRect()
517
+ const selected = routed
518
+ .querySelector<HTMLElement>('[aria-current="page"]')
519
+ ?.getBoundingClientRect()
520
+ expect(
521
+ selected?.left ?? -Infinity,
522
+ ).toBeGreaterThanOrEqual(bounds.left - 1)
523
+ expect(Math.abs(routed.scrollLeft)).toBeLessThanOrEqual(
524
+ 1,
525
+ )
526
+ })
527
+ const disc = panel.querySelector<HTMLElement>(
528
+ '[aria-selected="true"]',
529
+ )
530
+ disc?.focus()
531
+ await userEvent.keyboard("{ArrowLeft}")
532
+ await assertVisible(panel, ":focus")
533
+ await expect(disc).toHaveAttribute(
534
+ "aria-selected",
535
+ "true",
536
+ )
537
+ })
@@ -0,0 +1,93 @@
1
+ import { useLayoutEffect, useRef } from "react"
2
+ import type { TabsOrientation } from "./tabItems.ts"
3
+
4
+ /** Reveal within the tab bar only; never scroll an ancestor or move focus. */
5
+ export function useRevealCurrentTab(
6
+ currentKey: string | null,
7
+ orientation: TabsOrientation,
8
+ ) {
9
+ const containerRef = useRef<HTMLElement>(null)
10
+ useLayoutEffect(() => {
11
+ const container = containerRef.current
12
+ if (!container || currentKey === null) return
13
+ let isActive = true
14
+ const reveal = () => {
15
+ if (!isActive) return
16
+ // In manual panel mode, the focused tab can differ from the selected tab.
17
+ const focused = container.ownerDocument.activeElement
18
+ const current =
19
+ focused instanceof HTMLElement &&
20
+ container.contains(focused) &&
21
+ focused.matches('a, [role="tab"]')
22
+ ? focused
23
+ : container.querySelector<HTMLElement>(
24
+ '[aria-current="page"], [aria-selected="true"]',
25
+ )
26
+ if (!current) return
27
+ const viewport = container.getBoundingClientRect()
28
+ const target = current.getBoundingClientRect()
29
+ const start =
30
+ orientation === "horizontal"
31
+ ? target.left
32
+ : target.top
33
+ const end =
34
+ orientation === "horizontal"
35
+ ? target.right
36
+ : target.bottom
37
+ const viewStart =
38
+ orientation === "horizontal"
39
+ ? viewport.left
40
+ : viewport.top
41
+ const viewEnd =
42
+ orientation === "horizontal"
43
+ ? viewport.right
44
+ : viewport.bottom
45
+ const hasOversize = end - start > viewEnd - viewStart
46
+ const isRtl =
47
+ orientation === "horizontal" &&
48
+ getComputedStyle(container).direction === "rtl"
49
+ const delta = hasOversize
50
+ ? isRtl
51
+ ? end - viewEnd
52
+ : start - viewStart
53
+ : start < viewStart
54
+ ? start - viewStart
55
+ : end > viewEnd
56
+ ? end - viewEnd
57
+ : 0
58
+ if (orientation === "horizontal")
59
+ container.scrollLeft += delta
60
+ else container.scrollTop += delta
61
+ }
62
+ const frame = { id: 0 }
63
+ const scheduleReveal = () => {
64
+ cancelAnimationFrame(frame.id)
65
+ frame.id = requestAnimationFrame(reveal)
66
+ }
67
+ const sizes = new ResizeObserver(scheduleReveal)
68
+ const observeItems = () => {
69
+ sizes.disconnect()
70
+ sizes.observe(container)
71
+ for (const child of container.children)
72
+ sizes.observe(child)
73
+ reveal()
74
+ }
75
+ const contents = new MutationObserver(observeItems)
76
+ contents.observe(container, {
77
+ childList: true,
78
+ subtree: true,
79
+ characterData: true,
80
+ })
81
+ container.addEventListener("focusin", reveal)
82
+ observeItems()
83
+ void container.ownerDocument.fonts.ready.then(reveal)
84
+ return () => {
85
+ isActive = false
86
+ cancelAnimationFrame(frame.id)
87
+ sizes.disconnect()
88
+ contents.disconnect()
89
+ container.removeEventListener("focusin", reveal)
90
+ }
91
+ }, [currentKey, orientation])
92
+ return containerRef
93
+ }