All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Updated CSS for dashboard tabs to improve layout and overflow handling, ensuring better visibility and usability. - Replaced Typography's Text component with a custom ElipsisText component for improved text overflow management. - Simplified tab selection logic in NavigationTabsContext, enhancing scrolling behavior when selecting tabs. - Introduced a new utility function to scroll tabs into view, improving user experience during navigation.
804 lines
22 KiB
JavaScript
804 lines
22 KiB
JavaScript
import PropTypes from 'prop-types'
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useRef,
|
|
useState,
|
|
memo
|
|
} from 'react'
|
|
import { Button, Flex, Popover } from 'antd'
|
|
import DashboardTabPreview from './DashboardTabPreview'
|
|
import ElipsisText from './ElipsisText'
|
|
import classNames from 'classnames'
|
|
import ScrollBox from './ScrollBox'
|
|
import PlusIcon from '../../Icons/PlusIcon'
|
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
import HomeIcon from '../../Icons/HomeIcon'
|
|
import { getModelByName } from '../../../database/ObjectModels'
|
|
import { getSidebarIconComponent } from '../../Icons/sidebarIconMap'
|
|
import {
|
|
useNavigationTabs,
|
|
useTabPreview
|
|
} from '../context/NavigationTabsContext'
|
|
import { getDesktopWindowId } from '../../../electrobun-bridge.js'
|
|
import { hasExternalTabDrag, writeTabDragData } from './tabDrag'
|
|
|
|
const DashboardTabChrome = () => (
|
|
<>
|
|
<span className='dashboard-tab-item-outline-container' aria-hidden='true'>
|
|
<span className='dashboard-tab-item-outline' aria-hidden='true' />
|
|
</span>
|
|
<span
|
|
className='dashboard-tab-item-corner dashboard-tab-item-corner-left'
|
|
aria-hidden='true'
|
|
>
|
|
<span className='dashboard-tab-item-corner-fill' />
|
|
<span className='dashboard-tab-item-corner-inner' />
|
|
</span>
|
|
<span
|
|
className='dashboard-tab-item-corner dashboard-tab-item-corner-right'
|
|
aria-hidden='true'
|
|
>
|
|
<span className='dashboard-tab-item-corner-fill' />
|
|
<span className='dashboard-tab-item-corner-inner' />
|
|
</span>
|
|
</>
|
|
)
|
|
|
|
const DashboardTabLabel = ({ tab, onClose }) => (
|
|
<Flex align='center' gap={6} className='dashboard-tabs-label'>
|
|
<ElipsisText>{tab.title || 'Farm Control'}</ElipsisText>
|
|
<span
|
|
className='dashboard-tabs-close'
|
|
role='button'
|
|
tabIndex={-1}
|
|
onClick={(event) => {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
onClose?.(tab.id)
|
|
}}
|
|
onMouseDown={(event) => event.stopPropagation()}
|
|
>
|
|
<XMarkIcon style={{ fontSize: 10 }} />
|
|
</span>
|
|
</Flex>
|
|
)
|
|
|
|
DashboardTabLabel.propTypes = {
|
|
tab: PropTypes.shape({
|
|
id: PropTypes.string.isRequired,
|
|
title: PropTypes.string
|
|
}).isRequired,
|
|
onClose: PropTypes.func
|
|
}
|
|
|
|
const DashboardTabItem = memo(function DashboardTabItem({
|
|
htmlId = undefined,
|
|
tab,
|
|
tabIndex,
|
|
isSelected,
|
|
isDragging,
|
|
dropPosition,
|
|
onDragStart,
|
|
onDragEnd,
|
|
onClick,
|
|
onKeyDown,
|
|
onDragOver,
|
|
onDrop,
|
|
onClose,
|
|
captureTabPreview
|
|
}) {
|
|
const {
|
|
src: previewSrc,
|
|
theme: previewTheme,
|
|
capturing: previewCapturing
|
|
} = useTabPreview(tab.id)
|
|
const [previewOpen, setPreviewOpen] = useState(false)
|
|
const previewTargetRef = useRef(null)
|
|
const model = tab.modelName ? getModelByName(tab.modelName) : null
|
|
const SidebarIcon = getSidebarIconComponent(tab.iconKey)
|
|
const Icon = model?.icon || SidebarIcon || HomeIcon
|
|
|
|
const handlePreviewOpenChange = useCallback(
|
|
(nextOpen) => {
|
|
if (isSelected || isDragging) {
|
|
setPreviewOpen(false)
|
|
return
|
|
}
|
|
setPreviewOpen(nextOpen)
|
|
if (nextOpen) {
|
|
void captureTabPreview(tab.id)
|
|
}
|
|
},
|
|
[captureTabPreview, isDragging, isSelected, tab.id]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (isSelected || isDragging) {
|
|
setPreviewOpen(false)
|
|
}
|
|
}, [isDragging, isSelected])
|
|
|
|
const tabButton = (
|
|
<button
|
|
ref={previewTargetRef}
|
|
type='button'
|
|
role='tab'
|
|
aria-selected={isSelected}
|
|
tabIndex={isSelected ? 0 : -1}
|
|
className={classNames(
|
|
'dashboard-tab-item',
|
|
'electrobun-webkit-app-region-no-drag',
|
|
{
|
|
'dashboard-tab-item-active': isSelected,
|
|
'dashboard-tab-item-dragging': isDragging,
|
|
'dashboard-tab-item-drop-before': dropPosition === 'before',
|
|
'dashboard-tab-item-drop-after': dropPosition === 'after'
|
|
}
|
|
)}
|
|
draggable
|
|
onDragStart={(event) => onDragStart(event, tab.id)}
|
|
onDragEnd={onDragEnd}
|
|
id={htmlId}
|
|
onClick={() => onClick(tab.id)}
|
|
onKeyDown={(event) => onKeyDown(event, tabIndex)}
|
|
onDragOver={(event) => onDragOver(event, tab)}
|
|
onDrop={(event) => onDrop(event, tab)}
|
|
>
|
|
<DashboardTabChrome />
|
|
<span className='dashboard-tab-item-body'>
|
|
<span className='dashboard-tab-item-icon'>
|
|
<Icon style={{ fontSize: 14 }} color='secondary' />
|
|
</span>
|
|
<DashboardTabLabel tab={tab} onClose={onClose} />
|
|
</span>
|
|
</button>
|
|
)
|
|
|
|
if (isSelected) {
|
|
return tabButton
|
|
}
|
|
|
|
return (
|
|
<Popover
|
|
trigger='hover'
|
|
placement='bottom'
|
|
arrow={false}
|
|
mouseEnterDelay={1}
|
|
mouseLeaveDelay={0}
|
|
destroyOnHidden
|
|
open={isDragging ? false : previewOpen}
|
|
onOpenChange={handlePreviewOpenChange}
|
|
content={
|
|
<DashboardTabPreview
|
|
src={previewSrc}
|
|
srcTheme={previewTheme}
|
|
loading={previewCapturing}
|
|
/>
|
|
}
|
|
classNames={{ root: 'dashboard-tab-preview-popover' }}
|
|
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
|
>
|
|
{tabButton}
|
|
</Popover>
|
|
)
|
|
})
|
|
|
|
DashboardTabItem.displayName = 'DashboardTabItem'
|
|
|
|
DashboardTabItem.propTypes = {
|
|
htmlId: PropTypes.string,
|
|
tab: PropTypes.shape({
|
|
id: PropTypes.string.isRequired,
|
|
title: PropTypes.string,
|
|
modelName: PropTypes.string,
|
|
iconKey: PropTypes.string
|
|
}).isRequired,
|
|
tabIndex: PropTypes.number.isRequired,
|
|
isSelected: PropTypes.bool,
|
|
isDragging: PropTypes.bool,
|
|
dropPosition: PropTypes.oneOf(['before', 'after']),
|
|
onDragStart: PropTypes.func.isRequired,
|
|
onDragEnd: PropTypes.func.isRequired,
|
|
onClick: PropTypes.func.isRequired,
|
|
onKeyDown: PropTypes.func.isRequired,
|
|
onDragOver: PropTypes.func.isRequired,
|
|
onDrop: PropTypes.func.isRequired,
|
|
onClose: PropTypes.func,
|
|
captureTabPreview: PropTypes.func
|
|
}
|
|
|
|
const STICKY_EDGE_INSET = 20
|
|
const STICKY_LINE_CORNER_GAP = 16
|
|
const STICKY_MASK_INSET = 58
|
|
|
|
const measureTabMetrics = (container) => {
|
|
const button = container.querySelector('.dashboard-tab-item')
|
|
const icon = container.querySelector('.dashboard-tab-item-icon')
|
|
if (!button || !icon) {
|
|
return {
|
|
naturalWidth: container.offsetWidth,
|
|
minWidth: container.offsetWidth
|
|
}
|
|
}
|
|
|
|
const styles = window.getComputedStyle(button)
|
|
const minWidth = Math.ceil(
|
|
icon.getBoundingClientRect().width +
|
|
parseFloat(styles.paddingLeft) +
|
|
parseFloat(styles.paddingRight) +
|
|
parseFloat(styles.borderLeftWidth) +
|
|
parseFloat(styles.borderRightWidth)
|
|
)
|
|
|
|
return {
|
|
naturalWidth: container.offsetWidth,
|
|
minWidth: Math.max(minWidth, 1)
|
|
}
|
|
}
|
|
|
|
const hideStickyClone = (cloneEl) => {
|
|
if (!cloneEl) return
|
|
delete cloneEl.dataset.edge
|
|
delete cloneEl.dataset.faded
|
|
cloneEl.style.removeProperty('--tab-sticky-width')
|
|
cloneEl.style.removeProperty('--tab-label-opacity')
|
|
cloneEl.style.left = ''
|
|
cloneEl.style.right = ''
|
|
cloneEl.setAttribute('inert', '')
|
|
}
|
|
|
|
const setSimplebarMaskInset = (maskEl, edge, inset) => {
|
|
if (!maskEl) return
|
|
const wrapEl = maskEl.closest('.dashboard-tabs-wrap') || maskEl
|
|
if (!edge) {
|
|
wrapEl.style.removeProperty('--tab-mask-left')
|
|
wrapEl.style.removeProperty('--tab-mask-right')
|
|
return
|
|
}
|
|
if (edge === 'left') {
|
|
wrapEl.style.setProperty('--tab-mask-left', `${inset}px`)
|
|
wrapEl.style.removeProperty('--tab-mask-right')
|
|
} else {
|
|
wrapEl.style.removeProperty('--tab-mask-left')
|
|
wrapEl.style.setProperty('--tab-mask-right', `${inset}px`)
|
|
}
|
|
}
|
|
|
|
const setScrollEdgeFades = (root, scrollEl) => {
|
|
if (!root) return
|
|
if (!scrollEl) {
|
|
root.dataset.atStart = ''
|
|
root.dataset.atEnd = ''
|
|
return
|
|
}
|
|
const atStart = scrollEl.scrollLeft <= 1
|
|
const atEnd =
|
|
scrollEl.scrollLeft + scrollEl.clientWidth >= scrollEl.scrollWidth - 1
|
|
if (atStart) root.dataset.atStart = ''
|
|
else delete root.dataset.atStart
|
|
if (atEnd) root.dataset.atEnd = ''
|
|
else delete root.dataset.atEnd
|
|
}
|
|
|
|
const setStickyStripLine = (root, lineEl, edge, lineInset) => {
|
|
if (root) {
|
|
if (!edge) delete root.dataset.stickyEdge
|
|
else root.dataset.stickyEdge = edge
|
|
}
|
|
const padding = 4
|
|
if (!lineEl) return
|
|
if (!edge) {
|
|
delete lineEl.dataset.edge
|
|
lineEl.style.left = ''
|
|
lineEl.style.right = ''
|
|
return
|
|
}
|
|
lineEl.dataset.edge = edge
|
|
if (edge === 'left') {
|
|
lineEl.style.left = `${lineInset + padding}px`
|
|
lineEl.style.right = '0px'
|
|
} else {
|
|
lineEl.style.left = '0px'
|
|
lineEl.style.right = `${lineInset + padding}px`
|
|
}
|
|
}
|
|
|
|
const setOriginalStickyState = (container, sticky) => {
|
|
if (!container) return
|
|
if (sticky) {
|
|
container.dataset.stickyPlaceholder = 'true'
|
|
container.setAttribute('inert', '')
|
|
} else {
|
|
delete container.dataset.stickyPlaceholder
|
|
container.removeAttribute('inert')
|
|
}
|
|
}
|
|
|
|
const bindActiveTabStickyScroll = (scrollEl, list, cloneEl, root, lineEl) => {
|
|
const state = {
|
|
activeEl: null,
|
|
updating: false
|
|
}
|
|
const maskEl = scrollEl.closest('.simplebar-mask')
|
|
const wrapperEl = scrollEl.closest('.simplebar-wrapper') || scrollEl
|
|
|
|
const clearStickyVisuals = () => {
|
|
hideStickyClone(cloneEl)
|
|
setStickyStripLine(root, lineEl, null)
|
|
setSimplebarMaskInset(maskEl, null)
|
|
}
|
|
|
|
const update = () => {
|
|
if (state.updating) return
|
|
state.updating = true
|
|
|
|
try {
|
|
setScrollEdgeFades(root, scrollEl)
|
|
const container = list.querySelector(
|
|
'.dashboard-tab-item-container-active'
|
|
)
|
|
if (state.activeEl && state.activeEl !== container) {
|
|
setOriginalStickyState(state.activeEl, false)
|
|
}
|
|
state.activeEl = container
|
|
|
|
if (!container || !cloneEl) {
|
|
clearStickyVisuals()
|
|
return
|
|
}
|
|
|
|
const metrics = measureTabMetrics(container)
|
|
const naturalWidth = metrics.naturalWidth
|
|
const minWidth = Math.min(metrics.minWidth, naturalWidth)
|
|
const maxShrink = Math.max(0, naturalWidth - minWidth)
|
|
const scrollLeft = scrollEl.scrollLeft
|
|
const viewportWidth = wrapperEl.clientWidth
|
|
if (viewportWidth <= 0) {
|
|
setOriginalStickyState(container, false)
|
|
clearStickyVisuals()
|
|
return
|
|
}
|
|
|
|
const wrapperRect = wrapperEl.getBoundingClientRect()
|
|
const containerRect = container.getBoundingClientRect()
|
|
const naturalLeft = scrollLeft + (containerRect.left - wrapperRect.left)
|
|
const isFirstTab = container === list.firstElementChild
|
|
if (isFirstTab && scrollLeft < 1) {
|
|
setOriginalStickyState(container, false)
|
|
clearStickyVisuals()
|
|
return
|
|
}
|
|
const overflowLeft = scrollLeft + STICKY_EDGE_INSET - naturalLeft
|
|
const overflowRight =
|
|
naturalLeft +
|
|
naturalWidth -
|
|
(scrollLeft + viewportWidth - STICKY_EDGE_INSET)
|
|
|
|
let edge = null
|
|
let overflow = 0
|
|
if (overflowLeft > 0.5 && overflowLeft >= overflowRight) {
|
|
edge = 'left'
|
|
overflow = overflowLeft
|
|
} else if (overflowRight > 0.5) {
|
|
edge = 'right'
|
|
overflow = overflowRight
|
|
}
|
|
|
|
if (!edge) {
|
|
setOriginalStickyState(container, false)
|
|
clearStickyVisuals()
|
|
return
|
|
}
|
|
|
|
const shrink = Math.min(overflow, maxShrink)
|
|
const visualWidth = Math.max(minWidth, naturalWidth - shrink)
|
|
const shrinkProgress = maxShrink === 0 ? 1 : shrink / maxShrink
|
|
const fade = 1 - shrinkProgress
|
|
const maskInset = STICKY_MASK_INSET * shrinkProgress
|
|
|
|
setOriginalStickyState(container, true)
|
|
cloneEl.removeAttribute('inert')
|
|
cloneEl.dataset.edge = edge
|
|
if (fade < 0.25) cloneEl.dataset.faded = 'true'
|
|
else delete cloneEl.dataset.faded
|
|
|
|
const parentRect =
|
|
cloneEl.offsetParent?.getBoundingClientRect() || wrapperRect
|
|
const cloneOffset =
|
|
edge === 'left'
|
|
? wrapperRect.left - parentRect.left + STICKY_EDGE_INSET
|
|
: parentRect.right - wrapperRect.right + STICKY_EDGE_INSET
|
|
if (edge === 'left') {
|
|
cloneEl.style.left = `${cloneOffset}px`
|
|
cloneEl.style.right = 'auto'
|
|
} else {
|
|
cloneEl.style.left = 'auto'
|
|
cloneEl.style.right = `${cloneOffset}px`
|
|
}
|
|
cloneEl.style.setProperty('--tab-sticky-width', `${visualWidth}px`)
|
|
cloneEl.style.setProperty('--tab-label-opacity', String(fade))
|
|
setSimplebarMaskInset(maskEl, edge, maskInset)
|
|
setStickyStripLine(
|
|
root,
|
|
lineEl,
|
|
edge,
|
|
cloneOffset + visualWidth + STICKY_LINE_CORNER_GAP
|
|
)
|
|
} finally {
|
|
state.updating = false
|
|
}
|
|
}
|
|
|
|
let frame = 0
|
|
const scheduleUpdate = () => {
|
|
if (frame) return
|
|
frame = window.requestAnimationFrame(() => {
|
|
frame = 0
|
|
update()
|
|
})
|
|
}
|
|
|
|
update()
|
|
scrollEl.addEventListener('scroll', scheduleUpdate, { passive: true })
|
|
window.addEventListener('resize', scheduleUpdate)
|
|
|
|
const observer = new ResizeObserver(scheduleUpdate)
|
|
observer.observe(scrollEl)
|
|
if (wrapperEl !== scrollEl) observer.observe(wrapperEl)
|
|
observer.observe(list)
|
|
|
|
return () => {
|
|
scrollEl.removeEventListener('scroll', scheduleUpdate)
|
|
window.removeEventListener('resize', scheduleUpdate)
|
|
observer.disconnect()
|
|
if (frame) window.cancelAnimationFrame(frame)
|
|
setOriginalStickyState(state.activeEl, false)
|
|
clearStickyVisuals()
|
|
delete root.dataset.atStart
|
|
delete root.dataset.atEnd
|
|
}
|
|
}
|
|
|
|
const useActiveTabStickyScroll = (activeTabId, tabs) => {
|
|
const rootRef = useRef(null)
|
|
const listRef = useRef(null)
|
|
const cloneRef = useRef(null)
|
|
const lineRef = useRef(null)
|
|
const tabLayoutKey = `${activeTabId}:${tabs.map((tab) => `${tab.id}:${tab.title || ''}`).join('|')}`
|
|
|
|
useLayoutEffect(() => {
|
|
const root = rootRef.current
|
|
const list = listRef.current
|
|
const cloneEl = cloneRef.current
|
|
const lineEl = lineRef.current
|
|
if (!root || !list) return undefined
|
|
|
|
let cancelled = false
|
|
let teardown = () => {}
|
|
let frame = 0
|
|
|
|
const tryBind = () => {
|
|
if (cancelled) return
|
|
const scrollEl = root.querySelector('.simplebar-content-wrapper')
|
|
if (!scrollEl) {
|
|
frame = window.requestAnimationFrame(tryBind)
|
|
return
|
|
}
|
|
teardown = bindActiveTabStickyScroll(
|
|
scrollEl,
|
|
list,
|
|
cloneEl,
|
|
root,
|
|
lineEl
|
|
)
|
|
}
|
|
|
|
tryBind()
|
|
|
|
return () => {
|
|
cancelled = true
|
|
if (frame) window.cancelAnimationFrame(frame)
|
|
teardown()
|
|
}
|
|
}, [tabLayoutKey])
|
|
|
|
return { rootRef, listRef, cloneRef, lineRef }
|
|
}
|
|
|
|
const DashboardTabs = () => {
|
|
const {
|
|
tabs,
|
|
activeTabId,
|
|
selectTab,
|
|
addTab,
|
|
registerTabStripRoot,
|
|
closeTab,
|
|
reorderTabs,
|
|
handleTabDragStart,
|
|
handleTabDragEnd,
|
|
handleExternalTabDrop,
|
|
captureTabPreview
|
|
} = useNavigationTabs()
|
|
const { rootRef, listRef, cloneRef, lineRef } = useActiveTabStickyScroll(
|
|
activeTabId,
|
|
tabs
|
|
)
|
|
const [draggedValue, setDraggedValue] = useState(null)
|
|
const [dropTarget, setDropTarget] = useState(null)
|
|
|
|
const clearDragState = useCallback(() => {
|
|
setDraggedValue(null)
|
|
setDropTarget(null)
|
|
}, [])
|
|
|
|
const handleItemDragStart = (event, tabId) => {
|
|
const tab = tabs.find((item) => item.id === tabId)
|
|
if (!tab) return
|
|
|
|
event.stopPropagation()
|
|
const item = event.currentTarget
|
|
if (item) {
|
|
const rect = item.getBoundingClientRect()
|
|
event.dataTransfer.setDragImage(
|
|
item,
|
|
event.clientX - rect.left,
|
|
event.clientY - rect.top
|
|
)
|
|
}
|
|
|
|
setDraggedValue(tabId)
|
|
event.dataTransfer.effectAllowed = 'move'
|
|
writeTabDragData(event, {
|
|
tab,
|
|
sourceWindowId: getDesktopWindowId()
|
|
})
|
|
handleTabDragStart(tabId)
|
|
}
|
|
|
|
const handleItemDragEnd = () => {
|
|
handleTabDragEnd()
|
|
clearDragState()
|
|
}
|
|
|
|
const handleItemDragOver = (event, tab) => {
|
|
const isExternal = hasExternalTabDrag(event)
|
|
if (draggedValue == null && !isExternal) return
|
|
if (draggedValue != null && String(tab.id) === String(draggedValue)) return
|
|
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
event.dataTransfer.dropEffect = 'move'
|
|
|
|
const rect = event.currentTarget.getBoundingClientRect()
|
|
setDropTarget({
|
|
value: tab.id,
|
|
insertBefore: event.clientX < rect.left + rect.width / 2
|
|
})
|
|
}
|
|
|
|
const handleItemDrop = (event, tab) => {
|
|
const isExternal = draggedValue == null && hasExternalTabDrag(event)
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
|
|
const target = dropTarget || {
|
|
value: tab.id,
|
|
insertBefore: true
|
|
}
|
|
|
|
if (isExternal) {
|
|
void handleExternalTabDrop(target.value, target.insertBefore)
|
|
clearDragState()
|
|
return
|
|
}
|
|
|
|
if (draggedValue == null) return
|
|
|
|
if (String(draggedValue) !== String(target.value)) {
|
|
reorderTabs(draggedValue, target.value, target.insertBefore)
|
|
}
|
|
|
|
clearDragState()
|
|
}
|
|
|
|
const handleListDragOver = (event) => {
|
|
const isExternal = hasExternalTabDrag(event)
|
|
if (draggedValue == null && !isExternal) return
|
|
if (
|
|
event.target instanceof Element &&
|
|
event.target.closest('.dashboard-tab-item')
|
|
) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
event.dataTransfer.dropEffect = 'move'
|
|
|
|
const lastTab = tabs[tabs.length - 1]
|
|
if (!lastTab) return
|
|
setDropTarget({
|
|
value: lastTab.id,
|
|
insertBefore: false
|
|
})
|
|
}
|
|
|
|
const handleListDrop = (event) => {
|
|
const isExternal = draggedValue == null && hasExternalTabDrag(event)
|
|
if (draggedValue == null && !isExternal) return
|
|
if (
|
|
event.target instanceof Element &&
|
|
event.target.closest('.dashboard-tab-item')
|
|
) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
|
|
const lastTab = tabs[tabs.length - 1]
|
|
if (!lastTab) {
|
|
clearDragState()
|
|
return
|
|
}
|
|
|
|
if (isExternal) {
|
|
void handleExternalTabDrop(lastTab.id, false)
|
|
clearDragState()
|
|
return
|
|
}
|
|
|
|
if (String(draggedValue) !== String(lastTab.id)) {
|
|
reorderTabs(draggedValue, lastTab.id, false)
|
|
}
|
|
|
|
clearDragState()
|
|
}
|
|
|
|
const handleStripDragOver = (event) => {
|
|
if (!hasExternalTabDrag(event)) return
|
|
event.preventDefault()
|
|
event.dataTransfer.dropEffect = 'move'
|
|
}
|
|
|
|
const handleStripDrop = (event) => {
|
|
if (!hasExternalTabDrag(event)) return
|
|
event.preventDefault()
|
|
const lastTab = tabs[tabs.length - 1]
|
|
void handleExternalTabDrop(lastTab?.id, false)
|
|
}
|
|
|
|
const handleKeyDown = (event, tabIndex) => {
|
|
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return
|
|
|
|
event.preventDefault()
|
|
const direction = event.key === 'ArrowRight' ? 1 : -1
|
|
const currentIndex = tabs.findIndex((tab) => tab.id === activeTabId)
|
|
const startIndex = currentIndex === -1 ? tabIndex : currentIndex
|
|
const nextIndex = (startIndex + direction + tabs.length) % tabs.length
|
|
selectTab(tabs[nextIndex].id)
|
|
}
|
|
|
|
const setRootRef = useCallback(
|
|
(element) => {
|
|
rootRef.current = element
|
|
registerTabStripRoot(element)
|
|
},
|
|
[registerTabStripRoot]
|
|
)
|
|
|
|
const isReordering = draggedValue != null || dropTarget != null
|
|
const activeTab = tabs.find((tab) => tab.id === activeTabId)
|
|
const activeTabIndex = activeTab
|
|
? tabs.findIndex((tab) => tab.id === activeTabId)
|
|
: 0
|
|
|
|
const tabItemProps = {
|
|
onDragStart: handleItemDragStart,
|
|
onDragEnd: handleItemDragEnd,
|
|
onClick: selectTab,
|
|
onKeyDown: handleKeyDown,
|
|
onDragOver: handleItemDragOver,
|
|
onDrop: handleItemDrop,
|
|
onClose: closeTab,
|
|
captureTabPreview
|
|
}
|
|
|
|
return (
|
|
<Flex
|
|
ref={setRootRef}
|
|
align='flex-start'
|
|
gap={4}
|
|
className={classNames(
|
|
'dashboard-tabs',
|
|
'electrobun-webkit-app-region-drag'
|
|
)}
|
|
style={{ flex: 1, minWidth: 0 }}
|
|
onDragOver={handleStripDragOver}
|
|
onDrop={handleStripDrop}
|
|
>
|
|
<ScrollBox className='dashboard-tabs-wrap' horizontal>
|
|
<Flex align='flex-start' gap={5} className='dashboard-tabs-inner'>
|
|
<div
|
|
ref={listRef}
|
|
className={classNames('dashboard-tabs-list', {
|
|
'dashboard-tabs-reordering': isReordering
|
|
})}
|
|
role='tablist'
|
|
onDragOver={handleListDragOver}
|
|
onDrop={handleListDrop}
|
|
>
|
|
{tabs.map((tab, index) => {
|
|
const isSelected = tab.id === activeTabId
|
|
const isDragging =
|
|
draggedValue != null && String(draggedValue) === String(tab.id)
|
|
const isDropTarget =
|
|
dropTarget != null &&
|
|
String(dropTarget.value) === String(tab.id) &&
|
|
String(draggedValue) !== String(tab.id)
|
|
|
|
return (
|
|
<div
|
|
key={tab.id}
|
|
className={classNames('dashboard-tab-item-container', {
|
|
'dashboard-tab-item-container-active': isSelected
|
|
})}
|
|
>
|
|
<DashboardTabItem
|
|
tab={tab}
|
|
tabIndex={index}
|
|
isSelected={isSelected}
|
|
isDragging={isDragging}
|
|
htmlId={`dashboard-tab-item-${tab.id}`}
|
|
dropPosition={
|
|
isDropTarget
|
|
? dropTarget.insertBefore
|
|
? 'before'
|
|
: 'after'
|
|
: undefined
|
|
}
|
|
{...tabItemProps}
|
|
/>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className='dashboard-tabs-add-button-container'>
|
|
<Button
|
|
type='text'
|
|
className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag'
|
|
onClick={() => addTab()}
|
|
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
|
|
/>
|
|
</div>
|
|
</Flex>
|
|
</ScrollBox>
|
|
<div
|
|
ref={lineRef}
|
|
className='dashboard-tabs-sticky-line'
|
|
aria-hidden='true'
|
|
/>
|
|
{activeTab ? (
|
|
<div
|
|
ref={cloneRef}
|
|
className='dashboard-tab-sticky-clone dashboard-tab-item-container dashboard-tab-item-container-active'
|
|
>
|
|
<DashboardTabItem
|
|
tab={activeTab}
|
|
tabIndex={activeTabIndex}
|
|
isSelected
|
|
isDragging={
|
|
draggedValue != null &&
|
|
String(draggedValue) === String(activeTab.id)
|
|
}
|
|
{...tabItemProps}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</Flex>
|
|
)
|
|
}
|
|
|
|
export default DashboardTabs
|