Enhance Dashboard Tabs with Sticky Visuals and Improved Masking
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Added CSS styles for sticky tab behavior, including new masking effects for better visual integration during scrolling. - Updated JavaScript logic to manage sticky visuals and tab metrics, improving user interaction and responsiveness. - Adjusted padding and layout properties for dashboard tab items to enhance overall aesthetics and usability.
This commit is contained in:
parent
84db1f19b7
commit
9ca872e589
@ -3471,12 +3471,59 @@ body.objectKanbanColumnResizing * {
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
--tab-mask-fade: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tabs-wrap {
|
.dashboard-tabs-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tabs-wrap .simplebar-mask {
|
||||||
|
--tab-mask-left: 0px;
|
||||||
|
--tab-mask-right: 0px;
|
||||||
|
-webkit-clip-path: inset(0 var(--tab-mask-right) 0 var(--tab-mask-left));
|
||||||
|
clip-path: inset(0 var(--tab-mask-right) 0 var(--tab-mask-left));
|
||||||
|
-webkit-mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent var(--tab-mask-left),
|
||||||
|
#000 calc(var(--tab-mask-left) + var(--tab-mask-fade)),
|
||||||
|
#000 calc(100% - var(--tab-mask-right) - var(--tab-mask-fade)),
|
||||||
|
transparent calc(100% - var(--tab-mask-right))
|
||||||
|
);
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent var(--tab-mask-left),
|
||||||
|
#000 calc(var(--tab-mask-left) + var(--tab-mask-fade)),
|
||||||
|
#000 calc(100% - var(--tab-mask-right) - var(--tab-mask-fade)),
|
||||||
|
transparent calc(100% - var(--tab-mask-right))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tabs-wrap::before,
|
||||||
|
.dashboard-tabs-wrap::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--tab-mask-fade);
|
||||||
|
height: 1px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--color-header-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tabs-wrap::before {
|
||||||
|
left: 0;
|
||||||
|
-webkit-mask-image: linear-gradient(to right, #000, transparent);
|
||||||
|
mask-image: linear-gradient(to right, #000, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tabs-wrap::after {
|
||||||
|
right: 0.2px;
|
||||||
|
-webkit-mask-image: linear-gradient(to left, #000, transparent);
|
||||||
|
mask-image: linear-gradient(to left, #000, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tabs-wrap .simplebar-track.simplebar-vertical {
|
.dashboard-tabs-wrap .simplebar-track.simplebar-vertical {
|
||||||
@ -3498,7 +3545,7 @@ body.objectKanbanColumnResizing * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding-left: 14px;
|
padding-left: 20px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 42px;
|
max-height: 42px;
|
||||||
}
|
}
|
||||||
@ -3555,7 +3602,7 @@ body.objectKanbanColumnResizing * {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
margin: 5px 0 0 0;
|
margin: 5px 0 0 0;
|
||||||
padding: 0 8px 0 8px;
|
padding: 0 10px 0 10px;
|
||||||
border: 1px solid var(--color-list-view-tabs-border);
|
border: 1px solid var(--color-list-view-tabs-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -3741,11 +3788,33 @@ body.objectKanbanColumnResizing * {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-item-container[data-sticky-placeholder='true'] > .dashboard-tab-item {
|
.dashboard-tab-item-container[data-sticky-placeholder='true']
|
||||||
|
> .dashboard-tab-item {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-tabs[data-sticky-edge] .dashboard-tab-item-container:before,
|
||||||
|
.dashboard-tabs[data-sticky-edge] .dashboard-tabs-add-button-container:before,
|
||||||
|
.dashboard-tabs[data-sticky-edge] .dashboard-tabs-wrap::before,
|
||||||
|
.dashboard-tabs[data-sticky-edge] .dashboard-tabs-wrap::after {
|
||||||
|
content: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tabs-sticky-line {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 3;
|
||||||
|
border-bottom: 1px solid var(--color-header-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tabs-sticky-line[data-edge] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-tab-sticky-clone {
|
.dashboard-tab-sticky-clone {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@ -136,6 +136,8 @@ DashboardTabItem.propTypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STICKY_EDGE_INSET = 20
|
const STICKY_EDGE_INSET = 20
|
||||||
|
const STICKY_LINE_CORNER_GAP = 16
|
||||||
|
const STICKY_MASK_INSET = 58
|
||||||
|
|
||||||
const measureTabMetrics = (container) => {
|
const measureTabMetrics = (container) => {
|
||||||
const button = container.querySelector('.dashboard-tab-item')
|
const button = container.querySelector('.dashboard-tab-item')
|
||||||
@ -173,6 +175,45 @@ const hideStickyClone = (cloneEl) => {
|
|||||||
cloneEl.setAttribute('inert', '')
|
cloneEl.setAttribute('inert', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setSimplebarMaskInset = (maskEl, edge, inset) => {
|
||||||
|
if (!maskEl) return
|
||||||
|
if (!edge) {
|
||||||
|
maskEl.style.removeProperty('--tab-mask-left')
|
||||||
|
maskEl.style.removeProperty('--tab-mask-right')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (edge === 'left') {
|
||||||
|
maskEl.style.setProperty('--tab-mask-left', `${inset}px`)
|
||||||
|
maskEl.style.removeProperty('--tab-mask-right')
|
||||||
|
} else {
|
||||||
|
maskEl.style.removeProperty('--tab-mask-left')
|
||||||
|
maskEl.style.setProperty('--tab-mask-right', `${inset}px`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
const setOriginalStickyState = (container, sticky) => {
|
||||||
if (!container) return
|
if (!container) return
|
||||||
if (sticky) {
|
if (sticky) {
|
||||||
@ -184,11 +225,19 @@ const setOriginalStickyState = (container, sticky) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindActiveTabStickyScroll = (scrollEl, list, cloneEl) => {
|
const bindActiveTabStickyScroll = (scrollEl, list, cloneEl, root, lineEl) => {
|
||||||
const state = {
|
const state = {
|
||||||
activeEl: null,
|
activeEl: null,
|
||||||
updating: false
|
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 = () => {
|
const update = () => {
|
||||||
if (state.updating) return
|
if (state.updating) return
|
||||||
@ -204,7 +253,7 @@ const bindActiveTabStickyScroll = (scrollEl, list, cloneEl) => {
|
|||||||
state.activeEl = container
|
state.activeEl = container
|
||||||
|
|
||||||
if (!container || !cloneEl) {
|
if (!container || !cloneEl) {
|
||||||
hideStickyClone(cloneEl)
|
clearStickyVisuals()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,16 +262,22 @@ const bindActiveTabStickyScroll = (scrollEl, list, cloneEl) => {
|
|||||||
const minWidth = Math.min(metrics.minWidth, naturalWidth)
|
const minWidth = Math.min(metrics.minWidth, naturalWidth)
|
||||||
const maxShrink = Math.max(0, naturalWidth - minWidth)
|
const maxShrink = Math.max(0, naturalWidth - minWidth)
|
||||||
const scrollLeft = scrollEl.scrollLeft
|
const scrollLeft = scrollEl.scrollLeft
|
||||||
const viewportWidth = scrollEl.clientWidth
|
const viewportWidth = wrapperEl.clientWidth
|
||||||
if (viewportWidth <= 0) {
|
if (viewportWidth <= 0) {
|
||||||
setOriginalStickyState(container, false)
|
setOriginalStickyState(container, false)
|
||||||
hideStickyClone(cloneEl)
|
clearStickyVisuals()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollRect = scrollEl.getBoundingClientRect()
|
const wrapperRect = wrapperEl.getBoundingClientRect()
|
||||||
const containerRect = container.getBoundingClientRect()
|
const containerRect = container.getBoundingClientRect()
|
||||||
const naturalLeft = scrollLeft + (containerRect.left - scrollRect.left)
|
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 overflowLeft = scrollLeft + STICKY_EDGE_INSET - naturalLeft
|
||||||
const overflowRight =
|
const overflowRight =
|
||||||
naturalLeft +
|
naturalLeft +
|
||||||
@ -241,13 +296,15 @@ const bindActiveTabStickyScroll = (scrollEl, list, cloneEl) => {
|
|||||||
|
|
||||||
if (!edge) {
|
if (!edge) {
|
||||||
setOriginalStickyState(container, false)
|
setOriginalStickyState(container, false)
|
||||||
hideStickyClone(cloneEl)
|
clearStickyVisuals()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const shrink = Math.min(overflow, maxShrink)
|
const shrink = Math.min(overflow, maxShrink)
|
||||||
const visualWidth = Math.max(minWidth, naturalWidth - shrink)
|
const visualWidth = Math.max(minWidth, naturalWidth - shrink)
|
||||||
const fade = maxShrink === 0 ? 1 : 1 - shrink / maxShrink
|
const shrinkProgress = maxShrink === 0 ? 1 : shrink / maxShrink
|
||||||
|
const fade = 1 - shrinkProgress
|
||||||
|
const maskInset = STICKY_MASK_INSET * shrinkProgress
|
||||||
|
|
||||||
setOriginalStickyState(container, true)
|
setOriginalStickyState(container, true)
|
||||||
cloneEl.removeAttribute('inert')
|
cloneEl.removeAttribute('inert')
|
||||||
@ -256,16 +313,27 @@ const bindActiveTabStickyScroll = (scrollEl, list, cloneEl) => {
|
|||||||
else delete cloneEl.dataset.faded
|
else delete cloneEl.dataset.faded
|
||||||
|
|
||||||
const parentRect =
|
const parentRect =
|
||||||
cloneEl.offsetParent?.getBoundingClientRect() || scrollRect
|
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') {
|
if (edge === 'left') {
|
||||||
cloneEl.style.left = `${scrollRect.left - parentRect.left + STICKY_EDGE_INSET}px`
|
cloneEl.style.left = `${cloneOffset}px`
|
||||||
cloneEl.style.right = 'auto'
|
cloneEl.style.right = 'auto'
|
||||||
} else {
|
} else {
|
||||||
cloneEl.style.left = 'auto'
|
cloneEl.style.left = 'auto'
|
||||||
cloneEl.style.right = `${parentRect.right - scrollRect.right + STICKY_EDGE_INSET}px`
|
cloneEl.style.right = `${cloneOffset}px`
|
||||||
}
|
}
|
||||||
cloneEl.style.setProperty('--tab-sticky-width', `${visualWidth}px`)
|
cloneEl.style.setProperty('--tab-sticky-width', `${visualWidth}px`)
|
||||||
cloneEl.style.setProperty('--tab-label-opacity', String(fade))
|
cloneEl.style.setProperty('--tab-label-opacity', String(fade))
|
||||||
|
setSimplebarMaskInset(maskEl, edge, maskInset)
|
||||||
|
setStickyStripLine(
|
||||||
|
root,
|
||||||
|
lineEl,
|
||||||
|
edge,
|
||||||
|
cloneOffset + visualWidth + STICKY_LINE_CORNER_GAP
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
state.updating = false
|
state.updating = false
|
||||||
}
|
}
|
||||||
@ -286,6 +354,7 @@ const bindActiveTabStickyScroll = (scrollEl, list, cloneEl) => {
|
|||||||
|
|
||||||
const observer = new ResizeObserver(scheduleUpdate)
|
const observer = new ResizeObserver(scheduleUpdate)
|
||||||
observer.observe(scrollEl)
|
observer.observe(scrollEl)
|
||||||
|
if (wrapperEl !== scrollEl) observer.observe(wrapperEl)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
scrollEl.removeEventListener('scroll', scheduleUpdate)
|
scrollEl.removeEventListener('scroll', scheduleUpdate)
|
||||||
@ -293,7 +362,7 @@ const bindActiveTabStickyScroll = (scrollEl, list, cloneEl) => {
|
|||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
if (frame) window.cancelAnimationFrame(frame)
|
if (frame) window.cancelAnimationFrame(frame)
|
||||||
setOriginalStickyState(state.activeEl, false)
|
setOriginalStickyState(state.activeEl, false)
|
||||||
hideStickyClone(cloneEl)
|
clearStickyVisuals()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,12 +370,14 @@ const useActiveTabStickyScroll = (activeTabId, tabs) => {
|
|||||||
const rootRef = useRef(null)
|
const rootRef = useRef(null)
|
||||||
const listRef = useRef(null)
|
const listRef = useRef(null)
|
||||||
const cloneRef = useRef(null)
|
const cloneRef = useRef(null)
|
||||||
|
const lineRef = useRef(null)
|
||||||
const tabLayoutKey = `${activeTabId}:${tabs.map((tab) => `${tab.id}:${tab.title || ''}`).join('|')}`
|
const tabLayoutKey = `${activeTabId}:${tabs.map((tab) => `${tab.id}:${tab.title || ''}`).join('|')}`
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const root = rootRef.current
|
const root = rootRef.current
|
||||||
const list = listRef.current
|
const list = listRef.current
|
||||||
const cloneEl = cloneRef.current
|
const cloneEl = cloneRef.current
|
||||||
|
const lineEl = lineRef.current
|
||||||
if (!root || !list) return undefined
|
if (!root || !list) return undefined
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@ -320,7 +391,13 @@ const useActiveTabStickyScroll = (activeTabId, tabs) => {
|
|||||||
frame = window.requestAnimationFrame(tryBind)
|
frame = window.requestAnimationFrame(tryBind)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
teardown = bindActiveTabStickyScroll(scrollEl, list, cloneEl)
|
teardown = bindActiveTabStickyScroll(
|
||||||
|
scrollEl,
|
||||||
|
list,
|
||||||
|
cloneEl,
|
||||||
|
root,
|
||||||
|
lineEl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
tryBind()
|
tryBind()
|
||||||
@ -332,7 +409,7 @@ const useActiveTabStickyScroll = (activeTabId, tabs) => {
|
|||||||
}
|
}
|
||||||
}, [tabLayoutKey])
|
}, [tabLayoutKey])
|
||||||
|
|
||||||
return { rootRef, listRef, cloneRef }
|
return { rootRef, listRef, cloneRef, lineRef }
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardTabs = () => {
|
const DashboardTabs = () => {
|
||||||
@ -347,7 +424,7 @@ const DashboardTabs = () => {
|
|||||||
handleTabDragEnd,
|
handleTabDragEnd,
|
||||||
handleExternalTabDrop
|
handleExternalTabDrop
|
||||||
} = useNavigationTabs()
|
} = useNavigationTabs()
|
||||||
const { rootRef, listRef, cloneRef } = useActiveTabStickyScroll(
|
const { rootRef, listRef, cloneRef, lineRef } = useActiveTabStickyScroll(
|
||||||
activeTabId,
|
activeTabId,
|
||||||
tabs
|
tabs
|
||||||
)
|
)
|
||||||
@ -591,6 +668,11 @@ const DashboardTabs = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
</ScrollBox>
|
</ScrollBox>
|
||||||
|
<div
|
||||||
|
ref={lineRef}
|
||||||
|
className='dashboard-tabs-sticky-line'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
{activeTab ? (
|
{activeTab ? (
|
||||||
<div
|
<div
|
||||||
ref={cloneRef}
|
ref={cloneRef}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user