@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.
- package/dist/forms/form-renderer.js +7 -2
- package/dist/presentation/tab-strip-drag.d.ts +75 -0
- package/dist/presentation/tab-strip-drag.js +25 -0
- package/dist/presentation/tab-strip-drag.test.node.d.ts +8 -0
- package/dist/presentation/tab-strip-geometry.d.ts +75 -0
- package/dist/presentation/tab-strip-geometry.js +36 -0
- package/dist/presentation/tab-strip-geometry.test.node.d.ts +8 -0
- package/dist/presentation/tabs.d.ts +30 -7
- package/dist/presentation/tabs.js +260 -29
- package/dist/presentation/tabs.module.js +11 -1
- package/dist/presentation/tabs_module.css +166 -5
- package/package.json +7 -7
- package/src/forms/form-renderer.tsx +10 -3
- package/src/presentation/tab-strip-drag.test.node.ts +143 -0
- package/src/presentation/tab-strip-drag.ts +114 -0
- package/src/presentation/tab-strip-geometry.test.node.ts +254 -0
- package/src/presentation/tab-strip-geometry.ts +158 -0
- package/src/presentation/tabs.module.css +209 -11
- package/src/presentation/tabs.test.tsx +481 -0
- package/src/presentation/tabs.tsx +369 -34
- package/dist/exports-parity.test.node.js +0 -37
- package/dist/fields/array/array-field.jsx +0 -209
- package/dist/fields/blocks/blocks-field.jsx +0 -248
- package/dist/fields/checkbox/checkbox-field.jsx +0 -24
- package/dist/fields/code/code-editor.jsx +0 -211
- package/dist/fields/code/code-field.jsx +0 -109
- package/dist/fields/column-formatter.jsx +0 -25
- package/dist/fields/date-time-formatter.jsx +0 -18
- package/dist/fields/datetime/datetime-field.jsx +0 -22
- package/dist/fields/draggable-context-menu.jsx +0 -50
- package/dist/fields/field-admin.test.node.js +0 -40
- package/dist/fields/field-renderer.jsx +0 -136
- package/dist/fields/field-services-context.jsx +0 -17
- package/dist/fields/file/file-field.jsx +0 -176
- package/dist/fields/file/file-upload-field.jsx +0 -89
- package/dist/fields/group/group-field.jsx +0 -46
- package/dist/fields/image/image-field.jsx +0 -183
- package/dist/fields/image/image-upload-field.jsx +0 -157
- package/dist/fields/local-date-time.jsx +0 -69
- package/dist/fields/locale-badge.jsx +0 -19
- package/dist/fields/numerical/numerical-field.jsx +0 -93
- package/dist/fields/relation/relation-column-formatter.jsx +0 -74
- package/dist/fields/relation/relation-display.jsx +0 -118
- package/dist/fields/relation/relation-field.jsx +0 -108
- package/dist/fields/relation/relation-many-field.jsx +0 -142
- package/dist/fields/relation/relation-picker.jsx +0 -240
- package/dist/fields/relation/relation-summary.jsx +0 -45
- package/dist/fields/select/select-field.jsx +0 -27
- package/dist/fields/sortable-item.jsx +0 -54
- package/dist/fields/text/text-field.jsx +0 -79
- package/dist/fields/text-area/text-area-field.jsx +0 -79
- package/dist/forms/available-locales-reconcile.test.node.js +0 -43
- package/dist/forms/available-locales-widget.jsx +0 -63
- package/dist/forms/document-actions.jsx +0 -486
- package/dist/forms/form-context.jsx +0 -598
- package/dist/forms/form-modals.jsx +0 -128
- package/dist/forms/form-renderer.jsx +0 -532
- package/dist/forms/form-status-display.jsx +0 -69
- package/dist/forms/navigation-guard.jsx +0 -68
- package/dist/forms/nested-path.test.node.js +0 -115
- package/dist/forms/path-widget.jsx +0 -125
- package/dist/forms/pending-uploads.test.node.js +0 -19
- package/dist/forms/repeating-items.test.node.js +0 -29
- package/dist/forms/scheduled-publication-control.jsx +0 -389
- package/dist/forms/scheduled-publication-state.test.node.js +0 -121
- package/dist/forms/scheduled-publication-time.test.node.js +0 -70
- package/dist/forms/status-transitions.test.node.js +0 -76
- package/dist/forms/tree-placement-widget.jsx +0 -160
- package/dist/forms/upload-executor.test.node.js +0 -368
- package/dist/forms/use-form-layout.test.node.js +0 -69
- package/dist/modules/admin-account/components/change-password.jsx +0 -169
- package/dist/modules/admin-account/components/container.jsx +0 -164
- package/dist/modules/admin-account/components/preferences.jsx +0 -148
- package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
- package/dist/modules/admin-account/components/update.jsx +0 -186
- package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
- package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
- package/dist/modules/admin-roles/components/create.jsx +0 -155
- package/dist/modules/admin-roles/components/permissions.jsx +0 -239
- package/dist/modules/admin-roles/components/update.jsx +0 -148
- package/dist/modules/admin-users/components/create.jsx +0 -206
- package/dist/modules/admin-users/components/roles.jsx +0 -126
- package/dist/modules/admin-users/components/set-password.jsx +0 -133
- package/dist/modules/admin-users/components/update.jsx +0 -215
- package/dist/modules/analytics/components/dashboard.jsx +0 -208
- package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
- package/dist/modules/analytics/components/timeseries.jsx +0 -193
- package/dist/modules/auth/components/sign-in-form.jsx +0 -89
- package/dist/modules/auth/safe-redirect.test.node.js +0 -40
- package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
- package/dist/presentation/group.jsx +0 -26
- package/dist/presentation/row.jsx +0 -23
- package/dist/presentation/tabs.jsx +0 -39
- package/dist/services/admin-services-context.jsx +0 -17
- package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
- package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
- package/dist/widgets/status-badge/status-badge.jsx +0 -46
|
@@ -8,12 +8,47 @@
|
|
|
8
8
|
* Copyright (c) Infonomic Company Limited
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
type CSSProperties,
|
|
13
|
+
type KeyboardEvent,
|
|
14
|
+
type MouseEvent,
|
|
15
|
+
type PointerEvent,
|
|
16
|
+
useCallback,
|
|
17
|
+
useEffect,
|
|
18
|
+
useLayoutEffect,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
} from 'react'
|
|
22
|
+
|
|
11
23
|
import { useTranslation } from '@byline/i18n/react'
|
|
12
|
-
import { Badge } from '@byline/ui/react'
|
|
24
|
+
import { Badge, Dropdown, EllipsisIcon } from '@byline/ui/react'
|
|
13
25
|
import cx from 'clsx'
|
|
14
26
|
|
|
27
|
+
import { advanceDrag, type DragState, shouldStartDrag, shouldSuppressClick } from './tab-strip-drag'
|
|
28
|
+
import { computeRevealScroll, computeStripState, type StripState } from './tab-strip-geometry'
|
|
15
29
|
import styles from './tabs.module.css'
|
|
16
30
|
|
|
31
|
+
const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Size and offset of the overflow trigger, in pixels.
|
|
35
|
+
*
|
|
36
|
+
* Pushed onto the element as custom properties so the stylesheet lays the
|
|
37
|
+
* trigger out from the same numbers the geometry reserves space with. Pixels
|
|
38
|
+
* rather than `rem` deliberately: the trigger is icon-only, so it has no text
|
|
39
|
+
* to scale, and a fixed size keeps the reserved width correct under text
|
|
40
|
+
* scaling and translation alike.
|
|
41
|
+
*/
|
|
42
|
+
const TRIGGER_SIZE_PX = 32
|
|
43
|
+
const TRIGGER_GAP_PX = 12
|
|
44
|
+
|
|
45
|
+
const INITIAL_STRIP_STATE: StripState = {
|
|
46
|
+
overflowing: false,
|
|
47
|
+
viewportWidth: 0,
|
|
48
|
+
atStart: true,
|
|
49
|
+
atEnd: true,
|
|
50
|
+
}
|
|
51
|
+
|
|
17
52
|
export interface AdminTabItem {
|
|
18
53
|
name: string
|
|
19
54
|
label: string
|
|
@@ -23,62 +58,362 @@ interface AdminTabsProps {
|
|
|
23
58
|
tabs: AdminTabItem[]
|
|
24
59
|
activeTab: string
|
|
25
60
|
onChange: (name: string) => void
|
|
61
|
+
/**
|
|
62
|
+
* Prefix for the generated tab and panel ids. The consumer that renders the
|
|
63
|
+
* panels builds its panel ids from the same base via `tabPanelId`, which is
|
|
64
|
+
* what associates the two halves for assistive technology.
|
|
65
|
+
*/
|
|
66
|
+
idBase: string
|
|
26
67
|
/** Error counts keyed by tab name — shows a danger badge when > 0. */
|
|
27
68
|
errorCounts?: Record<string, number>
|
|
28
69
|
className?: string
|
|
29
70
|
}
|
|
30
71
|
|
|
72
|
+
/** Id of the button for one tab. */
|
|
73
|
+
export const tabTriggerId = (idBase: string, name: string): string => `${idBase}-tab-${name}`
|
|
74
|
+
|
|
75
|
+
/** Id the consumer must put on the panel belonging to one tab. */
|
|
76
|
+
export const tabPanelId = (idBase: string, name: string): string => `${idBase}-panel-${name}`
|
|
77
|
+
|
|
78
|
+
function prefersReducedMotion(): boolean {
|
|
79
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
|
|
80
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
/**
|
|
32
84
|
* Tabs navigation bar for admin form layouts.
|
|
33
85
|
*
|
|
34
|
-
* Used by FormRenderer when a CollectionAdminConfig declares a `tabs` array
|
|
35
|
-
*
|
|
36
|
-
*
|
|
86
|
+
* Used by FormRenderer when a CollectionAdminConfig declares a `tabs` array,
|
|
87
|
+
* and by the collection history view. The number of tabs a collection can
|
|
88
|
+
* declare is unbounded while the column they sit in is not, so the strip
|
|
89
|
+
* scrolls horizontally and — only once the tabs genuinely exceed the column —
|
|
90
|
+
* grows a menu listing every tab.
|
|
91
|
+
*
|
|
92
|
+
* The menu is a jump list rather than a list of the tabs that happen to be
|
|
93
|
+
* out of view: its contents stay put as the strip scrolls, and no tab ever
|
|
94
|
+
* changes position in the strip as a result of being selected. Editors learn
|
|
95
|
+
* where a tab lives and it stays there.
|
|
96
|
+
*
|
|
97
|
+
* Scroll position is moved only when the selection changes or a tab takes
|
|
98
|
+
* focus. A reader who scrolls the strip to look at other labels keeps their
|
|
99
|
+
* position through re-measurement, re-render and resize.
|
|
37
100
|
*
|
|
38
|
-
* Stable override handles: `.byline-admin-tabs`, `.byline-admin-
|
|
39
|
-
* `.byline-admin-tab
|
|
40
|
-
* `.byline-admin-tab-badge
|
|
101
|
+
* Stable override handles: `.byline-admin-tabs`, `.byline-admin-tabs-viewport`,
|
|
102
|
+
* `.byline-admin-tablist`, `.byline-admin-tab`, `.byline-admin-tab-active`,
|
|
103
|
+
* `.byline-admin-tab-label`, `.byline-admin-tab-badge`,
|
|
104
|
+
* `.byline-admin-tabs-overflow-trigger`, `.byline-admin-tabs-overflow-menu`,
|
|
105
|
+
* `.byline-admin-tabs-overflow-item`.
|
|
41
106
|
*/
|
|
42
107
|
export const AdminTabs = ({
|
|
43
108
|
tabs,
|
|
44
109
|
activeTab,
|
|
45
110
|
onChange,
|
|
111
|
+
idBase,
|
|
46
112
|
errorCounts,
|
|
47
113
|
className,
|
|
48
114
|
}: AdminTabsProps) => {
|
|
49
115
|
const { t } = useTranslation('byline-admin')
|
|
116
|
+
|
|
117
|
+
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
118
|
+
const viewportRef = useRef<HTMLDivElement | null>(null)
|
|
119
|
+
const rowRef = useRef<HTMLDivElement | null>(null)
|
|
120
|
+
const tabRefs = useRef(new Map<string, HTMLButtonElement>())
|
|
121
|
+
// Set when a change originates from the keyboard or the menu, so focus
|
|
122
|
+
// follows the selection once the parent has re-rendered with it.
|
|
123
|
+
const focusAfterChange = useRef<string | null>(null)
|
|
124
|
+
|
|
125
|
+
const [strip, setStrip] = useState<StripState>(INITIAL_STRIP_STATE)
|
|
126
|
+
|
|
127
|
+
const measure = useCallback(() => {
|
|
128
|
+
const container = containerRef.current
|
|
129
|
+
const row = rowRef.current
|
|
130
|
+
if (container == null || row == null) return
|
|
131
|
+
|
|
132
|
+
const next = computeStripState({
|
|
133
|
+
rowWidth: row.scrollWidth,
|
|
134
|
+
containerWidth: container.clientWidth,
|
|
135
|
+
triggerWidth: TRIGGER_SIZE_PX,
|
|
136
|
+
triggerGap: TRIGGER_GAP_PX,
|
|
137
|
+
scrollLeft: viewportRef.current?.scrollLeft ?? 0,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
setStrip((prev) =>
|
|
141
|
+
prev.overflowing === next.overflowing &&
|
|
142
|
+
prev.viewportWidth === next.viewportWidth &&
|
|
143
|
+
prev.atStart === next.atStart &&
|
|
144
|
+
prev.atEnd === next.atEnd
|
|
145
|
+
? prev
|
|
146
|
+
: next
|
|
147
|
+
)
|
|
148
|
+
}, [])
|
|
149
|
+
|
|
150
|
+
useIsoLayoutEffect(() => {
|
|
151
|
+
measure()
|
|
152
|
+
|
|
153
|
+
const container = containerRef.current
|
|
154
|
+
const row = rowRef.current
|
|
155
|
+
if (container == null || row == null || typeof ResizeObserver === 'undefined') return
|
|
156
|
+
|
|
157
|
+
// The row is observed alongside the container because most of what changes
|
|
158
|
+
// the strip's width is content, not layout: a label being translated, an
|
|
159
|
+
// error badge appearing mid-validation, a conditional tab unmounting. None
|
|
160
|
+
// of those resize the container at all.
|
|
161
|
+
const observer = new ResizeObserver(() => measure())
|
|
162
|
+
observer.observe(container)
|
|
163
|
+
observer.observe(row)
|
|
164
|
+
return () => observer.disconnect()
|
|
165
|
+
}, [measure, tabs])
|
|
166
|
+
|
|
167
|
+
const reveal = useCallback((name: string) => {
|
|
168
|
+
const viewport = viewportRef.current
|
|
169
|
+
const row = rowRef.current
|
|
170
|
+
const tab = tabRefs.current.get(name)
|
|
171
|
+
if (viewport == null || row == null || tab == null) return
|
|
172
|
+
|
|
173
|
+
const next = computeRevealScroll({
|
|
174
|
+
viewportWidth: viewport.clientWidth,
|
|
175
|
+
scrollLeft: viewport.scrollLeft,
|
|
176
|
+
tabOffsetLeft: tab.offsetLeft,
|
|
177
|
+
tabWidth: tab.offsetWidth,
|
|
178
|
+
rowWidth: row.scrollWidth,
|
|
179
|
+
})
|
|
180
|
+
// `null` means the tab is already visible. Writing the current offset back
|
|
181
|
+
// would interrupt a scroll the reader is in the middle of.
|
|
182
|
+
if (next == null) return
|
|
183
|
+
|
|
184
|
+
// Scrolling the viewport directly rather than via `scrollIntoView`, which
|
|
185
|
+
// walks up the ancestor chain and would move the page vertically as well.
|
|
186
|
+
viewport.scrollTo?.({ left: next, behavior: prefersReducedMotion() ? 'auto' : 'smooth' })
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const pending = focusAfterChange.current
|
|
191
|
+
if (pending === activeTab) {
|
|
192
|
+
focusAfterChange.current = null
|
|
193
|
+
// `preventScroll` because the browser's own scroll-on-focus would scroll
|
|
194
|
+
// ancestors too; `reveal` below moves only the strip.
|
|
195
|
+
tabRefs.current.get(activeTab)?.focus({ preventScroll: true })
|
|
196
|
+
}
|
|
197
|
+
reveal(activeTab)
|
|
198
|
+
}, [activeTab, reveal])
|
|
199
|
+
|
|
200
|
+
const select = useCallback(
|
|
201
|
+
(name: string, moveFocus: boolean) => {
|
|
202
|
+
if (moveFocus) focusAfterChange.current = name
|
|
203
|
+
onChange(name)
|
|
204
|
+
},
|
|
205
|
+
[onChange]
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// ─── Drag-to-scroll ────────────────────────────────────────────────────
|
|
209
|
+
//
|
|
210
|
+
// Touch scrolls the viewport natively; this exists for mouse users, who
|
|
211
|
+
// otherwise have no way to reach an off-screen tab but the menu — a plain
|
|
212
|
+
// wheel has no horizontal axis.
|
|
213
|
+
|
|
214
|
+
const dragRef = useRef<DragState | null>(null)
|
|
215
|
+
// When the last drag ended. A drag finishing over a tab would otherwise
|
|
216
|
+
// select it, and a timestamp expires on its own where a flag would sit armed
|
|
217
|
+
// waiting for a click that a drag released off-strip never produces.
|
|
218
|
+
const dragEndedAtRef = useRef<number | null>(null)
|
|
219
|
+
const [dragging, setDragging] = useState(false)
|
|
220
|
+
|
|
221
|
+
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
|
222
|
+
const viewport = viewportRef.current
|
|
223
|
+
if (viewport == null) return
|
|
224
|
+
if (
|
|
225
|
+
shouldStartDrag({
|
|
226
|
+
pointerType: event.pointerType,
|
|
227
|
+
button: event.button,
|
|
228
|
+
overflowing: strip.overflowing,
|
|
229
|
+
}) === false
|
|
230
|
+
) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
dragRef.current = {
|
|
234
|
+
origin: { pointerX: event.clientX, scrollLeft: viewport.scrollLeft },
|
|
235
|
+
dragging: false,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
|
240
|
+
const state = dragRef.current
|
|
241
|
+
const viewport = viewportRef.current
|
|
242
|
+
const row = rowRef.current
|
|
243
|
+
if (state == null || viewport == null || row == null) return
|
|
244
|
+
|
|
245
|
+
const maxScroll = Math.max(0, row.scrollWidth - viewport.clientWidth)
|
|
246
|
+
const next = advanceDrag(state, event.clientX, maxScroll)
|
|
247
|
+
// Still inside the threshold — leave the viewport alone so the gesture can
|
|
248
|
+
// still resolve as a click.
|
|
249
|
+
if (next.scrollLeft == null) return
|
|
250
|
+
|
|
251
|
+
if (state.dragging === false) {
|
|
252
|
+
state.dragging = true
|
|
253
|
+
setDragging(true)
|
|
254
|
+
// Capture so the drag survives the pointer leaving the strip, which is
|
|
255
|
+
// most of the point: you can pull well past the edge and keep going.
|
|
256
|
+
viewport.setPointerCapture?.(event.pointerId)
|
|
257
|
+
}
|
|
258
|
+
viewport.scrollLeft = next.scrollLeft
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const handlePointerEnd = (event: PointerEvent<HTMLDivElement>) => {
|
|
262
|
+
const state = dragRef.current
|
|
263
|
+
if (state == null) return
|
|
264
|
+
dragRef.current = null
|
|
265
|
+
if (state.dragging === false) return
|
|
266
|
+
|
|
267
|
+
setDragging(false)
|
|
268
|
+
viewportRef.current?.releasePointerCapture?.(event.pointerId)
|
|
269
|
+
dragEndedAtRef.current = performance.now()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const handleClickCapture = (event: MouseEvent<HTMLDivElement>) => {
|
|
273
|
+
if (shouldSuppressClick(dragEndedAtRef.current, performance.now()) === false) return
|
|
274
|
+
dragEndedAtRef.current = null
|
|
275
|
+
event.stopPropagation()
|
|
276
|
+
event.preventDefault()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
280
|
+
const current = tabs.findIndex((tab) => tab.name === activeTab)
|
|
281
|
+
if (current < 0 || tabs.length === 0) return
|
|
282
|
+
|
|
283
|
+
let nextIndex: number
|
|
284
|
+
switch (event.key) {
|
|
285
|
+
case 'ArrowRight':
|
|
286
|
+
nextIndex = (current + 1) % tabs.length
|
|
287
|
+
break
|
|
288
|
+
case 'ArrowLeft':
|
|
289
|
+
nextIndex = (current - 1 + tabs.length) % tabs.length
|
|
290
|
+
break
|
|
291
|
+
case 'Home':
|
|
292
|
+
nextIndex = 0
|
|
293
|
+
break
|
|
294
|
+
case 'End':
|
|
295
|
+
nextIndex = tabs.length - 1
|
|
296
|
+
break
|
|
297
|
+
default:
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
event.preventDefault()
|
|
302
|
+
const next = tabs[nextIndex]
|
|
303
|
+
if (next == null || next.name === activeTab) return
|
|
304
|
+
select(next.name, true)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const sizing = {
|
|
308
|
+
'--byline-admin-tabs-trigger-size': `${TRIGGER_SIZE_PX}px`,
|
|
309
|
+
'--byline-admin-tabs-trigger-gap': `${TRIGGER_GAP_PX}px`,
|
|
310
|
+
} as CSSProperties
|
|
311
|
+
|
|
50
312
|
return (
|
|
51
313
|
<div
|
|
52
|
-
|
|
53
|
-
|
|
314
|
+
ref={containerRef}
|
|
315
|
+
style={sizing}
|
|
54
316
|
className={cx('byline-admin-tabs', styles.tabs, className)}
|
|
55
317
|
>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
318
|
+
<div
|
|
319
|
+
ref={viewportRef}
|
|
320
|
+
onScroll={measure}
|
|
321
|
+
onPointerDown={handlePointerDown}
|
|
322
|
+
onPointerMove={handlePointerMove}
|
|
323
|
+
onPointerUp={handlePointerEnd}
|
|
324
|
+
onPointerCancel={handlePointerEnd}
|
|
325
|
+
onClickCapture={handleClickCapture}
|
|
326
|
+
data-at-start={strip.atStart}
|
|
327
|
+
data-at-end={strip.atEnd}
|
|
328
|
+
data-draggable={strip.overflowing}
|
|
329
|
+
data-dragging={dragging}
|
|
330
|
+
className={cx('byline-admin-tabs-viewport', styles.viewport)}
|
|
331
|
+
>
|
|
332
|
+
<div
|
|
333
|
+
ref={rowRef}
|
|
334
|
+
role="tablist"
|
|
335
|
+
aria-label={t('presentation.formTabsAriaLabel')}
|
|
336
|
+
onKeyDown={handleKeyDown}
|
|
337
|
+
className={cx('byline-admin-tablist', styles.tablist)}
|
|
338
|
+
>
|
|
339
|
+
{tabs.map((tab) => {
|
|
340
|
+
const isActive = tab.name === activeTab
|
|
341
|
+
const errorCount = errorCounts?.[tab.name] ?? 0
|
|
342
|
+
return (
|
|
343
|
+
<button
|
|
344
|
+
key={tab.name}
|
|
345
|
+
ref={(node) => {
|
|
346
|
+
if (node == null) tabRefs.current.delete(tab.name)
|
|
347
|
+
else tabRefs.current.set(tab.name, node)
|
|
348
|
+
}}
|
|
349
|
+
type="button"
|
|
350
|
+
role="tab"
|
|
351
|
+
id={tabTriggerId(idBase, tab.name)}
|
|
352
|
+
aria-controls={tabPanelId(idBase, tab.name)}
|
|
353
|
+
aria-selected={isActive}
|
|
354
|
+
// Roving tabindex: the strip is one stop in the tab order, and
|
|
355
|
+
// the arrow keys move within it.
|
|
356
|
+
tabIndex={isActive ? 0 : -1}
|
|
357
|
+
onClick={() => select(tab.name, false)}
|
|
358
|
+
onFocus={() => reveal(tab.name)}
|
|
359
|
+
className={cx(
|
|
360
|
+
'byline-admin-tab',
|
|
361
|
+
styles.tab,
|
|
362
|
+
isActive && ['byline-admin-tab-active', styles['tab-active']]
|
|
363
|
+
)}
|
|
364
|
+
>
|
|
365
|
+
<span className={cx('byline-admin-tab-label', styles.label)}>
|
|
366
|
+
{tab.label}
|
|
367
|
+
{errorCount > 0 && (
|
|
368
|
+
<Badge intent="danger" className={cx('byline-admin-tab-badge', styles.badge)}>
|
|
369
|
+
{errorCount}
|
|
370
|
+
</Badge>
|
|
371
|
+
)}
|
|
372
|
+
</span>
|
|
373
|
+
</button>
|
|
374
|
+
)
|
|
375
|
+
})}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
{strip.overflowing && (
|
|
380
|
+
<Dropdown.Root>
|
|
381
|
+
<Dropdown.Trigger
|
|
382
|
+
aria-label={t('presentation.formTabsAllTabsLabel')}
|
|
383
|
+
className={cx('byline-admin-tabs-overflow-trigger', styles['overflow-trigger'])}
|
|
70
384
|
>
|
|
71
|
-
<
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
385
|
+
<EllipsisIcon
|
|
386
|
+
className={cx('byline-admin-tabs-overflow-icon', styles['overflow-icon'])}
|
|
387
|
+
/>
|
|
388
|
+
</Dropdown.Trigger>
|
|
389
|
+
<Dropdown.Portal>
|
|
390
|
+
<Dropdown.Content
|
|
391
|
+
sideOffset={6}
|
|
392
|
+
align="end"
|
|
393
|
+
className={cx('byline-admin-tabs-overflow-menu', styles['overflow-menu'])}
|
|
394
|
+
>
|
|
395
|
+
{tabs.map((tab) => {
|
|
396
|
+
const errorCount = errorCounts?.[tab.name] ?? 0
|
|
397
|
+
return (
|
|
398
|
+
<Dropdown.Item
|
|
399
|
+
key={tab.name}
|
|
400
|
+
onClick={() => select(tab.name, true)}
|
|
401
|
+
data-active={tab.name === activeTab}
|
|
402
|
+
className={cx('byline-admin-tabs-overflow-item', styles['overflow-item'])}
|
|
403
|
+
>
|
|
404
|
+
{tab.label}
|
|
405
|
+
{errorCount > 0 && (
|
|
406
|
+
<Badge intent="danger" className={cx('byline-admin-tab-badge', styles.badge)}>
|
|
407
|
+
{errorCount}
|
|
408
|
+
</Badge>
|
|
409
|
+
)}
|
|
410
|
+
</Dropdown.Item>
|
|
411
|
+
)
|
|
412
|
+
})}
|
|
413
|
+
</Dropdown.Content>
|
|
414
|
+
</Dropdown.Portal>
|
|
415
|
+
</Dropdown.Root>
|
|
416
|
+
)}
|
|
82
417
|
</div>
|
|
83
418
|
)
|
|
84
419
|
}
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
* Guard against `publishConfig.exports` drift.
|
|
10
|
-
*
|
|
11
|
-
* `@byline/admin` carries a `publishConfig.exports` block that npm uses to
|
|
12
|
-
* **override** the top-level `exports` at publish time. The workspace and dev
|
|
13
|
-
* builds resolve through the top-level `exports` (or source), so a subpath
|
|
14
|
-
* added there but forgotten in `publishConfig.exports` typechecks and builds
|
|
15
|
-
* locally yet is **missing from the published package** — surfacing only as a
|
|
16
|
-
* downstream consumer's build error ("X is not exported …"). That is exactly
|
|
17
|
-
* how `./admin-activity` slipped through in v3.11.0.
|
|
18
|
-
*
|
|
19
|
-
* This test fails the moment the two blocks drift, so the gap is caught in
|
|
20
|
-
* `pnpm test` / CI rather than in a production Docker build.
|
|
21
|
-
*/
|
|
22
|
-
import { readFileSync } from 'node:fs';
|
|
23
|
-
import { dirname, resolve } from 'node:path';
|
|
24
|
-
import { fileURLToPath } from 'node:url';
|
|
25
|
-
import { describe, expect, it } from 'vitest';
|
|
26
|
-
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
27
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
28
|
-
describe('package.json export parity', () => {
|
|
29
|
-
it('every top-level export subpath is also declared in publishConfig.exports', () => {
|
|
30
|
-
const publishExports = pkg.publishConfig?.exports;
|
|
31
|
-
// If there is no override, the top-level exports ship as-is — nothing to check.
|
|
32
|
-
if (publishExports == null)
|
|
33
|
-
return;
|
|
34
|
-
const missing = Object.keys(pkg.exports).filter((key) => !(key in publishExports));
|
|
35
|
-
expect(missing, `publishConfig.exports is missing subpath(s) present in the top-level exports: ${missing.join(', ')}. The published package would not expose them — add them to BOTH blocks.`).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
});
|