Skip to content
Open
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
4 changes: 2 additions & 2 deletions extensions/default/src/ViewerLayout/ViewerHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }
// Whatever fills the right side of the menu bar, in order: undo/redo then
// patient info by default. Each item is rendered as a component, so it can
// bring its own hooks, and reordering the list reorders the header.
const rightSideItems =
customizationService.getCustomization('ohif.headerRightSide')?.items ?? [];
const rightSideItems = customizationService.getCustomization('ohif.headerRightSide')?.items ?? [];

const menuOptions = [
{
Expand Down Expand Up @@ -104,6 +103,7 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }

return (
<Header
isResponsive
menuOptions={menuOptions}
isReturnEnabled={!!appConfig.showStudyList}
onClickReturnButton={onClickReturnButton}
Expand Down
13 changes: 6 additions & 7 deletions extensions/default/src/ViewerLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,18 @@ function ViewerLayout({
const viewportComponents = viewports.map(getViewportComponentData);

return (
<div>
<div className="flex h-screen min-h-0 flex-col overflow-hidden">
<ViewerHeader
hotkeysManager={hotkeysManager}
extensionManager={extensionManager}
servicesManager={servicesManager}
appConfig={appConfig}
/>
<div
className="relative flex w-full flex-row flex-nowrap items-stretch overflow-hidden bg-background"
style={{ height: 'calc(100vh - 52px)' }}
>
<div className="bg-background relative flex min-h-0 w-full flex-1 flex-row flex-nowrap items-stretch overflow-hidden">
<React.Fragment>
{showLoadingIndicator && <LoadingIndicatorProgress className="h-full w-full bg-background" />}
{showLoadingIndicator && (
<LoadingIndicatorProgress className="bg-background h-full w-full" />
)}
<ResizablePanelGroup {...resizablePanelGroupProps}>
{/* LEFT SIDEPANELS */}
{hasLeftPanels ? (
Expand All @@ -186,7 +185,7 @@ function ViewerLayout({
<ResizablePanel {...resizableViewportGridPanelProps}>
<div className="flex h-full flex-1 flex-col">
<div
className="relative flex h-full flex-1 items-center justify-center overflow-hidden bg-background"
className="bg-background relative flex h-full flex-1 items-center justify-center overflow-hidden"
onMouseEnter={handleMouseEnter}
>
<ViewportGridComp
Expand Down
95 changes: 78 additions & 17 deletions platform/ui-next/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ interface HeaderProps {
isReturnEnabled?: boolean;
onClickReturnButton?: () => void;
isSticky?: boolean;
/** Enables the two-row layout with horizontally scrollable controls below the desktop breakpoint. */
isResponsive?: boolean;
WhiteLabeling?: {
createLogoComponentFn?: (React: any, props: any) => ReactNode;
};
Expand All @@ -44,6 +46,7 @@ function Header({
isReturnEnabled = true,
onClickReturnButton,
isSticky = false,
isResponsive = false,
WhiteLabeling,
RightSide = [],
Secondary,
Expand All @@ -64,8 +67,24 @@ function Header({
isSticky={isSticky}
{...props}
>
<div className="relative h-[48px] items-center">
<div className="absolute left-0 top-1/2 flex -translate-y-1/2 items-center">
<div
className={classNames(
'relative items-center',
isResponsive
? 'grid h-[96px] grid-cols-[minmax(0,1fr)_auto] grid-rows-2 lg:block lg:h-[48px]'
: 'h-[48px]'
)}
data-cy="app-header"
>
<div
className={classNames(
'flex items-center',
isResponsive
? 'relative col-start-1 row-start-1 min-w-0 lg:absolute lg:left-0 lg:top-1/2 lg:-translate-y-1/2'
: 'absolute left-0 top-1/2 -translate-y-1/2'
)}
data-cy="app-header-branding"
>
<div
className={classNames(
'mr-3 inline-flex items-center',
Expand All @@ -80,22 +99,64 @@ function Header({
</div>
</div>
</div>
<div className="absolute top-1/2 left-[250px] h-8 -translate-y-1/2">{Secondary}</div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<div className="flex items-center justify-center space-x-2">{children}</div>
<div
className={classNames(
isResponsive
? 'col-span-2 row-start-2 flex min-w-0 items-center justify-start gap-2 overflow-x-auto lg:contents'
: 'contents'
)}
data-cy="app-header-toolbar"
>
<div
className={classNames(
'h-8',
isResponsive
? 'shrink-0 lg:absolute lg:top-1/2 lg:left-[250px] lg:-translate-y-1/2'
: 'absolute top-1/2 left-[250px] -translate-y-1/2'
)}
>
{Secondary}
</div>
<div
className={classNames(
isResponsive
? 'shrink-0 lg:absolute lg:top-1/2 lg:left-1/2 lg:-translate-x-1/2 lg:-translate-y-1/2 lg:transform'
: 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform'
)}
>
<div className="flex items-center justify-center space-x-2">{children}</div>
</div>
<div
className={classNames(
'flex items-center',
isResponsive
? 'shrink-0 lg:absolute lg:top-1/2 lg:right-[28px] lg:-translate-y-1/2'
: 'absolute top-1/2 right-[28px] -translate-y-1/2'
)}
data-cy="app-header-context-actions"
>
{RightSide.map((item, index) => (
// The separator is an `::after` so that `empty:hidden` can drop
// the whole slot — separator included — when the item rendered
// nothing (e.g. patient info with `showPatientInfo: 'disabled'`).
<div
key={index}
className="after:border-muted flex items-center after:mx-1.5 after:h-[25px] after:border-r after:content-[''] empty:hidden"
>
{item}
</div>
))}
</div>
</div>
<div className="absolute right-0 top-1/2 flex -translate-y-1/2 select-none items-center">
{RightSide.map((item, index) => (
// The separator is an `::after` so that `empty:hidden` can drop
// the whole slot — separator included — when the item rendered
// nothing (e.g. patient info with `showPatientInfo: 'disabled'`).
<div
key={index}
className="after:border-muted flex items-center empty:hidden after:mx-1.5 after:h-[25px] after:border-r after:content-['']"
>
{item}
</div>
))}
<div
className={classNames(
'flex select-none items-center',
isResponsive
? 'relative col-start-2 row-start-1 justify-self-end lg:absolute lg:top-1/2 lg:right-0 lg:-translate-y-1/2'
: 'absolute top-1/2 right-0 -translate-y-1/2'
)}
data-cy="app-header-actions"
>
<div className="flex-shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down
64 changes: 64 additions & 0 deletions tests/ViewerHeaderResponsive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { addOHIFConfiguration, expect, test, visitStudy } from './utils';

const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';

test('keeps the default app header compact on mobile', async ({ page, headerPageObject }) => {
await addOHIFConfiguration(page, {
customizationService: [{ 'workList.variant': { $set: 'legacy' } }],
});
await page.setViewportSize({ width: 382, height: 693 });
await page.goto('/');

await expect(headerPageObject.locator).toBeVisible();

const headerBox = await headerPageObject.locator.boundingBox();

expect(headerBox).not.toBeNull();
expect(headerBox!.height).toBe(48);
});

test('keeps the viewer header regions separate on mobile', async ({ page, headerPageObject }) => {
await page.setViewportSize({ width: 382, height: 693 });
await visitStudy(page, studyInstanceUID, 'viewer', 2000);

const { locator, branding, toolbar, firstToolbarSection, actions, contextActions } =
headerPageObject;

await expect(locator).toBeVisible();
await expect(branding).toBeVisible();
await expect(toolbar).toBeVisible();
await expect(actions).toBeVisible();
await expect(contextActions).toBeVisible();

const [brandingBox, toolbarBox, actionsBox] = await Promise.all([
branding.boundingBox(),
toolbar.boundingBox(),
actions.boundingBox(),
]);

expect(brandingBox).not.toBeNull();
expect(toolbarBox).not.toBeNull();
expect(actionsBox).not.toBeNull();
expect(brandingBox!.y + brandingBox!.height).toBeLessThanOrEqual(toolbarBox!.y);
expect(brandingBox!.x + brandingBox!.width).toBeLessThanOrEqual(actionsBox!.x);

await toolbar.evaluate(element => element.scrollTo({ left: element.scrollWidth }));
await expect(contextActions).toBeInViewport();

await page.setViewportSize({ width: 640, height: 900 });
await toolbar.evaluate(element => element.scrollTo({ left: 0 }));
const [tabletToolbarBox, firstToolbarSectionBox] = await Promise.all([
toolbar.boundingBox(),
firstToolbarSection.boundingBox(),
]);

expect(tabletToolbarBox).not.toBeNull();
expect(firstToolbarSectionBox).not.toBeNull();
expect(firstToolbarSectionBox!.x).toBeGreaterThanOrEqual(tabletToolbarBox!.x);

await page.setViewportSize({ width: 1024, height: 900 });
const desktopHeaderBox = await locator.boundingBox();

expect(desktopHeaderBox).not.toBeNull();
expect(desktopHeaderBox!.height).toBe(48);
});
29 changes: 29 additions & 0 deletions tests/pages/HeaderPageObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Page } from '@playwright/test';

export class HeaderPageObject {
constructor(private readonly page: Page) {}

get locator() {
return this.page.getByTestId('app-header');
}

get branding() {
return this.page.getByTestId('app-header-branding');
}

get toolbar() {
return this.page.getByTestId('app-header-toolbar');
}

get firstToolbarSection() {
return this.toolbar.locator(':scope > :first-child');
}

get actions() {
return this.page.getByTestId('app-header-actions');
}

get contextActions() {
return this.page.getByTestId('app-header-context-actions');
}
}
2 changes: 2 additions & 0 deletions tests/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ViewportPageObject } from './ViewportPageObject';
import { NotFoundStudyPageObject } from './NotFoundStudyPageObject';
import { DicomTagBrowserPageObject } from './DicomTagBrowserPageObject';
import { MagnifyGlassPageObject } from './MagnifyGlassPageObject';
import { HeaderPageObject } from './HeaderPageObject';

export {
DOMOverlayPageObject,
Expand All @@ -16,4 +17,5 @@ export {
NotFoundStudyPageObject,
DicomTagBrowserPageObject,
MagnifyGlassPageObject,
HeaderPageObject,
};
5 changes: 5 additions & 0 deletions tests/utils/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
RightPanelPageObject,
ViewportPageObject,
NotFoundStudyPageObject,
HeaderPageObject,
} from '../pages';

type PageObjects = {
Expand All @@ -16,6 +17,7 @@ type PageObjects = {
rightPanelPageObject: RightPanelPageObject;
viewportPageObject: ViewportPageObject;
notFoundStudyPageObject: NotFoundStudyPageObject;
headerPageObject: HeaderPageObject;
};

type TestFixtures = PageObjects & {
Expand Down Expand Up @@ -48,6 +50,9 @@ export const test = base.extend<TestFixtures>({
notFoundStudyPageObject: async ({ page }, use) => {
await use(new NotFoundStudyPageObject(page));
},
headerPageObject: async ({ page }, use) => {
await use(new HeaderPageObject(page));
},
});

export { expect } from 'playwright-test-coverage';