Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/12603-dialog-gutter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@object-ui/components': minor
'@object-ui/app-shell': patch
'@object-ui/fields': patch
'@object-ui/plugin-grid': patch
'@object-ui/plugin-form': patch
---

Every wide centred dialog keeps a 1rem gutter on each side of the window from `sm` (640px) up, by one shared class (objectui#12603).

`@object-ui/components` adds one export, `DIALOG_VIEWPORT_GUTTER` (`sm:w-[calc(100%-2rem)]`): the gutter `ModalForm` introduced for its `lg` and `xl` sizes (objectui#12593), promoted so every wide dialog reads the same value. A dialog that raises its cap with `sm:max-w-2xl` or wider writes this constant beside the cap.

Dialogs that ran edge to edge between `sm` and their max width and now keep the gutter:

- the action modal's `lg` and `xl` sizes (`xl` is what a `type: 'modal'` action with a page target and no size opens at);
- the action parameter dialog with a long parameter list, the flow runner's object-form or long screen step, and the CEL test-run dialog;
- the full-screen text editor dialog, the image lightbox and the import wizard.

The record picker reads the same class under every width rung; it kept a `95vw` margin of its own before, so between `sm` and its cap it is now the window less 2rem instead. `ModalForm` reads the shared export instead of its private copy, with no change in output.

Unchanged: sheets, the phone sheet below `sm`, the `full` size (`95vw`), and every dialog's width on a window wider than its max width plus 2rem.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* The action modal's bounded wide sizes keep the shared viewport gutter
* (objectui#12603).
*
* A `type: 'modal'` action with a page target and no size opens at `xl`
* (`resolveModalTarget`), the centred dialog's `sm:max-w-5xl`. Before
* objectui#12603 that dialog ran edge to edge in every window between `sm`
* and 1024px. `lg` and `xl` now also read `DIALOG_VIEWPORT_GUTTER`, so the
* dialog keeps 1rem either side from `sm` up and is as wide as before above
* its max width plus 2rem.
*
* CONTROLs: `full` keeps its own `95vw` and reads no gutter; `default` never
* reaches the edge at or above `sm` and reads none either; a `side` placement
* is a sheet, edge-anchored by design, and is unchanged.
*/

import * as React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { DIALOG_VIEWPORT_GUTTER } from '@object-ui/components';
import { useActionModal } from '../useActionModal';

afterEach(cleanup);

type Handler = (schema: unknown) => Promise<unknown>;

function Harness({ onReady }: { onReady: (h: Handler) => void }) {
const { modalHandler, modalElement } = useActionModal();
React.useEffect(() => onReady(modalHandler), [onReady, modalHandler]);
return <>{modalElement}</>;
}

async function open(descriptor: Record<string, unknown>): Promise<string[]> {
let handler: Handler | null = null;
render(<Harness onReady={(h) => { handler = h; }} />);
await act(async () => {
void handler!({ title: 'Quick view', content: { type: 'box' }, ...descriptor });
});
const surface = screen.getByRole('dialog');
return (surface.getAttribute('class') ?? '').split(/\s+/).filter(Boolean);
}

describe('useActionModal: the wide sizes keep the shared viewport gutter (objectui#12603)', () => {
it('xl, the size a page target with no size opens at, reads the gutter beside its cap', async () => {
const classes = await open({ size: 'xl' });
expect(classes).toContain('sm:max-w-5xl');
expect(classes).toContain(DIALOG_VIEWPORT_GUTTER);
});

it('lg reads the same gutter', async () => {
const classes = await open({ size: 'lg' });
expect(classes).toContain('sm:max-w-2xl');
expect(classes).toContain(DIALOG_VIEWPORT_GUTTER);
});

it('CONTROL: full keeps its own 95vw and default reads no gutter', async () => {
const full = await open({ size: 'full' });
expect(full).toContain('sm:max-w-[95vw]');
expect(full).toContain('sm:w-full');
expect(full).not.toContain(DIALOG_VIEWPORT_GUTTER);
cleanup();
const plain = await open({});
expect(plain).not.toContain(DIALOG_VIEWPORT_GUTTER);
});

it('CONTROL: a side placement is a sheet and reads no gutter', async () => {
const classes = await open({ size: 'xl', placement: 'side' });
expect(classes).toContain('sm:max-w-3xl');
expect(classes).not.toContain(DIALOG_VIEWPORT_GUTTER);
});
});
11 changes: 9 additions & 2 deletions packages/app-shell/src/hooks/useActionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
DIALOG_VIEWPORT_GUTTER,
Drawer,
DrawerContent,
DrawerDescription,
Expand Down Expand Up @@ -97,11 +98,17 @@ export interface ModalDescriptor {

type ActionResult = { success: boolean; reload?: boolean; data?: any; [k: string]: any };

/**
* The centred dialog's width per size. The bounded wide sizes, `lg` and `xl`,
* read the shared {@link DIALOG_VIEWPORT_GUTTER} so they keep 1rem from the
* window edge at every width from `sm` up (objectui#12603); `full` keeps its
* own `95vw`, and `sm` / `default` never reach the edge at or above `sm`.
*/
const SIZE_CLASS: Record<ModalSize, string> = {
sm: 'sm:max-w-sm',
default: 'sm:max-w-lg',
lg: 'sm:max-w-2xl',
xl: 'sm:max-w-5xl',
lg: `sm:max-w-2xl ${DIALOG_VIEWPORT_GUTTER}`,
xl: `sm:max-w-5xl ${DIALOG_VIEWPORT_GUTTER}`,
full: 'sm:max-w-[95vw] sm:w-full',
};
const SIDE_SIZE_CLASS: Partial<Record<ModalSize, string>> = {
Expand Down
3 changes: 2 additions & 1 deletion packages/app-shell/src/views/ActionParamDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DIALOG_VIEWPORT_GUTTER,
Button,
Label,
Collapsible,
Expand Down Expand Up @@ -508,7 +509,7 @@ export function ActionParamDialog({ state, onOpenChange }: ActionParamDialogProp
{/* Bounded (objectui#12080): a flex column at most 90vh tall, in which
only the body below can give up height (`min-h-0`) and scroll, so the
header and the footer always stay on screen. */}
<DialogContent className={cn('flex max-h-[90vh] flex-col', isLongList && 'sm:max-w-3xl')}>
<DialogContent className={cn('flex max-h-[90vh] flex-col', isLongList && `sm:max-w-3xl ${DIALOG_VIEWPORT_GUTTER}`)}>
<DialogHeader>
<DialogTitle>{state.title || t('actionDialog.title')}</DialogTitle>
{/* `whitespace-pre-line` so a description composed of more than one
Expand Down
3 changes: 2 additions & 1 deletion packages/app-shell/src/views/FlowRunner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ import {
DialogFooter,
DialogTitle,
DialogDescription,
DIALOG_VIEWPORT_GUTTER,
Button,
cn,
} from '@object-ui/components';
Expand Down Expand Up @@ -600,7 +601,7 @@ export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dat
tall, in which only the body below can give up height (`min-h-0`)
and scroll, so the header and the footer always stay on screen. */}
<DialogContent
className={cn('flex max-h-[90vh] flex-col', isObjectForm || isLongScreen ? 'sm:max-w-3xl' : 'sm:max-w-md')}
className={cn('flex max-h-[90vh] flex-col', isObjectForm || isLongScreen ? `sm:max-w-3xl ${DIALOG_VIEWPORT_GUTTER}` : 'sm:max-w-md')}
>
<DialogHeader>
{/* The flow being run (objectui#11092), above the step's own heading,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DIALOG_VIEWPORT_GUTTER,
Label,
Textarea,
cn,
Expand Down Expand Up @@ -126,7 +127,7 @@ export function CelTestRunDialog({

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
<DialogContent className={`sm:max-w-2xl ${DIALOG_VIEWPORT_GUTTER}`}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FlaskConical className="h-4 w-4 text-muted-foreground" />
Expand Down
Loading
Loading