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
5 changes: 5 additions & 0 deletions .changeset/fancy-laws-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"xmlui": patch
---

Add a new layout, "desktop", to App
4 changes: 2 additions & 2 deletions docs/content/components/App.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,6 @@ Here are a few samples demonstrating the usage of the `layout` property. All sam

#### `desktop` [#desktop]

The `desktop` layout is designed for full-screen desktop applications. It stretches the app to fill the entire browser viewport with zero padding and margins. The header (if present) docks to the top, the footer (if present) docks to the bottom, and the main content area stretches to fill all remaining vertical and horizontal space. This layout ignores all max-width constraints and scrollbar gutter settings to ensure edge-to-edge display.

```xmlui-pg copy name="Example: 'desktop' layout" height="300px"
<App layout="desktop">
<AppHeader>
Expand All @@ -327,6 +325,8 @@ The `desktop` layout is designed for full-screen desktop applications. It stretc
</App>
```

The `desktop` layout is designed for full-screen desktop applications. It stretches the app to fill the entire browser viewport with zero padding and margins. The header (if present) docks to the top, the footer (if present) docks to the bottom, and the main content area stretches to fill all remaining vertical and horizontal space. This layout ignores all max-width constraints and scrollbar gutter settings to ensure edge-to-edge display.

### `loggedInUser` [#loggedinuser]

Stores information about the currently logged-in user. By not defining this property, you can indicate that no user is logged in.
Expand Down
11 changes: 5 additions & 6 deletions docs/content/components/_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
| [Bookmark](./Bookmark) | As its name suggests, this component places a bookmark into its parent component's view. The component has an `id` that you can use in links to navigate (scroll to) the bookmark's location. |
| [Breakout](./Breakout) | The `Breakout` component creates a breakout section. It allows its child to occupy the entire width of the UI even if the app or the parent container constrains the maximum content width. |
| [Button](./Button) | `Button` is the primary interactive component for triggering actions like form submissions, navigation, opening modals, and API calls. It supports multiple visual styles and sizes to match different UI contexts and importance levels. |
| [CHStack](./CHStack) | This component represents a stack that renders its contents horizontally and aligns that in the center along both axes. |
| [CVStack](./CVStack) | This component represents a stack that renders its contents vertically and aligns that in the center along both axes. |
| [Card](./Card) | `Card` is a versatile container that groups related content with a visual boundary, typically featuring background color, padding, borders, and rounded corners. It's ideal for organizing information, creating sections, and establishing visual hierarchy in your interface. |
| [Carousel](./Carousel) | This component displays a slideshow by cycling through elements (images, text, or custom slides) like a carousel. |
| [ChangeListener](./ChangeListener) | `ChangeListener` is an invisible component that watches for changes in values and triggers actions in response. It's essential for creating reactive behavior when you need to respond to data changes, state updates, or component property modifications outside of normal event handlers. |
| [Checkbox](./Checkbox) | `Checkbox` allows users to make binary choices with a clickable box that shows checked/unchecked states. It's essential for settings, preferences, multi-select lists, and accepting terms or conditions. |
| [CHStack](./CHStack) | This component represents a stack that renders its contents horizontally and aligns that in the center along both axes. |
| [ColorPicker](./ColorPicker) | `ColorPicker` enables users to choose colors by specifying RGB, HSL, or HEX values. |
| [Column](./Column) | `Column` defines the structure and behavior of individual table columns within a [`Table`](/components/Table) component. Each Column controls data binding, header display, sorting capabilities, sizing, and can contain any XMLUI components for rich cell content. |
| [ContentSeparator](./ContentSeparator) | `ContentSeparator` creates visual dividers between content sections using horizontal or vertical lines. It's essential for improving readability by breaking up dense content, separating list items, or creating clear boundaries between different UI sections. |
| [CVStack](./CVStack) | This component represents a stack that renders its contents vertically and aligns that in the center along both axes. |
| [DataSource](./DataSource) | `DataSource` fetches and caches data from API endpoints, versus [`APICall`](/components/APICall) which creates, updates or deletes data. |
| [DateInput](./DateInput) | `DateInput` provides a text-based date input interface for selecting single dates or date ranges, with direct keyboard input similar to TimeInput. It offers customizable formatting and validation options without dropdown calendars. |
| [DatePicker](./DatePicker) | `DatePicker` provides an interactive calendar interface for selecting single dates or date ranges, with customizable formatting and validation options. It displays a text input that opens a calendar popup when clicked, offering both keyboard and mouse interaction. |
Expand All @@ -46,11 +46,11 @@
| [H4](./H4) | Represents a heading level 4 text |
| [H5](./H5) | Represents a heading level 5 text |
| [H6](./H6) | Represents a heading level 6 text |
| [Heading](./Heading) | `Heading` displays hierarchical text headings with semantic importance levels from H1 to H6, following HTML heading standards. It provides text overflow handling, anchor link generation, and integrates with [TableOfContents](/components/TableOfContents). |
| [HSplitter](./HSplitter) | `Splitter` component divides a container into two resizable sections. These are are identified by their names: primary and secondary. They have a draggable bar between them. When only a single child is visible (due to conditional rendering with `when` attributes), the splitter bar is not displayed and the single panel stretches to fill the entire viewport of the splitter container. |
| [HStack](./HStack) | This component represents a stack rendering its contents horizontally. |
| [Icon](./Icon) | `Icon` displays scalable vector icons from XMLUI's built-in icon registry using simple name references. Icons are commonly used in buttons, navigation elements, and status indicators. |
| [Heading](./Heading) | `Heading` displays hierarchical text headings with semantic importance levels from H1 to H6, following HTML heading standards. It provides text overflow handling, anchor link generation, and integrates with [TableOfContents](/components/TableOfContents). |
| [IFrame](./IFrame) | `IFrame` embeds external content from another HTML document into the current page. It provides security controls through sandbox and allow attributes, and supports features like fullscreen display and referrer policy configuration. |
| [Icon](./Icon) | `Icon` displays scalable vector icons from XMLUI's built-in icon registry using simple name references. Icons are commonly used in buttons, navigation elements, and status indicators. |
| [Image](./Image) | `Image` displays pictures from URLs or local sources with built-in responsive sizing, aspect ratio control, and accessibility features. It handles different image formats and provides options for lazy loading and click interactions. |
| [Items](./Items) | `Items` renders data arrays without built-in layout or styling, providing a lightweight alternative to `List`. Unlike `List`, it provides no virtualization, grouping, or visual formatting — just pure data iteration. |
| [LabelList](./LabelList) | `LabelList` adds custom data labels to chart components when automatic labeling isn't sufficient. It's a specialized component for advanced chart customization scenarios where you need precise control over label positioning and appearance. |
Expand Down Expand Up @@ -109,5 +109,4 @@ Hover over the component to see the tooltip with the current value. On mobile, t
| [Tooltip](./Tooltip) | A tooltip component that displays text when hovering over trigger content. |
| [Tree](./Tree) | The `Tree` component is a virtualized tree component that displays hierarchical data with support for flat and hierarchy data formats. |
| [VSplitter](./VSplitter) | `Splitter` component divides a container into two resizable sections. These are are identified by their names: primary and secondary. They have a draggable bar between them. When only a single child is visible (due to conditional rendering with `when` attributes), the splitter bar is not displayed and the single panel stretches to fill the entire viewport of the splitter container. |
| [VStack](./VStack) | This component represents a stack rendering its contents vertically. |

| [VStack](./VStack) | This component represents a stack rendering its contents vertically. |
4 changes: 2 additions & 2 deletions xmlui/src/components/App/App.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,6 @@ Here are a few samples demonstrating the usage of the `layout` property. All sam

#### `desktop`

The `desktop` layout is designed for full-screen desktop applications. It stretches the app to fill the entire browser viewport with zero padding and margins. The header (if present) docks to the top, the footer (if present) docks to the bottom, and the main content area stretches to fill all remaining vertical and horizontal space. This layout ignores all max-width constraints and scrollbar gutter settings to ensure edge-to-edge display.

```xmlui-pg copy name="Example: 'desktop' layout" height="300px"
<App layout="desktop">
<AppHeader>
Expand All @@ -296,6 +294,8 @@ The `desktop` layout is designed for full-screen desktop applications. It stretc
</App>
```

The `desktop` layout is designed for full-screen desktop applications. It stretches the app to fill the entire browser viewport with zero padding and margins. The header (if present) docks to the top, the footer (if present) docks to the bottom, and the main content area stretches to fill all remaining vertical and horizontal space. This layout ignores all max-width constraints and scrollbar gutter settings to ensure edge-to-edge display.

%-PROP-END

%-PROP-START scrollWholePage
Expand Down
219 changes: 219 additions & 0 deletions xmlui/src/components/App/App.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,99 @@ test.describe("Basic Functionality", () => {
await initTestBed(`<App layout="vertical-full-header">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});

test("renders with desktop layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="desktop">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});

test("desktop layout fills viewport dimensions", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="desktop" testId="app">test content</App>`);

const app = page.getByTestId("app");
await expect(app).toBeVisible();
await expect(app).toHaveClass(/desktop/);
});

test("desktop layout renders with header and footer", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" testId="app">
<AppHeader testId="header">
<property name="logoTemplate">
<Text value="Desktop App" />
</property>
</AppHeader>
<Pages fallbackPath="/">
<Page url="/">
<Text testId="main-content">Main Content</Text>
</Page>
</Pages>
<Footer testId="footer">Footer Content</Footer>
</App>
`);

await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByTestId("header")).toBeVisible();
await expect(page.getByTestId("main-content")).toBeVisible();
await expect(page.getByTestId("footer")).toBeVisible();
await expect(page.getByText("Desktop App")).toBeVisible();
await expect(page.getByText("Footer Content")).toBeVisible();
});

test("desktop layout works without header", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" testId="app">
<Pages fallbackPath="/">
<Page url="/">
<Text testId="content">Content without header</Text>
</Page>
</Pages>
<Footer testId="footer">Footer</Footer>
</App>
`);

await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByTestId("content")).toBeVisible();
await expect(page.getByTestId("footer")).toBeVisible();
});

test("desktop layout works without footer", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" testId="app">
<AppHeader testId="header">
<property name="logoTemplate">
<Text value="Header Only" />
</property>
</AppHeader>
<Pages fallbackPath="/">
<Page url="/">
<Text testId="content">Content without footer</Text>
</Page>
</Pages>
</App>
`);

await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByTestId("header")).toBeVisible();
await expect(page.getByTestId("content")).toBeVisible();
await expect(page.getByText("Header Only")).toBeVisible();
});

test("desktop layout works with only content", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" testId="app">
<Pages fallbackPath="/">
<Page url="/">
<Text testId="content">Content only</Text>
</Page>
</Pages>
</App>
`);

await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByTestId("content")).toBeVisible();
});

test("handles layout prop changes correctly", async ({
page,
initTestBed,
Expand Down Expand Up @@ -405,3 +498,129 @@ test.describe("Drawer Handling", () => {
await expect (hamburgerButton).not.toBeVisible();
});
});

// =============================================================================
// DESKTOP LAYOUT SPECIFIC TESTS
// =============================================================================

test.describe("Desktop Layout", () => {
test("desktop layout applies nested-app class when used in NestedApp context", async ({ initTestBed, page }) => {
// Note: In actual nested context (playground), isNested would be true automatically
// This test verifies the class is applied when the condition is met
await initTestBed(`<App layout="desktop" testId="app">test content</App>`);

const app = page.getByTestId("app");
await expect(app).toBeVisible();
await expect(app).toHaveClass(/desktop/);
});

test("desktop layout stretches content area vertically", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" testId="app">
<AppHeader testId="header">Header</AppHeader>
<Pages fallbackPath="/">
<Page url="/">
<Text testId="content">Content that should stretch</Text>
</Page>
</Pages>
<Footer testId="footer">Footer</Footer>
</App>
`);

await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByTestId("header")).toBeVisible();
await expect(page.getByTestId("content")).toBeVisible();
await expect(page.getByTestId("footer")).toBeVisible();
});

test("desktop layout handles scrolling content", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" testId="app">
<AppHeader testId="header">Header</AppHeader>
<Pages fallbackPath="/">
<Page url="/">
<VStack testId="content" gap="4">
<Text value="Item 1" />
<Text value="Item 2" />
<Text value="Item 3" />
<Text value="Item 4" />
<Text value="Item 5" />
</VStack>
</Page>
</Pages>
<Footer testId="footer">Footer</Footer>
</App>
`);

await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByText("Item 1")).toBeVisible();
await expect(page.getByText("Item 5")).toBeVisible();
});

test("desktop layout ignores scrollWholePage property", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" scrollWholePage="false" testId="app">
<Text testId="content">Content</Text>
</App>
`);

const app = page.getByTestId("app");
await expect(app).toBeVisible();
await expect(app).toHaveClass(/desktop/);
await expect(page.getByTestId("content")).toBeVisible();
});

test("desktop layout with NavPanel does not display navigation", async ({ initTestBed, page }) => {
await initTestBed(`
<App layout="desktop" testId="app">
<AppHeader testId="header">Header</AppHeader>
<NavPanel testId="nav">
<NavLink label="Home" to="/" />
<NavLink label="About" to="/about" />
</NavPanel>
<Pages fallbackPath="/">
<Page url="/">
<Text testId="content">Home Page</Text>
</Page>
</Pages>
<Footer testId="footer">Footer</Footer>
</App>
`);

await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByTestId("header")).toBeVisible();
await expect(page.getByTestId("content")).toBeVisible();
await expect(page.getByTestId("footer")).toBeVisible();

// NavPanel should not be visible in desktop layout
// (it's designed for full-screen apps without navigation panels)
await expect(page.getByTestId("nav")).not.toBeVisible();
});

test("desktop layout switches from other layout correctly", async ({ initTestBed, page, createButtonDriver }) => {
await initTestBed(`
<App var.currentLayout="horizontal" layout="{currentLayout}" testId="app">
<AppHeader testId="header">Header</AppHeader>
<Button testId="switchBtn" label="Switch to Desktop" onClick="currentLayout = 'desktop'" />
<Pages fallbackPath="/">
<Page url="/">
<Text testId="content">Content</Text>
</Page>
</Pages>
<Footer testId="footer">Footer</Footer>
</App>
`);

const app = page.getByTestId("app");
const buttonDriver = await createButtonDriver("switchBtn");

// Initially horizontal layout
await expect(app).toHaveClass(/horizontal/);
await expect(app).not.toHaveClass(/desktop/);

// Switch to desktop layout
await buttonDriver.click();
await expect(app).toHaveClass(/desktop/);
await expect(app).not.toHaveClass(/horizontal/);
});
});