fix(ui): a scroller with no edge to fade carries no mask
iPad WebKit kept a scrolled container's mask stuck to its content: at the bottom of a scrolled page the last strip, a table's pager, sat under a dark band. With nothing to fade there is no mask; isolation keeps its layer.
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { describe, it, expect, beforeAll, afterEach } from "vitest";
|
||||||
|
import { render, cleanup, fireEvent } from "@testing-library/react";
|
||||||
|
import { useScrollFade } from "./useScrollFade";
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
class Stub {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
||||||
|
});
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
function Scroller({ fadeTop = true }: { fadeTop?: boolean }) {
|
||||||
|
const fade = useScrollFade(20, fadeTop);
|
||||||
|
const { ref } = fade;
|
||||||
|
return <div data-testid="s" ref={ref} style={fade.style} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A scroller 100 px tall over 300 px of content, scrolled to `top`. */
|
||||||
|
function scrolledTo(el: HTMLElement, top: number) {
|
||||||
|
Object.defineProperties(el, {
|
||||||
|
clientHeight: { configurable: true, value: 100 },
|
||||||
|
scrollHeight: { configurable: true, value: 300 },
|
||||||
|
scrollTop: { configurable: true, value: top, writable: true },
|
||||||
|
});
|
||||||
|
fireEvent.scroll(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
// iPad WebKit kept a scrolled container's mask stuck to the content: at the bottom of a scrolled page
|
||||||
|
// the last strip (a table's pager) sat under a dark band. With no edge to fade there is no mask.
|
||||||
|
describe("useScrollFade", () => {
|
||||||
|
it("masks the edge that is clipped", () => {
|
||||||
|
const { getByTestId } = render(<Scroller />);
|
||||||
|
const el = getByTestId("s");
|
||||||
|
scrolledTo(el, 0);
|
||||||
|
expect(el.style.maskImage).toContain("calc(100% - 20px)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets no mask once nothing fades, and keeps the layer it made", () => {
|
||||||
|
const { getByTestId } = render(<Scroller fadeTop={false} />);
|
||||||
|
const el = getByTestId("s");
|
||||||
|
scrolledTo(el, 200);
|
||||||
|
expect(el.style.maskImage).toBe("");
|
||||||
|
expect(el.style.isolation).toBe("isolate");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,7 +18,12 @@ export function useScrollFade(
|
|||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
} {
|
} {
|
||||||
const { ref, start, end } = useScrollEdges("y");
|
const { ref, start, end } = useScrollEdges("y");
|
||||||
const mask = `linear-gradient(to bottom, transparent, #000 ${start && fadeTop ? fadePx : 0}px, #000 calc(100% - ${
|
// No edge to fade, no mask at all: iPad WebKit kept a scrolled container's mask stuck to the
|
||||||
|
// content, so at the bottom of a scrolled page the last strip (a table's pager) sat under a dark
|
||||||
|
// band. `isolation` keeps the stacking context the mask made, so nothing inside changes layer.
|
||||||
|
const top = start && fadeTop;
|
||||||
|
if (!top && !end) return { ref, style: { isolation: "isolate" } };
|
||||||
|
const mask = `linear-gradient(to bottom, transparent, #000 ${top ? fadePx : 0}px, #000 calc(100% - ${
|
||||||
end ? fadePx : 0
|
end ? fadePx : 0
|
||||||
}px), transparent)`;
|
}px), transparent)`;
|
||||||
return { ref, style: { maskImage: mask, WebkitMaskImage: mask } };
|
return { ref, style: { maskImage: mask, WebkitMaskImage: mask } };
|
||||||
|
|||||||
Reference in New Issue
Block a user