Skip to content

Commit 07b1a3d

Browse files
authored
Add a new layout, "desktop", to App (#2327)
1 parent 800fe22 commit 07b1a3d

File tree

5 files changed

+233
-10
lines changed

5 files changed

+233
-10
lines changed

.changeset/fancy-laws-drop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"xmlui": patch
3+
---
4+
5+
Add a new layout, "desktop", to App

docs/content/components/App.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,6 @@ Here are a few samples demonstrating the usage of the `layout` property. All sam
299299

300300
#### `desktop` [#desktop]
301301

302-
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.
303-
304302
```xmlui-pg copy name="Example: 'desktop' layout" height="300px"
305303
<App layout="desktop">
306304
<AppHeader>
@@ -327,6 +325,8 @@ The `desktop` layout is designed for full-screen desktop applications. It stretc
327325
</App>
328326
```
329327

328+
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.
329+
330330
### `loggedInUser` [#loggedinuser]
331331

332332
Stores information about the currently logged-in user. By not defining this property, you can indicate that no user is logged in.

docs/content/components/_overview.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
| [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. |
1515
| [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. |
1616
| [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. |
17+
| [CHStack](./CHStack) | This component represents a stack that renders its contents horizontally and aligns that in the center along both axes. |
18+
| [CVStack](./CVStack) | This component represents a stack that renders its contents vertically and aligns that in the center along both axes. |
1719
| [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. |
1820
| [Carousel](./Carousel) | This component displays a slideshow by cycling through elements (images, text, or custom slides) like a carousel. |
1921
| [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. |
2022
| [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. |
21-
| [CHStack](./CHStack) | This component represents a stack that renders its contents horizontally and aligns that in the center along both axes. |
2223
| [ColorPicker](./ColorPicker) | `ColorPicker` enables users to choose colors by specifying RGB, HSL, or HEX values. |
2324
| [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. |
2425
| [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. |
25-
| [CVStack](./CVStack) | This component represents a stack that renders its contents vertically and aligns that in the center along both axes. |
2626
| [DataSource](./DataSource) | `DataSource` fetches and caches data from API endpoints, versus [`APICall`](/components/APICall) which creates, updates or deletes data. |
2727
| [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. |
2828
| [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. |
@@ -46,11 +46,11 @@
4646
| [H4](./H4) | Represents a heading level 4 text |
4747
| [H5](./H5) | Represents a heading level 5 text |
4848
| [H6](./H6) | Represents a heading level 6 text |
49-
| [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). |
5049
| [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. |
5150
| [HStack](./HStack) | This component represents a stack rendering its contents horizontally. |
52-
| [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. |
51+
| [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). |
5352
| [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. |
53+
| [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. |
5454
| [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. |
5555
| [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. |
5656
| [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. |
@@ -109,5 +109,4 @@ Hover over the component to see the tooltip with the current value. On mobile, t
109109
| [Tooltip](./Tooltip) | A tooltip component that displays text when hovering over trigger content. |
110110
| [Tree](./Tree) | The `Tree` component is a virtualized tree component that displays hierarchical data with support for flat and hierarchy data formats. |
111111
| [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. |
112-
| [VStack](./VStack) | This component represents a stack rendering its contents vertically. |
113-
112+
| [VStack](./VStack) | This component represents a stack rendering its contents vertically. |

xmlui/src/components/App/App.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,6 @@ Here are a few samples demonstrating the usage of the `layout` property. All sam
268268

269269
#### `desktop`
270270

271-
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.
272-
273271
```xmlui-pg copy name="Example: 'desktop' layout" height="300px"
274272
<App layout="desktop">
275273
<AppHeader>
@@ -296,6 +294,8 @@ The `desktop` layout is designed for full-screen desktop applications. It stretc
296294
</App>
297295
```
298296

297+
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.
298+
299299
%-PROP-END
300300

301301
%-PROP-START scrollWholePage

xmlui/src/components/App/App.spec.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,99 @@ test.describe("Basic Functionality", () => {
4444
await initTestBed(`<App layout="vertical-full-header">test text</App>`);
4545
await expect(page.getByText("test text")).toBeVisible();
4646
});
47+
48+
test("renders with desktop layout", async ({ initTestBed, page }) => {
49+
await initTestBed(`<App layout="desktop">test text</App>`);
50+
await expect(page.getByText("test text")).toBeVisible();
51+
});
52+
53+
test("desktop layout fills viewport dimensions", async ({ initTestBed, page }) => {
54+
await initTestBed(`<App layout="desktop" testId="app">test content</App>`);
55+
56+
const app = page.getByTestId("app");
57+
await expect(app).toBeVisible();
58+
await expect(app).toHaveClass(/desktop/);
59+
});
60+
61+
test("desktop layout renders with header and footer", async ({ initTestBed, page }) => {
62+
await initTestBed(`
63+
<App layout="desktop" testId="app">
64+
<AppHeader testId="header">
65+
<property name="logoTemplate">
66+
<Text value="Desktop App" />
67+
</property>
68+
</AppHeader>
69+
<Pages fallbackPath="/">
70+
<Page url="/">
71+
<Text testId="main-content">Main Content</Text>
72+
</Page>
73+
</Pages>
74+
<Footer testId="footer">Footer Content</Footer>
75+
</App>
76+
`);
77+
78+
await expect(page.getByTestId("app")).toBeVisible();
79+
await expect(page.getByTestId("header")).toBeVisible();
80+
await expect(page.getByTestId("main-content")).toBeVisible();
81+
await expect(page.getByTestId("footer")).toBeVisible();
82+
await expect(page.getByText("Desktop App")).toBeVisible();
83+
await expect(page.getByText("Footer Content")).toBeVisible();
84+
});
85+
86+
test("desktop layout works without header", async ({ initTestBed, page }) => {
87+
await initTestBed(`
88+
<App layout="desktop" testId="app">
89+
<Pages fallbackPath="/">
90+
<Page url="/">
91+
<Text testId="content">Content without header</Text>
92+
</Page>
93+
</Pages>
94+
<Footer testId="footer">Footer</Footer>
95+
</App>
96+
`);
97+
98+
await expect(page.getByTestId("app")).toBeVisible();
99+
await expect(page.getByTestId("content")).toBeVisible();
100+
await expect(page.getByTestId("footer")).toBeVisible();
101+
});
102+
103+
test("desktop layout works without footer", async ({ initTestBed, page }) => {
104+
await initTestBed(`
105+
<App layout="desktop" testId="app">
106+
<AppHeader testId="header">
107+
<property name="logoTemplate">
108+
<Text value="Header Only" />
109+
</property>
110+
</AppHeader>
111+
<Pages fallbackPath="/">
112+
<Page url="/">
113+
<Text testId="content">Content without footer</Text>
114+
</Page>
115+
</Pages>
116+
</App>
117+
`);
118+
119+
await expect(page.getByTestId("app")).toBeVisible();
120+
await expect(page.getByTestId("header")).toBeVisible();
121+
await expect(page.getByTestId("content")).toBeVisible();
122+
await expect(page.getByText("Header Only")).toBeVisible();
123+
});
124+
125+
test("desktop layout works with only content", async ({ initTestBed, page }) => {
126+
await initTestBed(`
127+
<App layout="desktop" testId="app">
128+
<Pages fallbackPath="/">
129+
<Page url="/">
130+
<Text testId="content">Content only</Text>
131+
</Page>
132+
</Pages>
133+
</App>
134+
`);
135+
136+
await expect(page.getByTestId("app")).toBeVisible();
137+
await expect(page.getByTestId("content")).toBeVisible();
138+
});
139+
47140
test("handles layout prop changes correctly", async ({
48141
page,
49142
initTestBed,
@@ -405,3 +498,129 @@ test.describe("Drawer Handling", () => {
405498
await expect (hamburgerButton).not.toBeVisible();
406499
});
407500
});
501+
502+
// =============================================================================
503+
// DESKTOP LAYOUT SPECIFIC TESTS
504+
// =============================================================================
505+
506+
test.describe("Desktop Layout", () => {
507+
test("desktop layout applies nested-app class when used in NestedApp context", async ({ initTestBed, page }) => {
508+
// Note: In actual nested context (playground), isNested would be true automatically
509+
// This test verifies the class is applied when the condition is met
510+
await initTestBed(`<App layout="desktop" testId="app">test content</App>`);
511+
512+
const app = page.getByTestId("app");
513+
await expect(app).toBeVisible();
514+
await expect(app).toHaveClass(/desktop/);
515+
});
516+
517+
test("desktop layout stretches content area vertically", async ({ initTestBed, page }) => {
518+
await initTestBed(`
519+
<App layout="desktop" testId="app">
520+
<AppHeader testId="header">Header</AppHeader>
521+
<Pages fallbackPath="/">
522+
<Page url="/">
523+
<Text testId="content">Content that should stretch</Text>
524+
</Page>
525+
</Pages>
526+
<Footer testId="footer">Footer</Footer>
527+
</App>
528+
`);
529+
530+
await expect(page.getByTestId("app")).toBeVisible();
531+
await expect(page.getByTestId("header")).toBeVisible();
532+
await expect(page.getByTestId("content")).toBeVisible();
533+
await expect(page.getByTestId("footer")).toBeVisible();
534+
});
535+
536+
test("desktop layout handles scrolling content", async ({ initTestBed, page }) => {
537+
await initTestBed(`
538+
<App layout="desktop" testId="app">
539+
<AppHeader testId="header">Header</AppHeader>
540+
<Pages fallbackPath="/">
541+
<Page url="/">
542+
<VStack testId="content" gap="4">
543+
<Text value="Item 1" />
544+
<Text value="Item 2" />
545+
<Text value="Item 3" />
546+
<Text value="Item 4" />
547+
<Text value="Item 5" />
548+
</VStack>
549+
</Page>
550+
</Pages>
551+
<Footer testId="footer">Footer</Footer>
552+
</App>
553+
`);
554+
555+
await expect(page.getByTestId("app")).toBeVisible();
556+
await expect(page.getByText("Item 1")).toBeVisible();
557+
await expect(page.getByText("Item 5")).toBeVisible();
558+
});
559+
560+
test("desktop layout ignores scrollWholePage property", async ({ initTestBed, page }) => {
561+
await initTestBed(`
562+
<App layout="desktop" scrollWholePage="false" testId="app">
563+
<Text testId="content">Content</Text>
564+
</App>
565+
`);
566+
567+
const app = page.getByTestId("app");
568+
await expect(app).toBeVisible();
569+
await expect(app).toHaveClass(/desktop/);
570+
await expect(page.getByTestId("content")).toBeVisible();
571+
});
572+
573+
test("desktop layout with NavPanel does not display navigation", async ({ initTestBed, page }) => {
574+
await initTestBed(`
575+
<App layout="desktop" testId="app">
576+
<AppHeader testId="header">Header</AppHeader>
577+
<NavPanel testId="nav">
578+
<NavLink label="Home" to="/" />
579+
<NavLink label="About" to="/about" />
580+
</NavPanel>
581+
<Pages fallbackPath="/">
582+
<Page url="/">
583+
<Text testId="content">Home Page</Text>
584+
</Page>
585+
</Pages>
586+
<Footer testId="footer">Footer</Footer>
587+
</App>
588+
`);
589+
590+
await expect(page.getByTestId("app")).toBeVisible();
591+
await expect(page.getByTestId("header")).toBeVisible();
592+
await expect(page.getByTestId("content")).toBeVisible();
593+
await expect(page.getByTestId("footer")).toBeVisible();
594+
595+
// NavPanel should not be visible in desktop layout
596+
// (it's designed for full-screen apps without navigation panels)
597+
await expect(page.getByTestId("nav")).not.toBeVisible();
598+
});
599+
600+
test("desktop layout switches from other layout correctly", async ({ initTestBed, page, createButtonDriver }) => {
601+
await initTestBed(`
602+
<App var.currentLayout="horizontal" layout="{currentLayout}" testId="app">
603+
<AppHeader testId="header">Header</AppHeader>
604+
<Button testId="switchBtn" label="Switch to Desktop" onClick="currentLayout = 'desktop'" />
605+
<Pages fallbackPath="/">
606+
<Page url="/">
607+
<Text testId="content">Content</Text>
608+
</Page>
609+
</Pages>
610+
<Footer testId="footer">Footer</Footer>
611+
</App>
612+
`);
613+
614+
const app = page.getByTestId("app");
615+
const buttonDriver = await createButtonDriver("switchBtn");
616+
617+
// Initially horizontal layout
618+
await expect(app).toHaveClass(/horizontal/);
619+
await expect(app).not.toHaveClass(/desktop/);
620+
621+
// Switch to desktop layout
622+
await buttonDriver.click();
623+
await expect(app).toHaveClass(/desktop/);
624+
await expect(app).not.toHaveClass(/horizontal/);
625+
});
626+
});

0 commit comments

Comments
 (0)