mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
Merge origin/main (v1.44.2) into fix/v145-sheet-motion
next stays on main's 16.4.2 (the RCE and Server Actions advisories) with its two companions; release's newer dev tools and its own additions are kept. The lockfile is release's, re-resolved. pt-BR, which main does not have, takes the one key main added.
This commit is contained in:
commit
9c8972bc96
86 files changed
+5051
-487
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* The bottom sheet's anatomy, rendered statically: the grip, the 44 px X on
|
||||
* the title row, the preview step's wiring, and that every bottom sheet
|
||||
* (`<SheetContent side="bottom">`) takes this path while the other sides do
|
||||
* not. Radix portals do not render on the server, so its Dialog parts are
|
||||
* passed through as plain elements; what Radix itself does (focus, Escape,
|
||||
* the backdrop) and the drag in a browser are `e2e/bottom-sheet.spec.ts`.
|
||||
*/
|
||||
|
||||
vi.mock("@/lib/i18n/context", () => ({
|
||||
useTranslations: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock("radix-ui", () => {
|
||||
type P = { children?: ReactNode; [key: string]: unknown };
|
||||
const strip = ({ asChild: _asChild, ...rest }: P) => rest;
|
||||
return {
|
||||
Dialog: {
|
||||
Root: ({ children }: P) => <>{children}</>,
|
||||
Portal: ({ children }: P) => <>{children}</>,
|
||||
Overlay: (props: P) => <div data-mock="overlay" {...strip(props)} />,
|
||||
Content: ({ children, ...props }: P) => (
|
||||
<div role="dialog" {...strip(props)}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Close: ({ children, ...props }: P) => (
|
||||
<button type="button" {...strip(props)}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Title: ({ children }: P) => <h2>{children}</h2>,
|
||||
Description: ({ children }: P) => <p>{children}</p>,
|
||||
Trigger: ({ children }: P) => <>{children}</>,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { BottomSheetContent, BOTTOM_SHEET_CLASS } =
|
||||
await import("../bottom-sheet");
|
||||
const { SheetContent } = await import("../sheet");
|
||||
|
||||
function attr(html: string, slot: string): string {
|
||||
const match = new RegExp(`<[^>]*data-slot="${slot}"[^>]*>`).exec(html);
|
||||
return match?.[0] ?? "";
|
||||
}
|
||||
|
||||
describe("<BottomSheetContent>", () => {
|
||||
it("draws a decorative grip and a 44 px X on the title row", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<BottomSheetContent>body</BottomSheetContent>,
|
||||
);
|
||||
const grip = attr(html, "sheet-grip");
|
||||
expect(grip).toContain('aria-hidden="true"');
|
||||
expect(grip).toContain("h-6");
|
||||
expect(grip).toContain("touch-none");
|
||||
expect(html).toContain("h-1.25 w-9 rounded-full");
|
||||
const close = attr(html, "sheet-close-button");
|
||||
expect(close).toContain("size-11");
|
||||
expect(close).toContain("top-7.5");
|
||||
expect(html).toContain("common.close");
|
||||
// The swipe closes through a Radix Close of its own, out of reach.
|
||||
const swipe = attr(html, "sheet-swipe-close");
|
||||
expect(swipe).toContain("hidden");
|
||||
expect(swipe).toContain('tabindex="-1"');
|
||||
});
|
||||
|
||||
it("keeps its backdrop out of touch panning", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<BottomSheetContent>body</BottomSheetContent>,
|
||||
);
|
||||
expect(attr(html, "sheet-overlay")).toContain("touch-none");
|
||||
});
|
||||
|
||||
it("drops the grip, the drag and the X offset when it is not draggable", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<BottomSheetContent draggable={false}>body</BottomSheetContent>,
|
||||
);
|
||||
expect(html).not.toContain('data-slot="sheet-grip"');
|
||||
expect(html).not.toContain('data-slot="sheet-swipe-close"');
|
||||
expect(attr(html, "sheet-close-button")).toContain("top-3");
|
||||
});
|
||||
|
||||
it("opens a preview sheet on its preview step, with a grip that expands it", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<BottomSheetContent
|
||||
preview="58dvh"
|
||||
previewLabels={{ expand: "Expand", shrink: "Shrink" }}
|
||||
>
|
||||
body
|
||||
</BottomSheetContent>,
|
||||
);
|
||||
const content = attr(html, "sheet-content");
|
||||
expect(content).toContain('data-sheet-snap="preview"');
|
||||
expect(content).toContain("--sheet-height:90dvh");
|
||||
expect(content).toContain("--sheet-preview-height:58dvh");
|
||||
expect(content).toContain("h-(--sheet-height)");
|
||||
const grip = attr(html, "sheet-grip");
|
||||
expect(grip).toMatch(/^<button/);
|
||||
expect(grip).toContain('aria-label="Expand"');
|
||||
expect(grip).toContain('aria-expanded="false"');
|
||||
});
|
||||
|
||||
it("names its side for the keyboard rule and lets a consumer slot win", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<BottomSheetContent data-slot="mine" className="bg-card gap-0">
|
||||
body
|
||||
</BottomSheetContent>,
|
||||
);
|
||||
expect(html).toContain('data-sheet-side="bottom"');
|
||||
expect(html).toContain('data-slot="mine"');
|
||||
const content = attr(html, "mine");
|
||||
expect(content).toMatch(/\bbg-card\b/);
|
||||
expect(content).not.toContain("bg-background");
|
||||
expect(content).not.toMatch(/\bgap-4\b/);
|
||||
});
|
||||
|
||||
it("moves, opens and closes on its own timing and stays still under reduced motion", () => {
|
||||
expect(BOTTOM_SHEET_CLASS).toContain("data-[state=open]:duration-260");
|
||||
expect(BOTTOM_SHEET_CLASS).toContain("data-[state=closed]:duration-220");
|
||||
expect(BOTTOM_SHEET_CLASS).toContain("ease-out");
|
||||
expect(BOTTOM_SHEET_CLASS).toContain("motion-reduce:animate-none");
|
||||
expect(BOTTOM_SHEET_CLASS).toContain("max-h-[90dvh]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SheetContent>", () => {
|
||||
it("renders every bottom sheet through the bottom sheet", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<SheetContent side="bottom">body</SheetContent>,
|
||||
);
|
||||
expect(html).toContain('data-slot="sheet-grip"');
|
||||
expect(html).toContain('data-sheet-side="bottom"');
|
||||
});
|
||||
|
||||
it("leaves the side sheets as they were", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<SheetContent side="right" preview="58dvh">
|
||||
body
|
||||
</SheetContent>,
|
||||
);
|
||||
expect(html).not.toContain('data-slot="sheet-grip"');
|
||||
expect(html).not.toContain("data-sheet-snap");
|
||||
expect(html).toContain('data-sheet-side="right"');
|
||||
expect(attr(html, "sheet-close-button")).toContain("top-3 right-4");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,292 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { Dialog as SheetPrimitive } from "radix-ui";
|
||||
|
||||
import { useSheetDrag } from "@/hooks/use-sheet-drag";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { SheetSnap } from "@/lib/sheet-gesture";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* The bottom sheet: every `<SheetContent side="bottom">` renders through
|
||||
* here, and so does `ResponsiveSheet` on a phone.
|
||||
*
|
||||
* It is Radix's Dialog with a drag on top. Focus moving in, the focus trap,
|
||||
* Escape, the backdrop tap, `role="dialog"` and its name, and the scroll
|
||||
* lock on the page behind all stay Radix's. What this adds:
|
||||
*
|
||||
* - focus goes back to whatever opened the sheet (Radix only knows a
|
||||
* `Dialog.Trigger`, which most openers are not), unless the caller's
|
||||
* `onCloseAutoFocus` places it itself; and `aria-modal`;
|
||||
* - a grip at the top edge; the grip and the header drag the sheet always,
|
||||
* the content only while it is scrolled to its very top
|
||||
* (`use-sheet-drag.ts`, rules in `lib/sheet-gesture.ts`);
|
||||
* - a release past a quarter of the height, or a flick down, closes it,
|
||||
* anything less springs back; sideways movement never counts;
|
||||
* - the backdrop lightens as the sheet goes down, and neither it nor the
|
||||
* grip and header ever scroll or pull the page behind;
|
||||
* - opt in, a preview step (`preview`): the sheet opens at that height and
|
||||
* a pull up, or a tap on the grip, takes it to the full 90 dvh. Its
|
||||
* footer (`data-sheet-footer`) stays on the window's bottom edge and its
|
||||
* scrolling body (`data-sheet-body`) ends above it on either step.
|
||||
*
|
||||
* The X sits on the title row, 44 px square; the keyboard and a screen
|
||||
* reader close the sheet through it or Escape, never through a gesture.
|
||||
*/
|
||||
|
||||
export interface BottomSheetOptions {
|
||||
/**
|
||||
* Opens the sheet at this height (a CSS length, e.g. `58dvh`) with the
|
||||
* full 90 dvh one pull up. Needs `previewLabels` for the grip, which then
|
||||
* becomes a button that toggles the two steps.
|
||||
*/
|
||||
preview?: string;
|
||||
previewLabels?: { expand: string; shrink: string };
|
||||
/**
|
||||
* False leaves out the grip and the drag, for a sheet that fills the whole
|
||||
* window (the command palette): there is no backdrop to pull it towards.
|
||||
*/
|
||||
draggable?: boolean;
|
||||
}
|
||||
|
||||
export type BottomSheetContentProps = React.ComponentProps<
|
||||
typeof SheetPrimitive.Content
|
||||
> &
|
||||
BottomSheetOptions & { showCloseButton?: boolean };
|
||||
|
||||
/**
|
||||
* No `bottom-*` here: the sheet's `bottom` comes from the keyboard rule in
|
||||
* `globals.css`, which a utility would outrank. In 260 ms, out 220 ms, both
|
||||
* easing out; no slide at all under reduced motion. The `after:` strip
|
||||
* continues the sheet's own background below its edge, so the short
|
||||
* resisting travel above the top never shows the page underneath.
|
||||
*/
|
||||
export const BOTTOM_SHEET_CLASS =
|
||||
"bg-background fixed inset-x-0 z-50 flex max-h-[90dvh] flex-col gap-4 border-t shadow-lg transition-transform ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom data-[state=open]:duration-260 data-[state=closed]:duration-220 motion-reduce:animate-none motion-reduce:transition-none after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-10 after:bg-inherit after:content-['']";
|
||||
|
||||
const OVERLAY_CLASS =
|
||||
"fixed inset-0 z-50 touch-none bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 ease-out data-[state=open]:duration-260 data-[state=closed]:duration-220 motion-reduce:animate-none";
|
||||
|
||||
/** Puts a preview sheet back on its preview step each time it opens. */
|
||||
function OnOpen({ run }: { run: () => void }) {
|
||||
React.useLayoutEffect(() => {
|
||||
run();
|
||||
// Once per mount, which is once per opening.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
function SheetGrip({
|
||||
snap,
|
||||
labels,
|
||||
onToggle,
|
||||
}: {
|
||||
snap: SheetSnap;
|
||||
labels?: { expand: string; shrink: string };
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const bar = (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="bg-muted-foreground/40 h-1.25 w-9 rounded-full"
|
||||
/>
|
||||
);
|
||||
if (labels) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="sheet-grip"
|
||||
aria-label={snap === "full" ? labels.shrink : labels.expand}
|
||||
aria-expanded={snap === "full"}
|
||||
onClick={onToggle}
|
||||
className="focus-visible:ring-ring/50 mx-auto flex h-6 w-16 shrink-0 touch-none items-center justify-center rounded-full focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
{bar}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-grip"
|
||||
aria-hidden="true"
|
||||
className="flex h-6 shrink-0 touch-none items-center justify-center"
|
||||
>
|
||||
{bar}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BottomSheetContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
preview,
|
||||
previewLabels,
|
||||
draggable = true,
|
||||
ref,
|
||||
style,
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
onPointerCancel,
|
||||
onClickCapture,
|
||||
onOpenAutoFocus,
|
||||
onCloseAutoFocus,
|
||||
...props
|
||||
}: BottomSheetContentProps) {
|
||||
const { t } = useTranslations();
|
||||
const contentRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const overlayRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const probeRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const closeRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
// What had focus when the sheet opened. Radix hands focus back only to a
|
||||
// `Dialog.Trigger`, and most bottom sheets open from a plain button (the
|
||||
// More tab, a chart's tooltip), so focus fell to the body on close.
|
||||
const openerRef = React.useRef<HTMLElement | null>(null);
|
||||
const initial: SheetSnap = preview ? "preview" : "full";
|
||||
const [snap, setSnap] = React.useState<SheetSnap>(initial);
|
||||
const [node, setNode] = React.useState<HTMLDivElement | null>(null);
|
||||
|
||||
const drag = useSheetDrag({
|
||||
enabled: draggable,
|
||||
hasPreview: !!preview,
|
||||
snap,
|
||||
onSnap: setSnap,
|
||||
onClose: () => closeRef.current?.click(),
|
||||
contentRef,
|
||||
overlayRef,
|
||||
probeRef,
|
||||
node,
|
||||
});
|
||||
|
||||
const setContent = React.useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
contentRef.current = node;
|
||||
setNode(node);
|
||||
if (typeof ref === "function") ref(node);
|
||||
else if (ref) ref.current = node;
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
return (
|
||||
<SheetPrimitive.Portal data-slot="sheet-portal">
|
||||
<SheetPrimitive.Overlay
|
||||
ref={overlayRef}
|
||||
data-slot="sheet-overlay"
|
||||
className={OVERLAY_CLASS}
|
||||
/>
|
||||
<SheetPrimitive.Content
|
||||
ref={setContent}
|
||||
data-slot="sheet-content"
|
||||
// Read by the keyboard rule in `globals.css`: a bottom sheet stands
|
||||
// on the on-screen keyboard. Its own attribute rather than
|
||||
// `data-slot`, which consumers replace with theirs.
|
||||
data-sheet-side="bottom"
|
||||
data-sheet-snap={preview ? snap : undefined}
|
||||
// Radix makes the page behind inert and traps focus; this says so
|
||||
// to assistive technology that reads the attribute rather than the
|
||||
// hidden siblings.
|
||||
aria-modal="true"
|
||||
style={
|
||||
preview
|
||||
? ({
|
||||
"--sheet-height": "90dvh",
|
||||
"--sheet-preview-height": preview,
|
||||
...style,
|
||||
} as React.CSSProperties)
|
||||
: style
|
||||
}
|
||||
className={cn(
|
||||
BOTTOM_SHEET_CLASS,
|
||||
preview && "h-(--sheet-height) max-h-(--sheet-height)",
|
||||
className,
|
||||
)}
|
||||
onPointerDown={(event) => {
|
||||
onPointerDown?.(event);
|
||||
drag.onPointerDown?.(event);
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
onPointerMove?.(event);
|
||||
drag.onPointerMove?.(event);
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
onPointerUp?.(event);
|
||||
drag.onPointerUp?.(event);
|
||||
}}
|
||||
onPointerCancel={(event) => {
|
||||
onPointerCancel?.(event);
|
||||
drag.onPointerCancel?.(event);
|
||||
}}
|
||||
onClickCapture={(event) => {
|
||||
drag.onClickCapture?.(event);
|
||||
onClickCapture?.(event);
|
||||
}}
|
||||
onOpenAutoFocus={(event) => {
|
||||
const active = document.activeElement;
|
||||
openerRef.current =
|
||||
active instanceof HTMLElement && active !== document.body
|
||||
? active
|
||||
: null;
|
||||
onOpenAutoFocus?.(event);
|
||||
}}
|
||||
onCloseAutoFocus={(event) => {
|
||||
onCloseAutoFocus?.(event);
|
||||
const opener = openerRef.current;
|
||||
if (event.defaultPrevented || !opener?.isConnected) return;
|
||||
event.preventDefault();
|
||||
opener.focus({ preventScroll: true });
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{preview ? (
|
||||
<>
|
||||
<OnOpen run={() => setSnap(initial)} />
|
||||
<div
|
||||
ref={probeRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none invisible absolute top-0 left-0 w-px"
|
||||
style={{ height: preview }}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{draggable ? (
|
||||
<SheetGrip
|
||||
snap={snap}
|
||||
labels={preview ? previewLabels : undefined}
|
||||
onToggle={() => setSnap(snap === "full" ? "preview" : "full")}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
{showCloseButton ? (
|
||||
<SheetPrimitive.Close
|
||||
data-slot="sheet-close-button"
|
||||
className={cn(
|
||||
// On the title row: the grip's 24 px plus the header's 16 px
|
||||
// top padding put the title's line centre 52 px down.
|
||||
draggable ? "top-7.5" : "top-3",
|
||||
"ring-offset-background focus-visible:border-ring focus-visible:ring-ring/50 text-muted-foreground hover:bg-accent hover:text-foreground absolute right-2 inline-flex size-11 touch-none items-center justify-center rounded-md transition-colors focus-visible:ring-[3px] focus-visible:outline-none disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
)}
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">{t("common.close")}</span>
|
||||
</SheetPrimitive.Close>
|
||||
) : null}
|
||||
{draggable ? (
|
||||
// What a swipe presses: the same close as the X, so the sheet's
|
||||
// `onOpenChange` hears one kind of close however it was asked.
|
||||
<SheetPrimitive.Close
|
||||
ref={closeRef}
|
||||
hidden
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
data-slot="sheet-swipe-close"
|
||||
/>
|
||||
) : null}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
@@ -195,7 +195,9 @@ export function ResponsiveSheet({
|
||||
<SheetHeader
|
||||
data-slot="responsive-sheet-header"
|
||||
className={cn(
|
||||
"border-border/70 gap-1.5 border-b p-4",
|
||||
// Under the sheet's grip; the header drags the sheet, so a
|
||||
// touch on it never pans or pulls the page.
|
||||
"border-border/70 touch-none gap-1.5 border-b p-4",
|
||||
// Reserve the right gutter ONLY when the primitive paints its
|
||||
// absolute close button. With `showCloseButton={false}` the
|
||||
// consumer's own trailing control (headerAction) sits flush to
|
||||
|
||||
+41
-14
@@ -6,6 +6,10 @@ import { Dialog as SheetPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import {
|
||||
BottomSheetContent,
|
||||
type BottomSheetOptions,
|
||||
} from "@/components/ui/bottom-sheet";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
@@ -45,14 +49,43 @@ function SheetOverlay({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A sheet from one edge. `side="bottom"` is the bottom sheet
|
||||
* (`bottom-sheet.tsx`): a grip, the drag, and the options in
|
||||
* `BottomSheetOptions`, which the other sides ignore.
|
||||
*/
|
||||
function SheetContent({
|
||||
side = "right",
|
||||
preview,
|
||||
previewLabels,
|
||||
draggable,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> &
|
||||
BottomSheetOptions & {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
if (side === "bottom") {
|
||||
return (
|
||||
<BottomSheetContent
|
||||
preview={preview}
|
||||
previewLabels={previewLabels}
|
||||
draggable={draggable}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <EdgeSheetContent side={side} {...props} />;
|
||||
}
|
||||
|
||||
function EdgeSheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
side,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
side: "top" | "right" | "left";
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
@@ -61,14 +94,10 @@ function SheetContent({
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
// Read by the safe-area and on-screen-keyboard rules in
|
||||
// `globals.css`: a bottom sheet stands on the keyboard, the
|
||||
// full-height sides keep clear of the status bar and the notch.
|
||||
// Its own attribute rather than `data-slot`, which consumers
|
||||
// replace with theirs (`responsive-sheet-content`, `day-panel`).
|
||||
// A bottom sheet's `bottom` comes from that rule (0 until the
|
||||
// keyboard opens), so its branch below carries no `bottom-*`: a
|
||||
// utility would outrank the layered rule and pin it to the edge.
|
||||
// Read by the safe-area rules in `globals.css`: the full-height
|
||||
// sides keep clear of the status bar and the notch. Its own
|
||||
// attribute rather than `data-slot`, which consumers replace with
|
||||
// theirs.
|
||||
data-sheet-side={side}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
@@ -78,8 +107,6 @@ function SheetContent({
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 h-auto border-t",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -90,8 +117,8 @@ function SheetContent({
|
||||
// a 36 px floor, matching the Dialog close-X treatment from
|
||||
// MB2 (Decision I). 36 px (not 44 px) is the intentional
|
||||
// exception for fixed icon-only close affordances per the
|
||||
// dialog primitive's rationale: 44 px would crowd the sheet
|
||||
// header out of proportion on the bottom-sheet branch.
|
||||
// dialog primitive's rationale. The bottom sheet's X is 44 px
|
||||
// on its title row (`bottom-sheet.tsx`).
|
||||
<SheetPrimitive.Close
|
||||
data-slot="sheet-close-button"
|
||||
className="ring-offset-background focus-visible:border-ring focus-visible:ring-ring/50 text-muted-foreground hover:bg-accent hover:text-foreground absolute top-3 right-4 inline-flex min-h-11 min-w-11 items-center justify-center rounded-md transition-colors focus-visible:ring-[3px] focus-visible:outline-none disabled:pointer-events-none sm:min-h-9 sm:min-w-9 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
|
||||
Reference in new issue
Block a user