mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-11 03:12:55 +00:00
bottom color on modal
This commit is contained in:
1 parent
4e6f1bf9e4
commit
4e03bc0388
5 files changed
+128
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// Tested on iPhone (Safari 26, Low Power Mode too): in the app 6px works and
|
||||
// 4px doesn't (4px was enough on a bare test page, 2px never). One painted
|
||||
// frame is enough; a 16ms timer isn't, because it can fire before a paint
|
||||
// (Low Power Mode drops the refresh rate), so the strip lives for one frame.
|
||||
|
||||
/**
|
||||
* Makes iOS Safari re-colour its bottom bar from the page.
|
||||
*
|
||||
* Safari 26 takes the bottom bar's colour from a fixed element that appears on
|
||||
* the bottom edge (a bottom sheet turns it `bg-card`) and doesn't change it
|
||||
* back when that element goes away, or when an existing element changes
|
||||
* colour. A page-coloured strip that appears briefly on the edge makes Safari
|
||||
* sample again. The strip is 6px, no taller than the gap under the composer,
|
||||
* so it covers only background of its own colour. It has to be on top: a
|
||||
* strip behind the content, transparent or at `opacity-0` isn't sampled.
|
||||
*/
|
||||
function resetBottomTint(): void {
|
||||
const strip = document.createElement('div');
|
||||
strip.dataset.slot = 'bottom-tint-reset';
|
||||
strip.setAttribute('aria-hidden', 'true');
|
||||
strip.className =
|
||||
'bg-background pointer-events-none fixed inset-x-0 bottom-0 z-50 h-1.5';
|
||||
document.body.appendChild(strip);
|
||||
// The first frame paints the strip; remove it on the next.
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => strip.remove()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders nothing; resets Safari's bottom bar when it unmounts. Put it inside
|
||||
* a bottom sheet's Radix Content, which unmounts after its exit animation.
|
||||
*/
|
||||
function BottomTintReset(): null {
|
||||
React.useEffect(() => resetBottomTint, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
export { BottomTintReset, resetBottomTint };
|
||||
@@ -290,3 +290,45 @@ describe('Modal focus return', () => {
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal bottom-bar reset', () => {
|
||||
// Earlier tests close bottom sheets on unmount, which leaves strips behind.
|
||||
beforeEach(() => {
|
||||
document
|
||||
.querySelectorAll('[data-slot="bottom-tint-reset"]')
|
||||
.forEach((node) => node.remove());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
media.isMobile = false;
|
||||
});
|
||||
|
||||
const strip = () =>
|
||||
document.querySelector<HTMLElement>('[data-slot="bottom-tint-reset"]');
|
||||
|
||||
const renderModal = (open: boolean) =>
|
||||
render(
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={() => undefined}
|
||||
title="Upload"
|
||||
mobileVariant="sheet"
|
||||
>
|
||||
Body
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
it('resets the bottom bar when the phone sheet closes', async () => {
|
||||
media.isMobile = true;
|
||||
await renderModal(true);
|
||||
await renderModal(false);
|
||||
expect(strip()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('leaves the bar alone for the centred dialog', async () => {
|
||||
await renderModal(true);
|
||||
await renderModal(false);
|
||||
await settle();
|
||||
expect(strip()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { XIcon } from 'lucide-react';
|
||||
import { Dialog as DialogPrimitive, VisuallyHidden } from 'radix-ui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { BottomTintReset } from '@/components/ui/bottom-tint-reset';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -138,6 +139,7 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
|
||||
),
|
||||
)}
|
||||
>
|
||||
{isMobileSheet && <BottomTintReset />}
|
||||
{isMobileSheet && <SheetHandle />}
|
||||
{headerNode}
|
||||
<div
|
||||
|
||||
@@ -216,3 +216,46 @@ describe('SheetContent focus', () => {
|
||||
expect(document.activeElement).toBe(trigger());
|
||||
});
|
||||
});
|
||||
|
||||
describe('SheetContent bottom-bar reset', () => {
|
||||
// Earlier tests close bottom sheets on unmount, which leaves strips behind.
|
||||
beforeEach(() => {
|
||||
document
|
||||
.querySelectorAll('[data-slot="bottom-tint-reset"]')
|
||||
.forEach((node) => node.remove());
|
||||
});
|
||||
|
||||
const wait = (ms: number) =>
|
||||
act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
});
|
||||
const strip = () =>
|
||||
document.querySelector<HTMLElement>('[data-slot="bottom-tint-reset"]');
|
||||
|
||||
const renderSheet = (open: boolean, side: 'bottom' | 'right') =>
|
||||
render(
|
||||
<Sheet open={open}>
|
||||
<SheetContent side={side} title="Tools" aria-describedby={undefined} />
|
||||
</Sheet>,
|
||||
);
|
||||
|
||||
it('shows a 6px page-coloured strip on the bottom edge for one frame after a bottom sheet closes', async () => {
|
||||
await renderSheet(true, 'bottom');
|
||||
expect(strip()).toBeNull();
|
||||
await renderSheet(false, 'bottom');
|
||||
const classes = strip()!.className.split(' ');
|
||||
expect(classes).toEqual(
|
||||
expect.arrayContaining(['bg-background', 'fixed', 'bottom-0', 'h-1.5']),
|
||||
);
|
||||
expect(strip()!.getAttribute('aria-hidden')).toBe('true');
|
||||
await wait(120);
|
||||
expect(strip()).toBeNull();
|
||||
});
|
||||
|
||||
it('adds no strip for a side sheet', async () => {
|
||||
await renderSheet(true, 'right');
|
||||
await renderSheet(false, 'right');
|
||||
await wait(20);
|
||||
expect(strip()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { Dialog as SheetPrimitive } from 'radix-ui';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { BottomTintReset } from '@/components/ui/bottom-tint-reset';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn, overlayScrim } from '@/lib/utils';
|
||||
import { useFocusReturn } from '@/components/ui/use-focus-return';
|
||||
@@ -121,6 +122,7 @@ function SheetContent({
|
||||
{...focusReturn}
|
||||
>
|
||||
{title ? <SheetTitle className="sr-only">{title}</SheetTitle> : null}
|
||||
{side === 'bottom' && <BottomTintReset />}
|
||||
{handle && <SheetHandle />}
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
|
||||
Reference in new issue
Block a user