Skip to content

Commit

Permalink
using rounding in pixels in use-stick-header
Browse files Browse the repository at this point in the history
  • Loading branch information
dpitcock committed Feb 5, 2025
1 parent 5df9a5a commit bfd79ac
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 82 deletions.
153 changes: 79 additions & 74 deletions src/container/__tests__/sticky-header.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { act, renderHook } from '../../__tests__/render-hook';
import { ContainerProps } from '../interfaces';
import { useStickyHeader } from '../use-sticky-header';
jest.mock('../../../lib/components/container/use-sticky-header', () => ({
useStickyHeader: () => ({ isSticky: true }),
Expand All @@ -21,97 +22,101 @@ beforeEach(() => {
jest.resetAllMocks();
});

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 });

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

const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
describe.each(['embedded', 'full-page', 'cards', 'default', 'stacked'] as Array<
ContainerProps['variant'] | 'embedded' | 'full-page' | 'cards'
>)('useStickyHeader with variant %s', variant => {
test('should set isStuck properly when rootTop is less than headerTop', () => {
const rootRef = {
current: document.createElement('div'),
};
rootRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });

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

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

expect(result.current.isStuck).toBe(variant === 'full-page');

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

useStickyHeader with variant embedded › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

useStickyHeader with variant cards › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

useStickyHeader with variant default › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

useStickyHeader with variant stacked › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

useStickyHeader with variant embedded › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

useStickyHeader with variant cards › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

useStickyHeader with variant default › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)

Check failure on line 44 in src/container/__tests__/sticky-header.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

useStickyHeader with variant stacked › should set isStuck properly when rootTop is less than headerTop

expect(received).toBe(expected) // Object.is equality Expected: false Received: true at Object.<anonymous> (src/container/__tests__/sticky-header.test.tsx:44:36)
});

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

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

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 100 });
const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, variant, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('scroll'));
});

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.isStuck).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.style.borderTopWidth = '1px';

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

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.style.borderTopWidth = '1px';

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

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

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

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

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

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

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.isStuck).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 });

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

const headerRef = {
current: document.createElement('div'),
};
headerRef.current.getBoundingClientRect = jest.fn().mockReturnValue({ top: 200 });
const { result } = renderHook(() => useStickyHeader(rootRef, headerRef, variant, true, 0, 0, false));
act(() => {
window.dispatchEvent(new Event('resize'));
});

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.isStuck).toBe(false);
});
23 changes: 15 additions & 8 deletions src/container/use-sticky-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const StickyHeaderContext = createContext<StickyHeaderContextProps>({
export const useStickyHeader = (
rootRef: RefObject<HTMLDivElement>,
headerRef: RefObject<HTMLDivElement>,
variant: ContainerProps['variant'] | 'embedded' | 'full-page' | 'cards',
variant: ContainerProps['variant'] | 'embedded' | 'full-page' | 'cards' = 'default',
__stickyHeader?: boolean,
__stickyOffset?: number,
__mobileStickyOffset?: number,
Expand All @@ -59,10 +59,12 @@ export const useStickyHeader = (
// If it has overflow parents inside the app layout, we shouldn't apply a sticky offset.
const [hasInnerOverflowParents, setHasInnerOverflowParents] = useState(false);
const [isStuck, setIsStuck] = useState(false);

useLayoutEffect(() => {
if (rootRef.current) {
const overflowParents = getOverflowParents(rootRef.current);
const mainElement = findUpUntil(rootRef.current, elem => elem.tagName === 'MAIN');

// In both versions of the app layout, the scrolling element for disableBodyScroll
// is the <main>. If the closest overflow parent is also the closest <main> and we have
// offset values, it's safe to assume that it's the app layout scroll root and we
Expand Down Expand Up @@ -96,15 +98,20 @@ export const useStickyHeader = (
return;
}
if (rootRef.current && headerRef.current) {
const overflowParents = getOverflowParents(rootRef.current);
const mainElement = findUpUntil(rootRef.current, elem => elem.tagName === 'MAIN');
// const overflowParents = getOverflowParents(rootRef.current);
// const mainElement = findUpUntil(rootRef.current, elem => elem.tagName === 'MAIN');
const rootTopBorderWidth = parseFloat(getComputedStyle(rootRef.current).borderTopWidth) || 0;
const rootTop = rootRef.current.getBoundingClientRect().top + rootTopBorderWidth;
const headerTop = headerRef.current.getBoundingClientRect().top;
//using math.Round to adjust for rounding errors in floating-point arithmetic and timing issues
const rootTop = Math.round((rootRef.current.getBoundingClientRect().top + rootTopBorderWidth) * 10000) / 10000;
const headerTop = Math.round(headerRef.current.getBoundingClientRect().top * 10000) / 10000;

// If it has overflow parents inside the app layout, we ignore the expectation headerTop is at 0 for when table is in app layout.
const hasInnerOverflowParents = overflowParents.length > 0 && overflowParents[0] !== mainElement;
if ((variant === 'full-page' || headerTop === 0 || hasInnerOverflowParents) && rootTop < headerTop) {
// console.log({ headerTopDistance, currentHTD: rootTop - headerTop, overflowParents, mainElement, rootTopBorderWidth, rootTop, headerTop, variant, isStuck: (variant === 'full-page' || headerTop === 0 || hasInnerOverflowParents) && rootTop < headerTop})
if (
(variant === 'full-page' ||
headerTop === 0 || //when the header is at the top of the page
headerTop !== rootTop) &&
rootTop < headerTop
) {
setIsStuck(true);
} else {
setIsStuck(false);
Expand Down

0 comments on commit bfd79ac

Please sign in to comment.