@byline/admin 6.1.0 → 6.2.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.
Files changed (97) hide show
  1. package/dist/forms/form-renderer.js +7 -2
  2. package/dist/presentation/tab-strip-drag.d.ts +75 -0
  3. package/dist/presentation/tab-strip-drag.js +25 -0
  4. package/dist/presentation/tab-strip-drag.test.node.d.ts +8 -0
  5. package/dist/presentation/tab-strip-geometry.d.ts +75 -0
  6. package/dist/presentation/tab-strip-geometry.js +36 -0
  7. package/dist/presentation/tab-strip-geometry.test.node.d.ts +8 -0
  8. package/dist/presentation/tabs.d.ts +30 -7
  9. package/dist/presentation/tabs.js +260 -29
  10. package/dist/presentation/tabs.module.js +11 -1
  11. package/dist/presentation/tabs_module.css +166 -5
  12. package/package.json +7 -7
  13. package/src/forms/form-renderer.tsx +10 -3
  14. package/src/presentation/tab-strip-drag.test.node.ts +143 -0
  15. package/src/presentation/tab-strip-drag.ts +114 -0
  16. package/src/presentation/tab-strip-geometry.test.node.ts +254 -0
  17. package/src/presentation/tab-strip-geometry.ts +158 -0
  18. package/src/presentation/tabs.module.css +209 -11
  19. package/src/presentation/tabs.test.tsx +481 -0
  20. package/src/presentation/tabs.tsx +369 -34
  21. package/dist/exports-parity.test.node.js +0 -37
  22. package/dist/fields/array/array-field.jsx +0 -209
  23. package/dist/fields/blocks/blocks-field.jsx +0 -248
  24. package/dist/fields/checkbox/checkbox-field.jsx +0 -24
  25. package/dist/fields/code/code-editor.jsx +0 -211
  26. package/dist/fields/code/code-field.jsx +0 -109
  27. package/dist/fields/column-formatter.jsx +0 -25
  28. package/dist/fields/date-time-formatter.jsx +0 -18
  29. package/dist/fields/datetime/datetime-field.jsx +0 -22
  30. package/dist/fields/draggable-context-menu.jsx +0 -50
  31. package/dist/fields/field-admin.test.node.js +0 -40
  32. package/dist/fields/field-renderer.jsx +0 -136
  33. package/dist/fields/field-services-context.jsx +0 -17
  34. package/dist/fields/file/file-field.jsx +0 -176
  35. package/dist/fields/file/file-upload-field.jsx +0 -89
  36. package/dist/fields/group/group-field.jsx +0 -46
  37. package/dist/fields/image/image-field.jsx +0 -183
  38. package/dist/fields/image/image-upload-field.jsx +0 -157
  39. package/dist/fields/local-date-time.jsx +0 -69
  40. package/dist/fields/locale-badge.jsx +0 -19
  41. package/dist/fields/numerical/numerical-field.jsx +0 -93
  42. package/dist/fields/relation/relation-column-formatter.jsx +0 -74
  43. package/dist/fields/relation/relation-display.jsx +0 -118
  44. package/dist/fields/relation/relation-field.jsx +0 -108
  45. package/dist/fields/relation/relation-many-field.jsx +0 -142
  46. package/dist/fields/relation/relation-picker.jsx +0 -240
  47. package/dist/fields/relation/relation-summary.jsx +0 -45
  48. package/dist/fields/select/select-field.jsx +0 -27
  49. package/dist/fields/sortable-item.jsx +0 -54
  50. package/dist/fields/text/text-field.jsx +0 -79
  51. package/dist/fields/text-area/text-area-field.jsx +0 -79
  52. package/dist/forms/available-locales-reconcile.test.node.js +0 -43
  53. package/dist/forms/available-locales-widget.jsx +0 -63
  54. package/dist/forms/document-actions.jsx +0 -486
  55. package/dist/forms/form-context.jsx +0 -598
  56. package/dist/forms/form-modals.jsx +0 -128
  57. package/dist/forms/form-renderer.jsx +0 -532
  58. package/dist/forms/form-status-display.jsx +0 -69
  59. package/dist/forms/navigation-guard.jsx +0 -68
  60. package/dist/forms/nested-path.test.node.js +0 -115
  61. package/dist/forms/path-widget.jsx +0 -125
  62. package/dist/forms/pending-uploads.test.node.js +0 -19
  63. package/dist/forms/repeating-items.test.node.js +0 -29
  64. package/dist/forms/scheduled-publication-control.jsx +0 -389
  65. package/dist/forms/scheduled-publication-state.test.node.js +0 -121
  66. package/dist/forms/scheduled-publication-time.test.node.js +0 -70
  67. package/dist/forms/status-transitions.test.node.js +0 -76
  68. package/dist/forms/tree-placement-widget.jsx +0 -160
  69. package/dist/forms/upload-executor.test.node.js +0 -368
  70. package/dist/forms/use-form-layout.test.node.js +0 -69
  71. package/dist/modules/admin-account/components/change-password.jsx +0 -169
  72. package/dist/modules/admin-account/components/container.jsx +0 -164
  73. package/dist/modules/admin-account/components/preferences.jsx +0 -148
  74. package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
  75. package/dist/modules/admin-account/components/update.jsx +0 -186
  76. package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
  77. package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
  78. package/dist/modules/admin-roles/components/create.jsx +0 -155
  79. package/dist/modules/admin-roles/components/permissions.jsx +0 -239
  80. package/dist/modules/admin-roles/components/update.jsx +0 -148
  81. package/dist/modules/admin-users/components/create.jsx +0 -206
  82. package/dist/modules/admin-users/components/roles.jsx +0 -126
  83. package/dist/modules/admin-users/components/set-password.jsx +0 -133
  84. package/dist/modules/admin-users/components/update.jsx +0 -215
  85. package/dist/modules/analytics/components/dashboard.jsx +0 -208
  86. package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
  87. package/dist/modules/analytics/components/timeseries.jsx +0 -193
  88. package/dist/modules/auth/components/sign-in-form.jsx +0 -89
  89. package/dist/modules/auth/safe-redirect.test.node.js +0 -40
  90. package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
  91. package/dist/presentation/group.jsx +0 -26
  92. package/dist/presentation/row.jsx +0 -23
  93. package/dist/presentation/tabs.jsx +0 -39
  94. package/dist/services/admin-services-context.jsx +0 -17
  95. package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
  96. package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
  97. package/dist/widgets/status-badge/status-badge.jsx +0 -46
@@ -10,7 +10,7 @@ import { FieldRenderer } from "../fields/field-renderer.js";
10
10
  import { useBylineFieldServices } from "../fields/field-services-context.js";
11
11
  import { AdminGroup } from "../presentation/group.js";
12
12
  import { AdminRow } from "../presentation/row.js";
13
- import { AdminTabs } from "../presentation/tabs.js";
13
+ import { AdminTabs, tabPanelId, tabTriggerId } from "../presentation/tabs.js";
14
14
  import { AvailableLocalesWidget } from "./available-locales-widget.js";
15
15
  import { DocumentActions } from "./document-actions.js";
16
16
  import { FormProvider, useFieldValue, useFormContext } from "./form-context.js";
@@ -284,10 +284,12 @@ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRe
284
284
  const requested = activeTabBySet[set.name] ?? '';
285
285
  const resolvedActive = visibleTabs.length > 0 && !visibleTabs.some((t)=>t.name === requested) ? visibleTabs[0]?.name ?? requested : requested;
286
286
  const activeTab = visibleTabs.find((t)=>t.name === resolvedActive);
287
+ const idBase = `tabset:${set.name}`;
287
288
  return /*#__PURE__*/ jsxs("div", {
288
289
  className: clsx('byline-form-tabset', form_renderer_module.tabset),
289
290
  children: [
290
291
  visibleTabs.length > 0 && /*#__PURE__*/ jsx(AdminTabs, {
292
+ idBase: idBase,
291
293
  tabs: visibleTabs,
292
294
  activeTab: resolvedActive,
293
295
  onChange: (tabName)=>handleTabChange(set.name, tabName),
@@ -295,11 +297,14 @@ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRe
295
297
  className: clsx('byline-form-tabset-tabs', form_renderer_module["tabset-tabs"])
296
298
  }),
297
299
  activeTab && /*#__PURE__*/ jsx("div", {
300
+ role: "tabpanel",
301
+ id: tabPanelId(idBase, activeTab.name),
302
+ "aria-labelledby": tabTriggerId(idBase, activeTab.name),
298
303
  className: clsx('byline-form-tabset-fields', form_renderer_module["tabset-fields"]),
299
304
  children: activeTab.fields.map((name)=>renderItem(name))
300
305
  })
301
306
  ]
302
- }, `tabset:${set.name}`);
307
+ }, idBase);
303
308
  };
304
309
  const busyAnnouncement = isUploading ? t('forms.actions.uploading') : isSubmitting ? t('forms.actions.saving') : '';
305
310
  return /*#__PURE__*/ jsxs(Fragment, {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Pure state for click-and-drag scrolling of the tab strip.
10
+ *
11
+ * The DOM plumbing — pointer capture, the click suppression, the cursor —
12
+ * lives in `tabs.tsx`; the rules it follows are here so they can be tested
13
+ * without a pointer.
14
+ */
15
+ /**
16
+ * How far the pointer must travel before the gesture stops being a click and
17
+ * becomes a drag.
18
+ *
19
+ * Without a threshold every click on a tab would nudge the strip by a pixel or
20
+ * two, and the click suppression that follows a drag would swallow the very
21
+ * clicks that select tabs.
22
+ */
23
+ export declare const DRAG_THRESHOLD_PX = 5;
24
+ /**
25
+ * How long after a drag a click is treated as that drag's own click.
26
+ *
27
+ * A window rather than a consume-once flag, because the click is not
28
+ * guaranteed to arrive: a drag released outside the strip produces none, and
29
+ * a flag left armed would swallow the reader's next deliberate click on a tab.
30
+ * Far longer than the browser's synthetic click takes to arrive, far shorter
31
+ * than anyone can release and deliberately click again.
32
+ */
33
+ export declare const CLICK_SUPPRESSION_MS = 100;
34
+ /** Where a gesture began. An internal part of `DragState`, not a public type. */
35
+ interface DragOrigin {
36
+ /** Pointer x at the moment the button went down. */
37
+ pointerX: number;
38
+ /** The viewport's scroll offset at that same moment. */
39
+ scrollLeft: number;
40
+ }
41
+ export interface DragState {
42
+ origin: DragOrigin;
43
+ /** Whether the threshold has already been crossed. */
44
+ dragging: boolean;
45
+ }
46
+ export interface DragAdvance {
47
+ dragging: boolean;
48
+ /** The offset to scroll to, or `null` while the gesture is still a click. */
49
+ scrollLeft: number | null;
50
+ }
51
+ /**
52
+ * Whether a `pointerdown` should begin a potential drag.
53
+ *
54
+ * Mouse only. Touch and pen already scroll the viewport natively, with
55
+ * momentum and rubber-banding that a hand-rolled drag does not reproduce, so
56
+ * taking the gesture over would trade a good interaction for a worse one.
57
+ */
58
+ export declare function shouldStartDrag({ pointerType, button, overflowing, }: {
59
+ pointerType: string;
60
+ button: number;
61
+ overflowing: boolean;
62
+ }): boolean;
63
+ /**
64
+ * Advance a gesture given the pointer's current x.
65
+ *
66
+ * Returns `scrollLeft: null` while the pointer is still within the threshold,
67
+ * so the caller leaves the viewport alone and lets the click through.
68
+ */
69
+ export declare function advanceDrag({ origin, dragging }: DragState, pointerX: number, maxScroll: number): DragAdvance;
70
+ /**
71
+ * Whether a click arriving now belongs to a drag that has just ended, and
72
+ * should therefore not reach the tab underneath.
73
+ */
74
+ export declare function shouldSuppressClick(dragEndedAt: number | null, now: number): boolean;
75
+ export {};
@@ -0,0 +1,25 @@
1
+ const DRAG_THRESHOLD_PX = 5;
2
+ const CLICK_SUPPRESSION_MS = 100;
3
+ function shouldStartDrag({ pointerType, button, overflowing }) {
4
+ if ('mouse' !== pointerType) return false;
5
+ if (0 !== button) return false;
6
+ return overflowing;
7
+ }
8
+ function advanceDrag({ origin, dragging }, pointerX, maxScroll) {
9
+ const delta = pointerX - origin.pointerX;
10
+ const isDragging = dragging || Math.abs(delta) >= DRAG_THRESHOLD_PX;
11
+ if (false === isDragging) return {
12
+ dragging: false,
13
+ scrollLeft: null
14
+ };
15
+ const next = origin.scrollLeft - delta;
16
+ return {
17
+ dragging: true,
18
+ scrollLeft: Math.min(Math.max(next, 0), maxScroll)
19
+ };
20
+ }
21
+ function shouldSuppressClick(dragEndedAt, now) {
22
+ if (null == dragEndedAt) return false;
23
+ return now - dragEndedAt < CLICK_SUPPRESSION_MS;
24
+ }
25
+ export { CLICK_SUPPRESSION_MS, DRAG_THRESHOLD_PX, advanceDrag, shouldStartDrag, shouldSuppressClick };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export interface StripMeasurements {
9
+ /**
10
+ * Intrinsic content width of the tablist row.
11
+ *
12
+ * The row is laid out at `max-content`, so this is the width the tabs want
13
+ * regardless of how much room the viewport currently gives them. A row that
14
+ * shrank to its container would report the container's width back and the
15
+ * strip could never detect overflow at all.
16
+ */
17
+ rowWidth: number;
18
+ /**
19
+ * Usable width of the strip container.
20
+ *
21
+ * Independent of whether the trigger is rendered — the container is sized by
22
+ * the layout above it, not by its own children.
23
+ */
24
+ containerWidth: number;
25
+ /** Width the overflow trigger occupies when it is rendered. */
26
+ triggerWidth: number;
27
+ /** Gap between the viewport and the trigger. */
28
+ triggerGap: number;
29
+ /** Current horizontal scroll offset of the viewport. */
30
+ scrollLeft: number;
31
+ }
32
+ export interface StripState {
33
+ /** Whether the row needs the scrolling viewport and the overflow trigger. */
34
+ overflowing: boolean;
35
+ /** Width available to the scrolling viewport. */
36
+ viewportWidth: number;
37
+ /** Whether the viewport is scrolled fully left (hides the leading fade). */
38
+ atStart: boolean;
39
+ /** Whether the viewport is scrolled fully right (hides the trailing fade). */
40
+ atEnd: boolean;
41
+ }
42
+ export interface RevealMeasurements {
43
+ viewportWidth: number;
44
+ scrollLeft: number;
45
+ /** The tab's offset from the start of the row. */
46
+ tabOffsetLeft: number;
47
+ tabWidth: number;
48
+ rowWidth: number;
49
+ padding?: number;
50
+ }
51
+ /**
52
+ * Resolve whether the strip overflows, and where it sits within its scroll
53
+ * range.
54
+ *
55
+ * Overflow is decided against the bare container, and deliberately takes no
56
+ * account of the trigger: a row that fits the container fits, and reserving
57
+ * the trigger's width first would summon a menu for tabs that were never cut
58
+ * off. The trigger is subtracted only afterwards, to size the viewport it now
59
+ * shares the container with.
60
+ *
61
+ * That ordering is also what keeps the result stable. The trigger's presence
62
+ * changes the viewport but never `containerWidth` — which the layout above
63
+ * sets — and never `rowWidth`, which the row holds at `max-content`. So the
64
+ * overflow answer cannot feed back into its own inputs.
65
+ */
66
+ export declare function computeStripState({ rowWidth, containerWidth, triggerWidth, triggerGap, scrollLeft, }: StripMeasurements): StripState;
67
+ /**
68
+ * The scroll offset that brings one tab into view, or `null` when the tab is
69
+ * already visible and the viewport should be left untouched.
70
+ *
71
+ * Returning `null` rather than the current offset is the point: this runs on
72
+ * selection and on focus, and writing `scrollLeft` unconditionally would
73
+ * cancel a scroll the reader is in the middle of.
74
+ */
75
+ export declare function computeRevealScroll({ viewportWidth, scrollLeft, tabOffsetLeft, tabWidth, rowWidth, padding, }: RevealMeasurements): number | null;
@@ -0,0 +1,36 @@
1
+ const EDGE_EPSILON = 1;
2
+ const DEFAULT_REVEAL_PADDING = 8;
3
+ function computeStripState({ rowWidth, containerWidth, triggerWidth, triggerGap, scrollLeft }) {
4
+ const measured = containerWidth > 0;
5
+ const overflowing = measured && rowWidth > containerWidth;
6
+ if (false === overflowing) return {
7
+ overflowing: false,
8
+ viewportWidth: Math.max(0, containerWidth),
9
+ atStart: true,
10
+ atEnd: true
11
+ };
12
+ const viewportWidth = Math.max(0, containerWidth - triggerWidth - triggerGap);
13
+ const maxScroll = Math.max(0, rowWidth - viewportWidth);
14
+ return {
15
+ overflowing: true,
16
+ viewportWidth,
17
+ atStart: scrollLeft <= EDGE_EPSILON,
18
+ atEnd: scrollLeft >= maxScroll - EDGE_EPSILON
19
+ };
20
+ }
21
+ function computeRevealScroll({ viewportWidth, scrollLeft, tabOffsetLeft, tabWidth, rowWidth, padding = DEFAULT_REVEAL_PADDING }) {
22
+ const maxScroll = Math.max(0, rowWidth - viewportWidth);
23
+ const clamp = (value)=>Math.min(Math.max(value, 0), maxScroll);
24
+ const leadingEdge = tabOffsetLeft - padding;
25
+ const trailingEdge = tabOffsetLeft + tabWidth + padding;
26
+ let next;
27
+ if (tabWidth + 2 * padding >= viewportWidth) next = leadingEdge;
28
+ else if (leadingEdge < scrollLeft) next = leadingEdge;
29
+ else {
30
+ if (!(trailingEdge > scrollLeft + viewportWidth)) return null;
31
+ next = trailingEdge - viewportWidth;
32
+ }
33
+ const clamped = clamp(next);
34
+ return clamped === scrollLeft ? null : clamped;
35
+ }
36
+ export { computeRevealScroll, computeStripState };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -6,20 +6,43 @@ interface AdminTabsProps {
6
6
  tabs: AdminTabItem[];
7
7
  activeTab: string;
8
8
  onChange: (name: string) => void;
9
+ /**
10
+ * Prefix for the generated tab and panel ids. The consumer that renders the
11
+ * panels builds its panel ids from the same base via `tabPanelId`, which is
12
+ * what associates the two halves for assistive technology.
13
+ */
14
+ idBase: string;
9
15
  /** Error counts keyed by tab name — shows a danger badge when > 0. */
10
16
  errorCounts?: Record<string, number>;
11
17
  className?: string;
12
18
  }
19
+ /** Id of the button for one tab. */
20
+ export declare const tabTriggerId: (idBase: string, name: string) => string;
21
+ /** Id the consumer must put on the panel belonging to one tab. */
22
+ export declare const tabPanelId: (idBase: string, name: string) => string;
13
23
  /**
14
24
  * Tabs navigation bar for admin form layouts.
15
25
  *
16
- * Used by FormRenderer when a CollectionAdminConfig declares a `tabs` array.
17
- * Each tab is a simple button with a bottom-border active indicator.
18
- * Inactive tabs show a subtle hover state. Fully dark-mode aware.
26
+ * Used by FormRenderer when a CollectionAdminConfig declares a `tabs` array,
27
+ * and by the collection history view. The number of tabs a collection can
28
+ * declare is unbounded while the column they sit in is not, so the strip
29
+ * scrolls horizontally and — only once the tabs genuinely exceed the column —
30
+ * grows a menu listing every tab.
19
31
  *
20
- * Stable override handles: `.byline-admin-tabs`, `.byline-admin-tab`,
21
- * `.byline-admin-tab-active`, `.byline-admin-tab-label`,
22
- * `.byline-admin-tab-badge`.
32
+ * The menu is a jump list rather than a list of the tabs that happen to be
33
+ * out of view: its contents stay put as the strip scrolls, and no tab ever
34
+ * changes position in the strip as a result of being selected. Editors learn
35
+ * where a tab lives and it stays there.
36
+ *
37
+ * Scroll position is moved only when the selection changes or a tab takes
38
+ * focus. A reader who scrolls the strip to look at other labels keeps their
39
+ * position through re-measurement, re-render and resize.
40
+ *
41
+ * Stable override handles: `.byline-admin-tabs`, `.byline-admin-tabs-viewport`,
42
+ * `.byline-admin-tablist`, `.byline-admin-tab`, `.byline-admin-tab-active`,
43
+ * `.byline-admin-tab-label`, `.byline-admin-tab-badge`,
44
+ * `.byline-admin-tabs-overflow-trigger`, `.byline-admin-tabs-overflow-menu`,
45
+ * `.byline-admin-tabs-overflow-item`.
23
46
  */
24
- export declare const AdminTabs: ({ tabs, activeTab, onChange, errorCounts, className, }: AdminTabsProps) => import("react").JSX.Element;
47
+ export declare const AdminTabs: ({ tabs, activeTab, onChange, idBase, errorCounts, className, }: AdminTabsProps) => import("react").JSX.Element;
25
48
  export {};
@@ -1,39 +1,270 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
4
  import { useTranslation } from "@byline/i18n/react";
4
- import { Badge } from "@byline/ui/react";
5
+ import { Badge, Dropdown, EllipsisIcon } from "@byline/ui/react";
5
6
  import clsx from "clsx";
7
+ import { advanceDrag, shouldStartDrag, shouldSuppressClick } from "./tab-strip-drag.js";
8
+ import { computeRevealScroll, computeStripState } from "./tab-strip-geometry.js";
6
9
  import tabs_module from "./tabs.module.js";
7
- const AdminTabs = ({ tabs, activeTab, onChange, errorCounts, className })=>{
10
+ const useIsoLayoutEffect = "u" > typeof window ? useLayoutEffect : useEffect;
11
+ const TRIGGER_SIZE_PX = 32;
12
+ const TRIGGER_GAP_PX = 12;
13
+ const INITIAL_STRIP_STATE = {
14
+ overflowing: false,
15
+ viewportWidth: 0,
16
+ atStart: true,
17
+ atEnd: true
18
+ };
19
+ const tabTriggerId = (idBase, name)=>`${idBase}-tab-${name}`;
20
+ const tabPanelId = (idBase, name)=>`${idBase}-panel-${name}`;
21
+ function prefersReducedMotion() {
22
+ if ("u" < typeof window || 'function' != typeof window.matchMedia) return false;
23
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
24
+ }
25
+ const AdminTabs = ({ tabs, activeTab, onChange, idBase, errorCounts, className })=>{
8
26
  const { t } = useTranslation('byline-admin');
9
- return /*#__PURE__*/ jsx("div", {
10
- role: "tablist",
11
- "aria-label": t('presentation.formTabsAriaLabel'),
27
+ const containerRef = useRef(null);
28
+ const viewportRef = useRef(null);
29
+ const rowRef = useRef(null);
30
+ const tabRefs = useRef(new Map());
31
+ const focusAfterChange = useRef(null);
32
+ const [strip, setStrip] = useState(INITIAL_STRIP_STATE);
33
+ const measure = useCallback(()=>{
34
+ const container = containerRef.current;
35
+ const row = rowRef.current;
36
+ if (null == container || null == row) return;
37
+ const next = computeStripState({
38
+ rowWidth: row.scrollWidth,
39
+ containerWidth: container.clientWidth,
40
+ triggerWidth: TRIGGER_SIZE_PX,
41
+ triggerGap: TRIGGER_GAP_PX,
42
+ scrollLeft: viewportRef.current?.scrollLeft ?? 0
43
+ });
44
+ setStrip((prev)=>prev.overflowing === next.overflowing && prev.viewportWidth === next.viewportWidth && prev.atStart === next.atStart && prev.atEnd === next.atEnd ? prev : next);
45
+ }, []);
46
+ useIsoLayoutEffect(()=>{
47
+ measure();
48
+ const container = containerRef.current;
49
+ const row = rowRef.current;
50
+ if (null == container || null == row || "u" < typeof ResizeObserver) return;
51
+ const observer = new ResizeObserver(()=>measure());
52
+ observer.observe(container);
53
+ observer.observe(row);
54
+ return ()=>observer.disconnect();
55
+ }, [
56
+ measure,
57
+ tabs
58
+ ]);
59
+ const reveal = useCallback((name)=>{
60
+ const viewport = viewportRef.current;
61
+ const row = rowRef.current;
62
+ const tab = tabRefs.current.get(name);
63
+ if (null == viewport || null == row || null == tab) return;
64
+ const next = computeRevealScroll({
65
+ viewportWidth: viewport.clientWidth,
66
+ scrollLeft: viewport.scrollLeft,
67
+ tabOffsetLeft: tab.offsetLeft,
68
+ tabWidth: tab.offsetWidth,
69
+ rowWidth: row.scrollWidth
70
+ });
71
+ if (null == next) return;
72
+ viewport.scrollTo?.({
73
+ left: next,
74
+ behavior: prefersReducedMotion() ? 'auto' : 'smooth'
75
+ });
76
+ }, []);
77
+ useEffect(()=>{
78
+ const pending = focusAfterChange.current;
79
+ if (pending === activeTab) {
80
+ focusAfterChange.current = null;
81
+ tabRefs.current.get(activeTab)?.focus({
82
+ preventScroll: true
83
+ });
84
+ }
85
+ reveal(activeTab);
86
+ }, [
87
+ activeTab,
88
+ reveal
89
+ ]);
90
+ const select = useCallback((name, moveFocus)=>{
91
+ if (moveFocus) focusAfterChange.current = name;
92
+ onChange(name);
93
+ }, [
94
+ onChange
95
+ ]);
96
+ const dragRef = useRef(null);
97
+ const dragEndedAtRef = useRef(null);
98
+ const [dragging, setDragging] = useState(false);
99
+ const handlePointerDown = (event)=>{
100
+ const viewport = viewportRef.current;
101
+ if (null == viewport) return;
102
+ if (false === shouldStartDrag({
103
+ pointerType: event.pointerType,
104
+ button: event.button,
105
+ overflowing: strip.overflowing
106
+ })) return;
107
+ dragRef.current = {
108
+ origin: {
109
+ pointerX: event.clientX,
110
+ scrollLeft: viewport.scrollLeft
111
+ },
112
+ dragging: false
113
+ };
114
+ };
115
+ const handlePointerMove = (event)=>{
116
+ const state = dragRef.current;
117
+ const viewport = viewportRef.current;
118
+ const row = rowRef.current;
119
+ if (null == state || null == viewport || null == row) return;
120
+ const maxScroll = Math.max(0, row.scrollWidth - viewport.clientWidth);
121
+ const next = advanceDrag(state, event.clientX, maxScroll);
122
+ if (null == next.scrollLeft) return;
123
+ if (false === state.dragging) {
124
+ state.dragging = true;
125
+ setDragging(true);
126
+ viewport.setPointerCapture?.(event.pointerId);
127
+ }
128
+ viewport.scrollLeft = next.scrollLeft;
129
+ };
130
+ const handlePointerEnd = (event)=>{
131
+ const state = dragRef.current;
132
+ if (null == state) return;
133
+ dragRef.current = null;
134
+ if (false === state.dragging) return;
135
+ setDragging(false);
136
+ viewportRef.current?.releasePointerCapture?.(event.pointerId);
137
+ dragEndedAtRef.current = performance.now();
138
+ };
139
+ const handleClickCapture = (event)=>{
140
+ if (false === shouldSuppressClick(dragEndedAtRef.current, performance.now())) return;
141
+ dragEndedAtRef.current = null;
142
+ event.stopPropagation();
143
+ event.preventDefault();
144
+ };
145
+ const handleKeyDown = (event)=>{
146
+ const current = tabs.findIndex((tab)=>tab.name === activeTab);
147
+ if (current < 0 || 0 === tabs.length) return;
148
+ let nextIndex;
149
+ switch(event.key){
150
+ case 'ArrowRight':
151
+ nextIndex = (current + 1) % tabs.length;
152
+ break;
153
+ case 'ArrowLeft':
154
+ nextIndex = (current - 1 + tabs.length) % tabs.length;
155
+ break;
156
+ case 'Home':
157
+ nextIndex = 0;
158
+ break;
159
+ case 'End':
160
+ nextIndex = tabs.length - 1;
161
+ break;
162
+ default:
163
+ return;
164
+ }
165
+ event.preventDefault();
166
+ const next = tabs[nextIndex];
167
+ if (null == next || next.name === activeTab) return;
168
+ select(next.name, true);
169
+ };
170
+ const sizing = {
171
+ '--byline-admin-tabs-trigger-size': `${TRIGGER_SIZE_PX}px`,
172
+ '--byline-admin-tabs-trigger-gap': `${TRIGGER_GAP_PX}px`
173
+ };
174
+ return /*#__PURE__*/ jsxs("div", {
175
+ ref: containerRef,
176
+ style: sizing,
12
177
  className: clsx('byline-admin-tabs', tabs_module.tabs, className),
13
- children: tabs.map((tab)=>{
14
- const isActive = tab.name === activeTab;
15
- return /*#__PURE__*/ jsx("button", {
16
- type: "button",
17
- role: "tab",
18
- "aria-selected": isActive,
19
- onClick: ()=>onChange(tab.name),
20
- className: clsx('byline-admin-tab', tabs_module.tab, isActive && [
21
- 'byline-admin-tab-active',
22
- tabs_module["tab-active"]
23
- ]),
24
- children: /*#__PURE__*/ jsxs("span", {
25
- className: clsx('byline-admin-tab-label', tabs_module.label),
26
- children: [
27
- tab.label,
28
- (errorCounts?.[tab.name] ?? 0) > 0 && /*#__PURE__*/ jsx(Badge, {
29
- intent: "danger",
30
- className: clsx('byline-admin-tab-badge', tabs_module.badge),
31
- children: errorCounts?.[tab.name]
32
- })
33
- ]
178
+ children: [
179
+ /*#__PURE__*/ jsx("div", {
180
+ ref: viewportRef,
181
+ onScroll: measure,
182
+ onPointerDown: handlePointerDown,
183
+ onPointerMove: handlePointerMove,
184
+ onPointerUp: handlePointerEnd,
185
+ onPointerCancel: handlePointerEnd,
186
+ onClickCapture: handleClickCapture,
187
+ "data-at-start": strip.atStart,
188
+ "data-at-end": strip.atEnd,
189
+ "data-draggable": strip.overflowing,
190
+ "data-dragging": dragging,
191
+ className: clsx('byline-admin-tabs-viewport', tabs_module.viewport),
192
+ children: /*#__PURE__*/ jsx("div", {
193
+ ref: rowRef,
194
+ role: "tablist",
195
+ "aria-label": t('presentation.formTabsAriaLabel'),
196
+ onKeyDown: handleKeyDown,
197
+ className: clsx('byline-admin-tablist', tabs_module.tablist),
198
+ children: tabs.map((tab)=>{
199
+ const isActive = tab.name === activeTab;
200
+ const errorCount = errorCounts?.[tab.name] ?? 0;
201
+ return /*#__PURE__*/ jsx("button", {
202
+ ref: (node)=>{
203
+ if (null == node) tabRefs.current.delete(tab.name);
204
+ else tabRefs.current.set(tab.name, node);
205
+ },
206
+ type: "button",
207
+ role: "tab",
208
+ id: tabTriggerId(idBase, tab.name),
209
+ "aria-controls": tabPanelId(idBase, tab.name),
210
+ "aria-selected": isActive,
211
+ tabIndex: isActive ? 0 : -1,
212
+ onClick: ()=>select(tab.name, false),
213
+ onFocus: ()=>reveal(tab.name),
214
+ className: clsx('byline-admin-tab', tabs_module.tab, isActive && [
215
+ 'byline-admin-tab-active',
216
+ tabs_module["tab-active"]
217
+ ]),
218
+ children: /*#__PURE__*/ jsxs("span", {
219
+ className: clsx('byline-admin-tab-label', tabs_module.label),
220
+ children: [
221
+ tab.label,
222
+ errorCount > 0 && /*#__PURE__*/ jsx(Badge, {
223
+ intent: "danger",
224
+ className: clsx('byline-admin-tab-badge', tabs_module.badge),
225
+ children: errorCount
226
+ })
227
+ ]
228
+ })
229
+ }, tab.name);
230
+ })
34
231
  })
35
- }, tab.name);
36
- })
232
+ }),
233
+ strip.overflowing && /*#__PURE__*/ jsxs(Dropdown.Root, {
234
+ children: [
235
+ /*#__PURE__*/ jsx(Dropdown.Trigger, {
236
+ "aria-label": t('presentation.formTabsAllTabsLabel'),
237
+ className: clsx('byline-admin-tabs-overflow-trigger', tabs_module["overflow-trigger"]),
238
+ children: /*#__PURE__*/ jsx(EllipsisIcon, {
239
+ className: clsx('byline-admin-tabs-overflow-icon', tabs_module["overflow-icon"])
240
+ })
241
+ }),
242
+ /*#__PURE__*/ jsx(Dropdown.Portal, {
243
+ children: /*#__PURE__*/ jsx(Dropdown.Content, {
244
+ sideOffset: 6,
245
+ align: "end",
246
+ className: clsx('byline-admin-tabs-overflow-menu', tabs_module["overflow-menu"]),
247
+ children: tabs.map((tab)=>{
248
+ const errorCount = errorCounts?.[tab.name] ?? 0;
249
+ return /*#__PURE__*/ jsxs(Dropdown.Item, {
250
+ onClick: ()=>select(tab.name, true),
251
+ "data-active": tab.name === activeTab,
252
+ className: clsx('byline-admin-tabs-overflow-item', tabs_module["overflow-item"]),
253
+ children: [
254
+ tab.label,
255
+ errorCount > 0 && /*#__PURE__*/ jsx(Badge, {
256
+ intent: "danger",
257
+ className: clsx('byline-admin-tab-badge', tabs_module.badge),
258
+ children: errorCount
259
+ })
260
+ ]
261
+ }, tab.name);
262
+ })
263
+ })
264
+ })
265
+ ]
266
+ })
267
+ ]
37
268
  });
38
269
  };
39
- export { AdminTabs };
270
+ export { AdminTabs, tabPanelId, tabTriggerId };
@@ -1,10 +1,20 @@
1
1
  import "./tabs_module.css";
2
2
  const tabs_module = {
3
3
  tabs: "tabs-AUVgW2",
4
+ viewport: "viewport-Q34w7N",
5
+ tablist: "tablist-a3JXdm",
4
6
  tab: "tab-Tn7xcQ",
5
7
  "tab-active": "tab-active-cGf5N5",
6
8
  tabActive: "tab-active-cGf5N5",
7
9
  label: "label-PvJgbb",
8
- badge: "badge-NJVOEv"
10
+ badge: "badge-NJVOEv",
11
+ "overflow-trigger": "overflow-trigger-sQPQE8",
12
+ overflowTrigger: "overflow-trigger-sQPQE8",
13
+ "overflow-icon": "overflow-icon-E4aR_Z",
14
+ overflowIcon: "overflow-icon-E4aR_Z",
15
+ "overflow-menu": "overflow-menu-BkkmwS",
16
+ overflowMenu: "overflow-menu-BkkmwS",
17
+ "overflow-item": "overflow-item-ahgTA4",
18
+ overflowItem: "overflow-item-ahgTA4"
9
19
  };
10
20
  export default tabs_module;