Skip to content

Commit 282f252

Browse files
fix(plugin-view): the record drawer is a right-hand panel on a desktop and a bottom sheet on a phone (objectui#11775) (#11821)
Fixes #11775 Clause-②: no ## What was wrong `renderDrawerForm` in `plugin-view`'s `ObjectView` opened one vaul `Drawer` with `direction="right"` on every viewport. vaul applies `direction` to its slide and drag gesture only. The shipped `DrawerContent` is the upstream bottom sheet and draws as one whatever the direction (`fixed inset-x-0 bottom-0 ... rounded-t-[10px]`, a drag handle), so on a desktop the create, edit and view form drew as a sheet pinned to the bottom-left by `sm:max-w-2xl`. ## Route: the right-hand `Sheet` on a desktop (triage direction 2). No primitive is edited The claim lets the dev choose the route. This PR uses **neither** a wrapper under `packages/components/src/custom/` **nor** a `scripts/shadcn-local-patches.mjs` entry. Nothing under `packages/components` changes, and `packages/components/src/ui/drawer.tsx` is untouched. - **Desktop, 768px and up:** `Sheet` with `side="right"`. The breakpoint is `useIsMobile`, the one the record surface is already derived on. The Sheet keeps the old width classes (`w-full sm:max-w-2xl`, plus the `navigation.width` class as before) and uses `p-0` with a flex column. The header has `p-4 pr-12` so the title clears the Sheet's own close button. - **Phone:** vaul `Drawer` in vaul's default `bottom` direction. Its `DrawerContent` takes no width classes. ### Why not a direction-aware `DrawerContent` (the four axes) - **Real business need (measured).** `ObjectView` is the only `Drawer direction=` call site in `packages/` and `apps/`, read with `git grep` at `9990f9e`. Every side panel in the tree is already a `Sheet`: - `NavigationOverlay`'s `drawer` mode, which is the record view drawer; - `plugin-form`'s `DrawerForm`; - the `side` placement of `useActionModal`; - several console pages. `useActionModal` already splits the same way: `side` opens a `Sheet` and `bottom` opens a vaul `Drawer`. - **Long-term soundness.** Each shape gets one primitive, and this adds no local-patch family to keep in step with upstream. - The registry this repo tracks for the drawer, `styles/default/drawer.json`, serves a bottom-sheet-only primitive. Apart from the import rewrite it is byte-identical to the shipped file. I read it from shadcn-ui/ui `apps/v4/public/r/styles/default/drawer.json` through raw.githubusercontent.com; `ui.shadcn.com` is not reachable from here. - Only the `new-york-v4` style keys `DrawerContent` on `data-vaul-drawer-direction`. Re-syncing this one primitive to that style would change every bottom-sheet user: - function components with `data-slot`; - an overlay of `bg-black/50` with animations; - `max-h-[80vh]` on the bottom sheet; - different header and title classes; - no `shouldScaleBackground` default. So it is not the cleaner route. - **Making it harder for AI to write mistakes.** The call that declared a direction the primitive does not draw is gone. This PR adds no metadata and no fallback. The primitive still ignores `direction`, and the SDUI `drawer` node's declared `direction` still reaches it. That is measured and recorded under Acceptance notes. It is left to its own card: choosing between patching the primitive and pointing that node at `Sheet` shapes a public metadata contract. - **Startup-stage focus.** This touches one package. It adds no export, prop, type member or language-pack key, and no patch family, fixture or gate. **The phone half.** Before this PR the phone got a bottom sheet with direction `right`: it slid in from the right and was swiped right to close. It now slides up and is dragged down to close. It is also no longer capped by `sm:max-w-2xl`, which between 640px and 767px left it pinned to the left. The 700px row below measures this. ## Pins (jsdom-class environment: classes and data attributes, no layout) New file `packages/plugin-view/src/__tests__/ObjectView.drawerDirection-11775.test.tsx`: - **THE FIX** for create, edit and view at 1440px. The form's `[role="dialog"]`: - is not a vaul drawer; - carries `inset-y-0 right-0 h-full`; - carries none of `inset-x-0 bottom-0 rounded-t-[10px]`; - carries `w-full sm:max-w-2xl`, not the Sheet's default `sm:max-w-sm`. - **CONTROL** for create, edit and view at 390px. The form is on a vaul drawer with `data-vaul-drawer-direction="bottom"` and the bottom-sheet classes, and has no `right-0` and no `sm:max-w-2xl`. - **CONTROL** for an unrelated user: the stock `DrawerContent`, which `useActionModal`'s `bottom` placement and the SDUI `drawer` node render, still draws the bottom sheet. Two existing pins found "the drawer" by `[data-vaul-drawer]`: `ObjectView.pageSurfaceFallback-11015.test.tsx` and `ObjectView.splitPopoverCreate-10975.test.tsx`. Their helper now accepts either drawer surface, a right-anchored dialog or a vaul drawer. Their assertions are unchanged. They stay green under the ablation below, which is correct: they pin which surface opens, not its direction. ## Ablation (one-time, nothing committed) The fix was committed first (`fa16463`). `node scripts/ablation-replace.mjs` from the objectstack checkout then swapped the new `renderDrawerForm` back to the base markup (anchor 1 → 0, blob `46fbd4ed441f` → `b4542c644a0b`). It ran the three test files and restored the file: the blob equals HEAD and `git diff HEAD` is empty. - **Base markup:** `Tests 6 failed | 21 passed (27)`. All three desktop THE FIX rows failed with "the desktop form is still on vaul's drawer, whose content is styled as a bottom sheet". All three phone controls failed with "expected 'right' to be 'bottom'". The stock control and both existing pins stayed green. - **This PR:** `Tests 27 passed (27)`. ## Real-browser reading (Chromium at `/opt/pw-browsers/chromium`) The harness was temporary and is removed: a console Vite entry that mounts `ObjectView` with `layout: 'drawer'` on an in-memory data source. It ran on a dev server I started and then stopped. The script clicks New and reads the `[role="dialog"]` box after the open animation. The base markup was read in the same harness through the same ablation tool, and the file was restored afterwards. | viewport | base markup | this PR | |---|---|---| | 1440×900 | x=0 y=600, 672×300, vaul `right` | x=768 y=0, 672×900, `Sheet` | | 390×844 | x=0 y=496, 390×348, vaul `right` | x=0 y=496, 390×348, vaul `bottom` | | 700×900 | x=0 y=600, 672×300, vaul `right` | x=0 y=600, 700×300, vaul `bottom` | The base desktop reading has the card's shape: bottom-left and 672px wide. The card's 526px height came from a larger form. ## Local gates (all on `fa16463`, the PR head) | command | exit | the gate's own verdict | |---|---|---| | `pnpm exec vitest run --maxWorkers=2 packages/plugin-view/` | 0 | `Test Files 67 passed (67)` · `Tests 623 passed (623)` | | `pnpm --filter @object-ui/plugin-view type-check` | 0 | `tsc --noEmit && tsc -p tsconfig.test.json`, no output. `--listFiles` on the test program includes all 3 touched test files. Run after `turbo run build --filter='@object-ui/plugin-view^...'`: 15 tasks, 13 from cache, exit 0 | | `pnpm exec eslint` on the 4 touched `.ts`/`.tsx` files, `--format json` | 0 | 4 files, 0 errors. Warnings are the same as at the base: 61 on `ObjectView.tsx`, 2 on the 10975 test, 0 on the 11015 test, 0 on the new test | | `pnpm check:control-bytes` | 0 | `OK (scanned 7780 tracked text file(s); skipped 85 binary)` | | `pnpm check:test-path-roots` | 0 | `check-test-path-roots: OK` | | `pnpm check:changeset-claims` | 0 | `No pending changeset names a file this change touches.` | | `pnpm check:pending-changeset-literals` | 0 | `No test source names a pending changeset.` | | `pnpm check:new-line-citations` | 0 | `0 new citation(s), enforcement report-only` | | `node scripts/check-changeset-presence.mjs` | 0 | `4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)` | | `node scripts/check-changeset-no-major.mjs` | 0 | `No changeset declares a major bump.` | Lint was run on the touched files only. That narrowing is a measurement, not a skipped run: - **Population:** `eslint.config.js` lints `**/*.{ts,tsx}`, and this diff touches 4 such files, counted from the `--format json` output. - **Invariance:** the config enables no type-aware linting (zero hits for `projectService`, `parserOptions` or `project:`). So this diff cannot move the verdict on any file it does not touch. The diff adds and removes no `export` line. The same grep finds 8 export lines in `ObjectView.tsx` as a positive control. So no package outside `plugin-view` owes a re-test. Repo-wide lint and the test farm are CI's. ## Acceptance notes - **The SDUI `drawer` node meets the same primitive.** Not filed: the bar for filing is not met (see the reach line). I measured this in-process: a real `SchemaRenderer` and the real registry in the `dom` project, through a probe that was not committed. - An authored `direction: 'right'` stamps `data-vaul-drawer-direction="right"` but draws the bottom-sheet classes. - With no `direction`, it stamps `bottom`, while the JSDoc on `DrawerSchema.direction` says `@default 'right'`. - The docs page `content/docs/components/overlay/drawer.mdx` shows a `side` member that `DrawerSchema` does not declare. An authored `side: 'right'` stamps `bottom`. - Reach for filing is not met. This is an in-process reading only, and no producer in the tree authors `direction` or `side` on a `drawer` node; the catalog's `basic-drawer.json` authors neither. Carrier: none. - **`navigation.width` on this drawer probably does nothing (not measured).** `formWidthClass` builds a `max-w-[N...]` class string at runtime. Tailwind generates classes it finds in source, so an authored `navigation.width` probably draws nothing unless that exact class exists elsewhere. The desktop behaviour is the same before and after this PR. `NavigationOverlay` carries its width through a CSS custom property instead. Carrier: none. - **Tall forms on the phone (not measured).** The phone bottom sheet keeps upstream's `h-auto` with no max height, and this PR does not change that. A form taller than the viewport was not measured here. - **No docs edit.** The README and `content/docs/plugins/plugin-view.mdx` call this surface "the drawer" and name no edge. Nothing they say is false after this PR, and both files are outside the claimed surface. Implemented by the os-dev agent for the seat `domain:ui#3` PM loop, session `https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8`. --- _Generated by [Claude Code](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e06365c commit 282f252

5 files changed

Lines changed: 271 additions & 18 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-view': patch
3+
---
4+
5+
The record drawer of an object view opens as a right-hand panel on a desktop, and stays a bottom sheet on a phone (objectui#11775).
6+
7+
An object view whose record surface is the drawer (an authored `layout: 'drawer'` or `navigation: { mode: 'drawer' }`, a light object on a desktop, or a page with nowhere to route) opened its create, edit and view form in vaul's `Drawer` with `direction: 'right'`. vaul applies the direction to the slide and the drag gesture only, and the shipped `DrawerContent` styles itself as a bottom sheet whatever the direction, so on a desktop the form drew as a sheet pinned to the bottom-left of the window, at most 672px wide, with a drag handle.
8+
9+
- **Desktop:** the form opens in the right-hand `Sheet`, full height and anchored to the right edge, the panel the console's record drawer (`NavigationOverlay` in drawer mode) already opens a record in. It keeps the width it had: the full window up to 672px.
10+
- **Phone (below 768px):** the form stays on vaul's bottom sheet, which now slides up and is dragged down to close, the direction it is drawn in, and spans the screen at every phone width.
11+
12+
`DrawerContent` itself is unchanged, so every other bottom sheet draws as before.

‎packages/plugin-view/src/ObjectView.tsx‎

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ import {
4949
DrawerHeader,
5050
DrawerTitle,
5151
DrawerDescription,
52+
Sheet,
53+
SheetContent,
54+
SheetHeader,
55+
SheetTitle,
56+
SheetDescription,
5257
NavigationOverlay,
5358
Button,
5459
Tabs,
@@ -2669,22 +2674,60 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
26692674
return `max-w-[${w}]`;
26702675
}, [navigationConfig]);
26712676

2672-
// Render the form in a drawer
2673-
const renderDrawerForm = () => (
2674-
<Drawer open={isFormOpen} onOpenChange={setIsFormOpen} direction="right">
2675-
<DrawerContent className={cn('w-full sm:max-w-2xl', formWidthClass)}>
2676-
<DrawerHeader>
2677-
<DrawerTitle>{getFormTitle()}</DrawerTitle>
2678-
{formDescriptionText && (
2679-
<DrawerDescription>{formDescriptionText}</DrawerDescription>
2680-
)}
2681-
</DrawerHeader>
2682-
<div className="flex-1 overflow-y-auto px-4 pb-4">
2683-
<ObjectForm schema={buildFormSchema()} dataSource={dataSource} />
2684-
</div>
2685-
</DrawerContent>
2686-
</Drawer>
2687-
);
2677+
// Render the form in a drawer: a right-hand panel on a desktop, a bottom
2678+
// sheet on a phone (`isMobile`, the breakpoint the record surface is
2679+
// derived on).
2680+
//
2681+
// objectui#11775: this used to be one vaul `Drawer` with
2682+
// `direction="right"` on every viewport. vaul honours `direction` for the
2683+
// slide and the drag gesture only; the shipped `DrawerContent` is the
2684+
// upstream bottom sheet and styles itself as one whatever the direction, so
2685+
// on a desktop the form drew as a bottom sheet pinned to the bottom-left by
2686+
// `sm:max-w-2xl`. The desktop panel is now the right-hand `Sheet`, the
2687+
// primitive `NavigationOverlay`'s drawer mode and `DrawerForm` already open
2688+
// records in. The phone keeps vaul's bottom sheet, opened in vaul's own
2689+
// default direction so its slide and drag match how it is drawn, and spans
2690+
// the viewport like every other bottom sheet: the width classes, and an
2691+
// authored `navigation.width`, size the side panel only.
2692+
const renderDrawerForm = () => {
2693+
const form = (
2694+
<div className="flex-1 overflow-y-auto px-4 pb-4">
2695+
<ObjectForm schema={buildFormSchema()} dataSource={dataSource} />
2696+
</div>
2697+
);
2698+
if (isMobile) {
2699+
return (
2700+
<Drawer open={isFormOpen} onOpenChange={setIsFormOpen}>
2701+
<DrawerContent>
2702+
<DrawerHeader>
2703+
<DrawerTitle>{getFormTitle()}</DrawerTitle>
2704+
{formDescriptionText && (
2705+
<DrawerDescription>{formDescriptionText}</DrawerDescription>
2706+
)}
2707+
</DrawerHeader>
2708+
{form}
2709+
</DrawerContent>
2710+
</Drawer>
2711+
);
2712+
}
2713+
return (
2714+
<Sheet open={isFormOpen} onOpenChange={setIsFormOpen}>
2715+
<SheetContent
2716+
side="right"
2717+
className={cn('flex w-full flex-col gap-0 p-0 sm:max-w-2xl', formWidthClass)}
2718+
>
2719+
{/* `pr-12` keeps the title clear of the Sheet's own close button. */}
2720+
<SheetHeader className="p-4 pr-12">
2721+
<SheetTitle>{getFormTitle()}</SheetTitle>
2722+
{formDescriptionText && (
2723+
<SheetDescription>{formDescriptionText}</SheetDescription>
2724+
)}
2725+
</SheetHeader>
2726+
{form}
2727+
</SheetContent>
2728+
</Sheet>
2729+
);
2730+
};
26882731

26892732
// Render the form in a modal
26902733
const renderModalForm = () => (
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#11775 — the record drawer is a right-hand panel on a desktop and a
11+
* bottom sheet on a phone.
12+
*
13+
* `renderDrawerForm` used to open one vaul `Drawer` with `direction="right"`
14+
* on every viewport. vaul honours `direction` for its slide and drag gesture
15+
* only: the shipped `DrawerContent` is the upstream bottom sheet and carries
16+
* the bottom-sheet classes whatever the direction. On a 1440×900 desktop the
17+
* create form therefore drew as a 672×526 bottom sheet pinned to the
18+
* bottom-left by `sm:max-w-2xl`, with `data-vaul-drawer-direction="right"` on
19+
* it (the card's browser reading).
20+
*
21+
* The desktop panel is now the right-hand `Sheet`; the phone keeps vaul's
22+
* bottom sheet, opened in vaul's default direction.
23+
*
24+
* jsdom-class environments do not lay out, so these pins read the classes and
25+
* data attributes that decide the geometry, never a measured box:
26+
*
27+
* - THE FIX: on a desktop, create, edit and view draw the form on a
28+
* `[role="dialog"]` that is right-anchored and full height
29+
* (`inset-y-0 right-0 h-full`), is not a vaul drawer, and carries none of
30+
* the bottom sheet's classes.
31+
* - CONTROL: on a phone the same verbs draw it on vaul's bottom sheet, in the
32+
* `bottom` direction, spanning the viewport (no `sm:max-w-2xl` cap).
33+
* - CONTROL: the stock `DrawerContent` every other bottom-sheet user renders
34+
* (`useActionModal`'s `bottom` placement, the SDUI `drawer` node) still
35+
* draws the bottom sheet: the fix did not come from changing the primitive.
36+
*
37+
* `ObjectGrid` is a sink that keeps the handlers the view hands it, and
38+
* `ObjectForm` a sink that shows the mode it was built for.
39+
*/
40+
41+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
42+
import { render, screen, waitFor, cleanup, act, fireEvent, within } from '@testing-library/react';
43+
import type { DataSource, ObjectViewSchema } from '@object-ui/types';
44+
import { Drawer, DrawerContent, DrawerTitle } from '@object-ui/components';
45+
import { ObjectView } from '../ObjectView';
46+
47+
type GridHandlers = { onRowClick: (record: Record<string, unknown>) => void; onEdit?: (record: Record<string, unknown>) => void };
48+
const gridProps: GridHandlers[] = [];
49+
50+
vi.mock('@object-ui/plugin-grid', async (importOriginal) => ({
51+
...(await importOriginal<Record<string, unknown>>()),
52+
ObjectGrid: ({ onRowClick, onEdit }: GridHandlers) => {
53+
gridProps.push({ onRowClick, onEdit });
54+
return <div data-testid="object-grid" />;
55+
},
56+
}));
57+
vi.mock('@object-ui/plugin-form', async (importOriginal) => ({
58+
...(await importOriginal<Record<string, unknown>>()),
59+
ObjectForm: ({ schema }: { schema: { mode?: string } }) => <div data-testid="object-form" data-mode={schema.mode} />,
60+
}));
61+
62+
/** The card's viewport, and a phone below `useIsMobile`'s 768px breakpoint. */
63+
const DESKTOP = 1440;
64+
const MOBILE = 390;
65+
const ORIGINAL_WIDTH = window.innerWidth;
66+
67+
function setViewport(width: number) {
68+
Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: width });
69+
}
70+
71+
/** The upstream bottom sheet's geometry classes, and the right panel's. */
72+
const BOTTOM_SHEET = ['inset-x-0', 'bottom-0', 'rounded-t-[10px]'];
73+
const RIGHT_PANEL = ['inset-y-0', 'right-0', 'h-full'];
74+
75+
const ROW = { id: 'r1', name: 'Alpha' };
76+
77+
function makeDataSource() {
78+
const state = { settled: false };
79+
const ds = {
80+
find: vi.fn().mockResolvedValue({ data: [], total: 0 }),
81+
findOne: vi.fn(),
82+
create: vi.fn(),
83+
update: vi.fn(),
84+
delete: vi.fn(),
85+
getObjectSchema: vi.fn(async () => {
86+
state.settled = true;
87+
return { name: 'task', label: 'Tasks', fields: { name: { type: 'text' }, status: { type: 'text' } } };
88+
}),
89+
};
90+
return { ds: ds as unknown as DataSource, state };
91+
}
92+
93+
/** An object view whose author chose the drawer, so both viewports open it. */
94+
async function mount() {
95+
const { ds, state } = makeDataSource();
96+
const schema = { type: 'object-view', objectName: 'task', layout: 'drawer' } as unknown as ObjectViewSchema;
97+
render(<ObjectView schema={schema} dataSource={ds} />);
98+
await waitFor(() => expect(gridProps.length).toBeGreaterThan(0));
99+
await waitFor(() => expect(state.settled, 'the object schema was never read').toBe(true));
100+
await act(async () => {});
101+
}
102+
103+
type Verb = 'create' | 'edit' | 'view';
104+
const VERBS: Verb[] = ['create', 'edit', 'view'];
105+
const TITLES: Record<Verb, string> = { create: 'Create Tasks', edit: 'Edit Tasks', view: 'View Tasks' };
106+
107+
function run(verb: Verb) {
108+
const grid = gridProps[gridProps.length - 1];
109+
if (verb === 'create') {
110+
fireEvent.click(screen.getByRole('button', { name: 'New' }));
111+
} else if (verb === 'view') {
112+
act(() => grid.onRowClick(ROW));
113+
} else {
114+
expect(grid.onEdit, 'the grid was handed no `onEdit`').toBeTypeOf('function');
115+
act(() => grid.onEdit!(ROW));
116+
}
117+
}
118+
119+
/** The dialog surface the one form in the tree, built for `verb`, is drawn on. */
120+
async function surfaceOf(verb: Verb): Promise<HTMLElement> {
121+
const form = await screen.findByTestId('object-form');
122+
expect(form.getAttribute('data-mode')).toBe(verb);
123+
const surface = form.closest('[role="dialog"]') as HTMLElement | null;
124+
expect(surface, `the ${verb} form is not inside a dialog surface`).not.toBeNull();
125+
expect(within(surface!).getByText(TITLES[verb])).toBeTruthy();
126+
return surface!;
127+
}
128+
129+
const classesOf = (el: Element) => el.getAttribute('class')?.split(/\s+/).filter(Boolean) ?? [];
130+
131+
beforeEach(() => {
132+
cleanup();
133+
gridProps.length = 0;
134+
});
135+
afterEach(() => {
136+
cleanup();
137+
setViewport(ORIGINAL_WIDTH);
138+
});
139+
140+
describe('objectui#11775 — the record drawer is a right panel on a desktop, a bottom sheet on a phone', () => {
141+
for (const verb of VERBS) {
142+
it(`THE FIX: on a desktop, ${verb} draws the form in a right-anchored, full-height panel`, async () => {
143+
setViewport(DESKTOP);
144+
await mount();
145+
run(verb);
146+
const surface = await surfaceOf(verb);
147+
const classes = classesOf(surface);
148+
149+
expect(
150+
surface.closest('[data-vaul-drawer]'),
151+
'the desktop form is still on vaul\'s drawer, whose content is styled as a bottom sheet',
152+
).toBeNull();
153+
expect(classes, 'the panel is not right-anchored and full height').toEqual(expect.arrayContaining(RIGHT_PANEL));
154+
for (const bottom of BOTTOM_SHEET) {
155+
expect(classes, `the desktop panel still carries the bottom sheet's \`${bottom}\``).not.toContain(bottom);
156+
}
157+
// The caller's width wins over the Sheet's default `w-3/4 sm:max-w-sm`.
158+
expect(classes).toEqual(expect.arrayContaining(['w-full', 'sm:max-w-2xl']));
159+
expect(classes).not.toContain('sm:max-w-sm');
160+
});
161+
162+
it(`CONTROL: on a phone, ${verb} draws the form on vaul's bottom sheet, across the viewport`, async () => {
163+
setViewport(MOBILE);
164+
await mount();
165+
run(verb);
166+
const surface = await surfaceOf(verb);
167+
const classes = classesOf(surface);
168+
169+
expect(surface.hasAttribute('data-vaul-drawer'), 'the phone form is not on vaul\'s drawer').toBe(true);
170+
expect(
171+
surface.getAttribute('data-vaul-drawer-direction'),
172+
'the bottom sheet slides and drags in a direction other than the one it is drawn in',
173+
).toBe('bottom');
174+
expect(classes).toEqual(expect.arrayContaining(BOTTOM_SHEET));
175+
expect(classes).not.toContain('right-0');
176+
expect(classes, 'the phone bottom sheet is capped, so it pins to the left between 640px and 768px').not.toContain('sm:max-w-2xl');
177+
});
178+
}
179+
180+
it('CONTROL: the stock `DrawerContent` other bottom-sheet users render still draws the bottom sheet', () => {
181+
render(
182+
<Drawer open>
183+
<DrawerContent>
184+
<DrawerTitle>Stock bottom sheet</DrawerTitle>
185+
</DrawerContent>
186+
</Drawer>,
187+
);
188+
const surface = screen.getByText('Stock bottom sheet').closest('[data-vaul-drawer]') as HTMLElement | null;
189+
expect(surface).not.toBeNull();
190+
expect(surface!.getAttribute('data-vaul-drawer-direction')).toBe('bottom');
191+
expect(classesOf(surface!)).toEqual(expect.arrayContaining(BOTTOM_SHEET));
192+
});
193+
});

‎packages/plugin-view/src/__tests__/ObjectView.pageSurfaceFallback-11015.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ function run(verb: Verb) {
146146
async function formInDrawer(verb: Verb): Promise<HTMLElement> {
147147
const form = await screen.findByTestId('object-form');
148148
expect(form.getAttribute('data-mode')).toBe(verb);
149-
const drawer = form.closest('[data-vaul-drawer]') as HTMLElement | null;
149+
// The drawer is the right-hand Sheet on a desktop and vaul's bottom sheet on
150+
// a phone (objectui#11775).
151+
const drawer = (form.closest('[role="dialog"].inset-y-0.right-0') ?? form.closest('[data-vaul-drawer]')) as HTMLElement | null;
150152
expect(drawer, `the ${verb} form is not inside the drawer`).not.toBeNull();
151153
return drawer!;
152154
}

‎packages/plugin-view/src/__tests__/ObjectView.splitPopoverCreate-10975.test.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ async function formIn(mode: string): Promise<HTMLElement> {
9797
return form;
9898
}
9999

100-
const inDrawer = (el: HTMLElement) => el.closest('[data-vaul-drawer]') !== null;
100+
// The drawer is the right-hand Sheet on a desktop and vaul's bottom sheet on a
101+
// phone (objectui#11775); the modal is a centred dialog, neither of the two.
102+
const inDrawer = (el: HTMLElement) =>
103+
el.closest('[role="dialog"].inset-y-0.right-0') !== null || el.closest('[data-vaul-drawer]') !== null;
101104
const inDialog = (el: HTMLElement) => el.closest('[role="dialog"]') !== null;
102105

103106
describe('objectui#10975 — New opens a create form under `split` and `popover`', () => {

0 commit comments

Comments
 (0)