Skip to content

Commit

Permalink
Implement DetailsPage UI component (podman-desktop#7694)
Browse files Browse the repository at this point in the history
refactor: move DetailsPage to ui lib
Signed-off-by: Philippe Martin <[email protected]>
  • Loading branch information
feloy authored Jun 18, 2024
1 parent 2a31670 commit d722e15
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 63 deletions.
80 changes: 17 additions & 63 deletions packages/renderer/src/lib/ui/DetailsPage.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { CloseButton, Link } from '@podman-desktop/ui-svelte';
import { DetailsPage } from '@podman-desktop/ui-svelte';
import { router } from 'tinro';
import { currentPage, lastPage } from '../../stores/breadcrumb';
Expand All @@ -11,67 +11,21 @@ export let subtitle: string | undefined = undefined;
export function close(): void {
router.goto($lastPage.path);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
close();
e.preventDefault();
}
}
</script>

<svelte:window on:keydown="{handleKeydown}" />

<div class="flex flex-col w-full h-full shadow-pageheader">
<div class="flex flex-row w-full h-fit px-5 pt-4 pb-2">
<div class="flex flex-col w-full h-fit">
<div class="flex flew-row items-center text-sm text-[var(--pd-content-breadcrumb)]">
<Link
class="text-sm"
aria-label="back"
on:click="{() => router.goto($lastPage.path)}"
title="Go back to {$lastPage.name}">{$lastPage.name}</Link>
<div class="mx-2">&gt;</div>
<div class="grow font-extralight" aria-label="name">{$currentPage.name}</div>
<CloseButton class="justify-self-end" on:click="{() => router.goto($lastPage.path)}" />
</div>
<div class="flex flex-row items-start pt-1">
<div class="pr-3">
<slot name="icon" />
</div>
<div class="flex flex-col grow pr-2">
<div class="flex flex-row items-baseline">
<h1 aria-label="{title}" class="text-xl leading-tight text-[var(--pd-content-header)]">{title}</h1>
<div
class="text-[var(--pd-table-body-text-sub-secondary)] ml-2 leading-normal"
class:hidden="{!titleDetail}">
{titleDetail}
</div>
</div>
<div>
<span
class="text-sm leading-none text-[var(--pd-content-sub-header)] line-clamp-1"
class:hidden="{!subtitle}">{subtitle}</span>
<slot name="subtitle" />
</div>
</div>
<div class="flex flex-col">
<div class="flex flex-nowrap justify-self-end pl-3 space-x-2">
<slot name="actions" />
</div>
<div class="relative">
<div class="absolute top-0 right-0">
<slot name="detail" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-row px-2 border-b border-[var(--pd-content-divider)]">
<slot name="tabs" />
</div>
<div class="h-full bg-[var(--pd-details-bg)] min-h-0">
<slot name="content" />
</div>
</div>
<DetailsPage
title="{title}"
titleDetail="{titleDetail}"
subtitle="{subtitle}"
breadcrumbLeftPart="{$lastPage.name}"
breadcrumbRightPart="{$currentPage.name}"
breadcrumbTitle="Go back to {$lastPage.name}"
on:close="{close}"
on:breadcrumbClick="{close}">
<slot slot="icon" name="icon" />
<slot slot="subtitle" name="subtitle" />
<slot slot="actions" name="actions" />
<slot slot="detail" name="detail" />
<slot slot="tabs" name="tabs" />
<slot slot="content" name="content" />
</DetailsPage>
4 changes: 4 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"./SettingsNavItem": {
"types": "./dist/settingsNavItem/SettingsNavItem.d.ts",
"svelte": "./dist/settingsNavItem/SettingsNavItem.svelte"
},
"./DetailsPage": {
"types": "./dist/layouts/DetailsPage.d.ts",
"svelte": "./dist/layouts/DetailsPage.svelte"
}
},
"peerDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Checkbox from './checkbox/Checkbox.svelte';
import DropdownMenu from './dropdownMenu';
import Input from './inputs/Input.svelte';
import SearchInput from './inputs/SearchInput.svelte';
import DetailsPage from './layouts/DetailsPage.svelte';
import FormPage from './layouts/FormPage.svelte';
import NavPage from './layouts/NavPage.svelte';
import Link from './link/Link.svelte';
Expand All @@ -45,6 +46,7 @@ export {
Button,
Checkbox,
CloseButton,
DetailsPage,
DropdownMenu,
EmptyScreen,
ErrorMessage,
Expand Down
111 changes: 111 additions & 0 deletions packages/ui/src/lib/layouts/DetailsPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**********************************************************************
* Copyright (C) 2023-2024, 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import '@testing-library/jest-dom/vitest';

import { fireEvent, render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { beforeEach, expect, test, vi } from 'vitest';

import DetailsPage from './DetailsPage.svelte';

beforeEach(() => {
vi.clearAllMocks();
});

test('Expect title is defined', async () => {
const title = 'My Dummy Title';
render(DetailsPage, {
title,
});

const titleElement = screen.getByRole('heading', { level: 1, name: title });
expect(titleElement).toBeInTheDocument();
expect(titleElement).toHaveTextContent(title);
});

test('Expect name is defined', async () => {
const name = 'My Dummy Name';
render(DetailsPage, {
title: 'No Title',
breadcrumbRightPart: name,
});

const nameElement = screen.getByLabelText('name');
expect(nameElement).toBeInTheDocument();
expect(nameElement).toHaveTextContent(name);
});

test('Expect backlink is defined', async () => {
const backName = 'Last page';
const breadcrumbClickMock = vi.fn();
const comp = render(DetailsPage, {
title: 'No Title',
breadcrumbLeftPart: backName,
});
comp.component.$on('breadcrumbClick', breadcrumbClickMock);

const backElement = screen.getByLabelText('back');
expect(backElement).toBeInTheDocument();
expect(backElement).toHaveTextContent(backName);

await fireEvent.click(backElement);

expect(breadcrumbClickMock).toHaveBeenCalled();
});

test('Expect close link is defined', async () => {
const closeClickMock = vi.fn();
const comp = render(DetailsPage, {
title: 'No Title',
});
comp.component.$on('close', closeClickMock);

const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
await fireEvent.click(closeElement);

expect(closeClickMock).toHaveBeenCalled();
});

test('Expect Escape key closes', async () => {
const closeClickMock = vi.fn();
const comp = render(DetailsPage, {
title: 'No Title',
});
comp.component.$on('close', closeClickMock);

await userEvent.keyboard('{Escape}');

expect(closeClickMock).toHaveBeenCalled();
});

test('Expect subtitle is defined and cut', async () => {
const subtitle = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit';
render(DetailsPage, {
title: '',
subtitle,
});

// get the element having the 'Lorem ipsum' text
const subtitleElement = screen.getByText(subtitle);
expect(subtitleElement).toBeInTheDocument();

// expect class has the clamp
expect(subtitleElement).toHaveClass('line-clamp-1');
});
89 changes: 89 additions & 0 deletions packages/ui/src/lib/layouts/DetailsPage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import CloseButton from '../button/CloseButton.svelte';
import Link from '../link/Link.svelte';
export let title: string;
export let titleDetail: string | undefined = undefined;
export let subtitle: string | undefined = undefined;
export let breadcrumbLeftPart: string | undefined = undefined;
export let breadcrumbRightPart: string | undefined = undefined;
export let breadcrumbTitle: string | undefined = '';
const dispatchClose = createEventDispatcher<{ close: undefined }>();
export function close(): void {
dispatchClose('close');
}
const dispatchBreadCrumb = createEventDispatcher<{ breadcrumbClick: undefined }>();
function breadcrumbClick(): void {
dispatchBreadCrumb('breadcrumbClick');
}
function handleKeydown(e: KeyboardEvent): void {
if (e.key === 'Escape') {
close();
e.preventDefault();
}
}
</script>

<svelte:window on:keydown="{handleKeydown}" />

<div class="flex flex-col w-full h-full shadow-pageheader">
<div class="flex flex-row w-full h-fit px-5 pt-4 pb-2">
<div class="flex flex-col w-full h-fit">
<div class="flex flew-row items-center text-sm text-[var(--pd-content-breadcrumb)]">
{#if breadcrumbLeftPart}
<Link class="text-sm" aria-label="back" on:click="{breadcrumbClick}" title="{breadcrumbTitle}"
>{breadcrumbLeftPart}</Link>
{/if}
{#if breadcrumbRightPart}
<div class="mx-2">&gt;</div>
<div class="grow font-extralight" aria-label="name">{breadcrumbRightPart}</div>
{/if}
<CloseButton class="justify-self-end" on:click="{close}" />
</div>
<div class="flex flex-row items-start pt-1">
<div class="pr-3">
<slot name="icon" />
</div>
<div class="flex flex-col grow pr-2">
<div class="flex flex-row items-baseline">
<h1 aria-label="{title}" class="text-xl leading-tight text-[var(--pd-content-header)]">{title}</h1>
<div
class="text-[var(--pd-table-body-text-sub-secondary)] ml-2 leading-normal"
class:hidden="{!titleDetail}">
{titleDetail}
</div>
</div>
<div>
<span
class="text-sm leading-none text-[var(--pd-content-sub-header)] line-clamp-1"
class:hidden="{!subtitle}">{subtitle}</span>
<slot name="subtitle" />
</div>
</div>
<div class="flex flex-col">
<div class="flex flex-nowrap justify-self-end pl-3 space-x-2">
<slot name="actions" />
</div>
<div class="relative">
<div class="absolute top-0 right-0">
<slot name="detail" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-row px-2 border-b border-[var(--pd-content-divider)]">
<slot name="tabs" />
</div>
<div class="h-full bg-[var(--pd-details-bg)] min-h-0">
<slot name="content" />
</div>
</div>

0 comments on commit d722e15

Please sign in to comment.