Skip to content

Commit

Permalink
fix: fixing broken corners of table with sticky header bottom when sc…
Browse files Browse the repository at this point in the history
…rolled (#3291)
  • Loading branch information
dpitcock authored Feb 13, 2025
1 parent f8f32c5 commit 187f672
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 27 deletions.
154 changes: 132 additions & 22 deletions src/container/__tests__/sticky-header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,179 +26,188 @@ test('should set isStuck to false when __stickyHeader is false', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -200 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -200, bottom: -100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, false, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to false when __disableMobile is false', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -200 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -200, bottom: -100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, true));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to false when rootTop headerTop are equal and headerTop is not 0', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 200 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to true when rootTop less than a headerTop at 0', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -100 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -100, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(true);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to false when rootTop and headerTop are equal at 0', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to true when rootTop is less than headerTop', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(true);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to false when rootTop is larger than than nonZero headerTop', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 150 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to false when rootTop is larger than than zero headerTop', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200, bottom: 800 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should not set isStuck to true when rootTop has a border and is larger than than headerTop', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 199 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 199, bottom: 800 });
rootRef.current.style.borderTopWidth = '1px';

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuck to false when headerRef is null', () => {
test('should set isStuck and isStuckAtBottom to false when headerRef is null', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200, bottom: 500 });

const headerRef = {
current: null,
Expand All @@ -210,23 +219,124 @@ test('should set isStuck to false when headerRef is null', () => {
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should not react to synthetic window resize events', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 500 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200 });
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('resize'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuckAtBottom to true when rootBottom equals headerBottom', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -500, bottom: 100 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(true);
expect(result.current.isStuckAtBottom).toBe(true);
});

test('should set isStuckAtBottom to true when rootBottom less than headerBottom', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -500, bottom: 100 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0, bottom: 200 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(true);
expect(result.current.isStuckAtBottom).toBe(true);
});

test('should set isStuckAtBottom to false when rootBottom larger than headerBottom', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -500, bottom: 200 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 0, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(true);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuckAtBottom to false when rootBottom larger than headerBottom and headerTop is not 0', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -500, bottom: 200 });

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100, bottom: 100 });

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(true);
expect(result.current.isStuckAtBottom).toBe(false);
});

test('should set isStuckAtBottom to false when headerRef is null', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: -500, bottom: 200 });

const headerRef = {
current: null,
};

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

expect(result.current.isStuck).toBe(false);
expect(result.current.isStuckAtBottom).toBe(false);
});
5 changes: 3 additions & 2 deletions src/container/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function InternalContainer({
const baseProps = getBaseProps(restProps);
const rootRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const { isSticky, isStuck, stickyStyles } = useStickyHeader(
const { isSticky, isStuck, isStuckAtBottom, stickyStyles } = useStickyHeader(
rootRef,
headerRef,
__stickyHeader,
Expand Down Expand Up @@ -117,6 +117,7 @@ export default function InternalContainer({
fitHeight && styles['fit-height'],
hasMedia && (mediaPosition === 'side' ? styles['with-side-media'] : styles['with-top-media']),
shouldHaveStickyStyles && [styles['sticky-enabled']],
shouldHaveStickyStyles && isStuck && isStuckAtBottom && [styles['with-stuck-sticky-header-at-bottom']],
isRefresh && styles.refresh
)}
ref={mergedRef}
Expand All @@ -139,7 +140,7 @@ export default function InternalContainer({
>
{header && (
<ContainerHeaderContextProvider>
<StickyHeaderContext.Provider value={{ isStuck }}>
<StickyHeaderContext.Provider value={{ isStuck, isStuckAtBottom }}>
<div
className={clsx(
isRefresh && styles.refresh,
Expand Down
5 changes: 5 additions & 0 deletions src/container/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
inset-block-start: calc(-1 * #{awsui.$border-divider-section-width});
}
}

&.with-stuck-sticky-header-at-bottom {
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}

.with-side-media {
Expand Down
Loading

0 comments on commit 187f672

Please sign in to comment.