Skip to content

Commit b3961c7

Browse files
avinashbotjohannes-weber
authored andcommitted
Add checks to hide arrows if obscured behind sticky columns.
1 parent 3526c0a commit b3961c7

File tree

3 files changed

+107
-17
lines changed

3 files changed

+107
-17
lines changed

src/internal/components/drag-handle-wrapper/motion.scss

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,20 @@
7171
#{custom-props.$dragHandleAnimationBlockOffset}: -20px;
7272
}
7373

74-
.direction-button-wrapper-inline-start,
75-
.direction-button-wrapper-inline-end.direction-button-wrapper-rtl {
76-
#{custom-props.$dragHandleAnimationInlineOffset}: 20px;
74+
.direction-button-wrapper-inline-start {
75+
@include styles.with-direction('ltr') {
76+
#{custom-props.$dragHandleAnimationInlineOffset}: 20px;
77+
}
78+
@include styles.with-direction('rtl') {
79+
#{custom-props.$dragHandleAnimationInlineOffset}: -20px;
80+
}
7781
}
7882

79-
.direction-button-wrapper-inline-end,
80-
.direction-button-wrapper-inline-start.direction-button-wrapper-rtl {
81-
#{custom-props.$dragHandleAnimationInlineOffset}: -20px;
83+
.direction-button-wrapper-inline-end {
84+
@include styles.with-direction('ltr') {
85+
#{custom-props.$dragHandleAnimationInlineOffset}: -20px;
86+
}
87+
@include styles.with-direction('rtl') {
88+
#{custom-props.$dragHandleAnimationInlineOffset}: 20px;
89+
}
8290
}

src/table/resizer/index.tsx

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import DragHandleWrapper from '../../internal/components/drag-handle-wrapper';
1111
import { useVisualRefresh } from '../../internal/hooks/use-visual-mode';
1212
import { KeyCode } from '../../internal/keycode';
1313
import handleKey, { isEventLike } from '../../internal/utils/handle-key';
14+
import { scrollElementIntoView } from '../../internal/utils/scrollable-containers';
1415
import { DEFAULT_COLUMN_WIDTH } from '../use-column-widths';
1516
import { getHeaderWidth, getResizerElements } from './resizer-lookup';
1617

@@ -53,12 +54,14 @@ export function Resizer({
5354
const isVisualRefresh = useVisualRefresh();
5455

5556
const separatorId = useUniqueId();
56-
const resizerToggleRef = useRef<HTMLButtonElement>(null);
57-
const resizerSeparatorRef = useRef<HTMLSpanElement>(null);
57+
const positioningWrapperRef = useRef<HTMLDivElement | null>(null);
58+
const resizerToggleRef = useRef<HTMLButtonElement | null>(null);
59+
const resizerSeparatorRef = useRef<HTMLSpanElement | null>(null);
5860

5961
const [isPointerDown, setIsPointerDown] = useState(false);
6062
const [isDragging, setIsDragging] = useState(false);
6163
const [showUapButtons, setShowUapButtons] = useState(false);
64+
const [resizerObscured, setResizerObscured] = useState(false);
6265
const [isKeyboardDragging, setIsKeyboardDragging] = useState(false);
6366
const autoGrowTimeout = useRef<ReturnType<typeof setTimeout> | undefined>();
6467
const [resizerHasFocus, setResizerHasFocus] = useState(false);
@@ -69,6 +72,70 @@ export function Resizer({
6972
setHeaderCellWidth(getHeaderWidth(resizerToggleRef.current));
7073
}, []);
7174

75+
const isObscured = useCallback(() => {
76+
const elements = getResizerElements(resizerToggleRef.current);
77+
if (!elements || !positioningWrapperRef.current) {
78+
return false;
79+
}
80+
81+
let scrollPaddingInlineStart = 0;
82+
let scrollPaddingInlineEnd = 0;
83+
84+
// Calculate size of the headers at the exact moment of the call to deal
85+
// with auto-width columns and cached sticky column state.
86+
elements.allHeaders.forEach(header => {
87+
const { inlineSize } = getLogicalBoundingClientRect(header);
88+
if (header.style.insetInlineStart) {
89+
scrollPaddingInlineStart += inlineSize;
90+
}
91+
if (header.style.insetInlineEnd) {
92+
scrollPaddingInlineEnd += inlineSize;
93+
}
94+
});
95+
96+
const { insetInlineStart: scrollParentInsetInlineStart, insetInlineEnd: scrollParentInsetInlineEnd } =
97+
getLogicalBoundingClientRect(elements.scrollParent);
98+
const { insetInlineStart, insetInlineEnd, inlineSize } = getLogicalBoundingClientRect(elements.header);
99+
const relativeInsetInlineStart = insetInlineStart - scrollParentInsetInlineStart;
100+
const relativeInsetInlineEnd = scrollParentInsetInlineEnd - insetInlineEnd;
101+
const isSticky = !!elements.header.style.insetInlineStart || !!elements.header.style.insetInlineEnd;
102+
103+
return (
104+
// Is positioningWrapper obscured behind the left edge of scrollParent?
105+
Math.ceil(relativeInsetInlineStart + inlineSize) < (isSticky ? 0 : scrollPaddingInlineStart) ||
106+
// Is positioningWrapper obscured behind the right edge of scrollParent?
107+
Math.ceil(relativeInsetInlineEnd) < (isSticky ? 0 : scrollPaddingInlineEnd)
108+
);
109+
}, []);
110+
111+
const scrollIntoViewIfNeeded = useCallback(() => {
112+
if (resizerSeparatorRef.current && isObscured()) {
113+
scrollElementIntoView(resizerSeparatorRef.current);
114+
}
115+
}, [isObscured]);
116+
117+
useEffect(() => {
118+
if (!showUapButtons) {
119+
setResizerObscured(false);
120+
return;
121+
}
122+
123+
const elements = getResizerElements(resizerToggleRef.current);
124+
if (!elements) {
125+
return;
126+
}
127+
128+
const onScroll = () => setResizerObscured(isObscured());
129+
elements.scrollParent.addEventListener('scroll', onScroll);
130+
return () => elements.scrollParent.removeEventListener('scroll', onScroll);
131+
}, [isObscured, showUapButtons]);
132+
133+
useEffect(() => {
134+
if (showUapButtons) {
135+
setResizerObscured(isObscured());
136+
}
137+
}, [headerCellWidth, isObscured, showUapButtons]);
138+
72139
const updateTrackerPosition = useCallback((newOffset: number) => {
73140
const elements = getResizerElements(resizerToggleRef.current);
74141
if (!elements) {
@@ -180,9 +247,11 @@ export function Resizer({
180247
},
181248
onInlineStart: () => {
182249
updateColumnWidth(getLogicalBoundingClientRect(elements.header).inlineSize - 10);
250+
scrollIntoViewIfNeeded();
183251
},
184252
onInlineEnd: () => {
185253
updateColumnWidth(getLogicalBoundingClientRect(elements.header).inlineSize + 10);
254+
scrollIntoViewIfNeeded();
186255
},
187256
});
188257
}
@@ -234,6 +303,7 @@ export function Resizer({
234303
isKeyboardDragging,
235304
isPointerDown,
236305
resizerHasFocus,
306+
scrollIntoViewIfNeeded,
237307
onWidthUpdateCommit,
238308
resizeColumn,
239309
updateColumnWidth,
@@ -249,18 +319,22 @@ export function Resizer({
249319
const { tabIndex: resizerTabIndex } = useSingleTabStopNavigation(resizerToggleRef, { tabIndex });
250320

251321
return (
252-
<div className={clsx(styles['resizer-wrapper'], isVisualRefresh && styles['is-visual-refresh'])}>
322+
<div
323+
className={clsx(styles['resizer-wrapper'], isVisualRefresh && styles['is-visual-refresh'])}
324+
ref={positioningWrapperRef}
325+
>
253326
<DragHandleWrapper
254327
clickDragThreshold={3}
255328
hideButtonsOnDrag={false}
256329
directions={{
257-
'inline-start': headerCellWidth > minWidth ? 'active' : 'disabled',
258-
'inline-end': 'active',
330+
'inline-start': resizerObscured ? undefined : headerCellWidth > minWidth ? 'active' : 'disabled',
331+
'inline-end': resizerObscured ? undefined : 'active',
259332
}}
260333
triggerMode="controlled"
261-
controlledShowButtons={showUapButtons}
334+
controlledShowButtons={showUapButtons && !resizerObscured}
262335
wrapperClassName={styles['resizer-button-wrapper']}
263336
tooltipText={tooltipText}
337+
data-obscured={resizerObscured}
264338
onDirectionClick={direction => {
265339
const elements = getResizerElements(resizerToggleRef.current);
266340
if (!elements) {
@@ -269,10 +343,16 @@ export function Resizer({
269343

270344
if (direction === 'inline-start') {
271345
updateColumnWidth(getLogicalBoundingClientRect(elements.header).inlineSize - 20);
272-
requestAnimationFrame(onWidthUpdateCommit);
346+
requestAnimationFrame(() => {
347+
onWidthUpdateCommit();
348+
scrollIntoViewIfNeeded();
349+
});
273350
} else if (direction === 'inline-end') {
274351
updateColumnWidth(getLogicalBoundingClientRect(elements.header).inlineSize + 20);
275-
requestAnimationFrame(onWidthUpdateCommit);
352+
requestAnimationFrame(() => {
353+
onWidthUpdateCommit();
354+
scrollIntoViewIfNeeded();
355+
});
276356
}
277357
}}
278358
>

src/table/resizer/resizer-lookup.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function getResizerElements(resizerElement: null | HTMLElement) {
1414
return null;
1515
}
1616

17-
const header = findUpUntil(resizerElement, element => element.tagName.toLowerCase() === 'th');
17+
const header = findUpUntil(resizerElement, element => element.tagName === 'TH');
1818
if (!header) {
1919
return null;
2020
}
@@ -24,11 +24,13 @@ export function getResizerElements(resizerElement: null | HTMLElement) {
2424
return null;
2525
}
2626

27-
const table = tableRoot.querySelector<HTMLElement>(`table`);
27+
const table = tableRoot.querySelector<HTMLTableElement>(`table`);
2828
if (!table) {
2929
return null;
3030
}
3131

32+
const allHeaders = tableRoot.querySelectorAll<HTMLTableCellElement>(`thead th`);
33+
3234
// tracker is rendered inside table wrapper to align with its size
3335
const tracker = tableRoot.querySelector<HTMLElement>(`.${resizerStyles.tracker}`);
3436
if (!tracker) {
@@ -40,7 +42,7 @@ export function getResizerElements(resizerElement: null | HTMLElement) {
4042
return null;
4143
}
4244

43-
return { header, table, tracker, scrollParent };
45+
return { header, table, allHeaders, tracker, scrollParent };
4446
}
4547

4648
export function getHeaderWidth(resizerElement: null | HTMLElement): number {

0 commit comments

Comments
 (0)