diff --git a/pages/app-layout/split-panel-with-custom-header.page.tsx b/pages/app-layout/split-panel-with-custom-header.page.tsx new file mode 100644 index 0000000000..2edcfe909b --- /dev/null +++ b/pages/app-layout/split-panel-with-custom-header.page.tsx @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useEffect, useState } from 'react'; + +import AppLayout, { AppLayoutProps } from '~components/app-layout'; +import Button from '~components/button'; +import FormField from '~components/form-field'; +import Header from '~components/header'; +import Input from '~components/input'; +import Link from '~components/link'; +import SpaceBetween from '~components/space-between'; +import SplitPanel from '~components/split-panel'; +import Toggle from '~components/toggle'; + +import AppContext, { AppContextType } from '../app/app-context'; +import ScreenshotArea from '../utils/screenshot-area'; +import { Breadcrumbs, Containers, Navigation, ScrollableDrawerContent, Tools } from './utils/content-blocks'; +import labels from './utils/labels'; +import { splitPaneli18nStrings } from './utils/strings'; +import * as toolsContent from './utils/tools-content'; + +type SplitPanelDemoContext = React.Context< + AppContextType<{ + actionsAsLinks: boolean; + description?: string; + headerText?: string; + renderActions: boolean; + renderInfoLink: boolean; + splitPanelOpen: boolean; + splitPanelPosition: AppLayoutProps.SplitPanelPreferences['position']; + }> +>; + +export default function () { + const { urlParams, setUrlParams } = useContext(AppContext as SplitPanelDemoContext); + const [toolsOpen, setToolsOpen] = useState(false); + + // Initialize the header to a default value if not set. + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => setUrlParams({ ...urlParams, headerText: urlParams.headerText || 'Header text' }), []); + + return ( + + } + navigation={} + tools={{toolsContent.long}} + toolsOpen={toolsOpen} + splitPanelOpen={urlParams.splitPanelOpen} + onSplitPanelToggle={({ detail }) => setUrlParams({ ...urlParams, splitPanelOpen: detail.open })} + splitPanelPreferences={{ + position: urlParams.splitPanelPosition, + }} + onSplitPanelPreferencesChange={event => { + const { position } = event.detail; + setUrlParams({ splitPanelPosition: position === 'side' ? position : undefined }); + }} + onToolsChange={({ detail }) => setToolsOpen(detail.open)} + splitPanel={ + Action + ) : ( + + + + + )) + } + headerDescription={urlParams.description} + headerInfo={ + urlParams.renderInfoLink && ( + setToolsOpen(true)}> + Info + + ) + } + > + + + } + content={ + <> +
+
+ Demo page +
+
+ + setUrlParams({ ...urlParams, renderInfoLink: detail.checked })} + > + With info link + + + setUrlParams({ ...urlParams, renderActions: detail.checked })} + > + With action buttons + + {urlParams.renderActions && ( + setUrlParams({ ...urlParams, actionsAsLinks: detail.checked })} + > + As links + + )} + + + setUrlParams({ ...urlParams, headerText: detail.value })} + /> + + + setUrlParams({ ...urlParams, description: detail.value })} + /> + + + + + } + /> +
+ ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index fea1f30ae1..e05941a9a8 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -21583,6 +21583,21 @@ use the \`id\` attribute, consider setting it on a parent element instead.", "isDefault": true, "name": "children", }, + { + "description": "Actions for the header. Available only if you specify the \`header\` property.", + "isDefault": false, + "name": "headerActions", + }, + { + "description": "Supplementary text below the heading.", + "isDefault": false, + "name": "headerDescription", + }, + { + "description": "The area next to the heading, used to display an Info link.", + "isDefault": false, + "name": "headerInfo", + }, ], "releaseStatus": "stable", } @@ -83176,6 +83191,30 @@ Component's wrapper class", "type": "reference", }, }, + { + "name": "findHeaderActions", + "parameters": [], + "returnType": { + "name": "null | ElementWrapper", + "type": "union", + }, + }, + { + "name": "findHeaderDescription", + "parameters": [], + "returnType": { + "name": "null | ElementWrapper", + "type": "union", + }, + }, + { + "name": "findHeaderInfo", + "parameters": [], + "returnType": { + "name": "null | ElementWrapper", + "type": "union", + }, + }, { "name": "findOpenButton", "parameters": [], @@ -138429,6 +138468,33 @@ Component's wrapper class", "typeArguments": [], }, }, + { + "name": "findHeaderActions", + "parameters": [], + "returnType": { + "name": "ElementWrapper", + "type": "reference", + "typeArguments": [], + }, + }, + { + "name": "findHeaderDescription", + "parameters": [], + "returnType": { + "name": "ElementWrapper", + "type": "reference", + "typeArguments": [], + }, + }, + { + "name": "findHeaderInfo", + "parameters": [], + "returnType": { + "name": "ElementWrapper", + "type": "reference", + "typeArguments": [], + }, + }, { "name": "findOpenButton", "parameters": [], diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 23d212ddbe..7205147d8a 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -564,6 +564,9 @@ exports[`test-utils selectors 1`] = ` ], "split-panel": [ "awsui_close-button_rjqu5", + "awsui_header-actions_rjqu5", + "awsui_header-description_rjqu5", + "awsui_header-info_rjqu5", "awsui_header-text_rjqu5", "awsui_open-button_rjqu5", "awsui_open-position-bottom_rjqu5", diff --git a/src/app-layout/__integ__/app-layout-split-panel.test.ts b/src/app-layout/__integ__/app-layout-split-panel.test.ts index 16fc324ee5..21cf05bf4c 100644 --- a/src/app-layout/__integ__/app-layout-split-panel.test.ts +++ b/src/app-layout/__integ__/app-layout-split-panel.test.ts @@ -140,7 +140,7 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as const)('%s', theme => ); test( - 'switches to bottom position when when tools panel opens and available space is too small', + 'switches to bottom position when tools panel opens and available space is too small', setupTest(async page => { await page.setWindowSize({ ...viewports.desktop, width: 1100 }); await page.openPanel(); diff --git a/src/split-panel/__integ__/header.test.ts b/src/split-panel/__integ__/header.test.ts new file mode 100644 index 0000000000..180c62e467 --- /dev/null +++ b/src/split-panel/__integ__/header.test.ts @@ -0,0 +1,63 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; +import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; + +import createWrapper from '../../../lib/components/test-utils/selectors'; +import { viewports } from '../../app-layout/__integ__/constants'; +import { getUrlParams } from '../../app-layout/__integ__/utils'; + +const wrapper = createWrapper().findAppLayout(); +const url = '#/light/app-layout/split-panel-with-custom-header'; + +describe.each(['refresh', 'refresh-toolbar'] as const)('%s', theme => { + describe.each(['desktop', 'mobile'] as const)('%s', viewport => { + function setupTest(testFn: (page: BasePageObject) => Promise, params = {}) { + return useBrowser(async browser => { + const page = new BasePageObject(browser); + await page.setWindowSize(viewports[viewport]); + await browser.url(`${url}?${getUrlParams(theme, params)}`); + await page.waitForVisible(wrapper.findContentRegion().toSelector()); + await testFn(page); + }); + } + + describe('only the expand button is clickable in collapsed bottom split panel when certain header slots are populated', () => { + const cases = [ + { slotName: 'headerActions', devPageParam: 'renderActions' }, + { slotName: 'headerInfo', devPageParam: 'renderInfoLink' }, + ]; + test.each(cases)('$slotName', ({ devPageParam }) => + setupTest( + async page => { + await page.click(wrapper.findSplitPanel().findHeader().toSelector()); // Click on the header text + await expect( + page.isDisplayedInViewport(wrapper.findSplitPanel().findOpenPanelBottom().toSelector()) + ).resolves.toBe(false); + await page.click(wrapper.findSplitPanelOpenButton().toSelector()); + await expect( + page.isDisplayedInViewport(wrapper.findSplitPanel().findOpenPanelBottom().toSelector()) + ).resolves.toBe(true); + }, + { [devPageParam]: true } + )() + ); + }); + + test( + 'the entire header is clickable in collapsed bottom split panel even if headerDescription slot is populated', + setupTest( + async page => { + await page.click(wrapper.findSplitPanel().findHeader().toSelector()); // Click on the header text + await expect( + page.isDisplayedInViewport(wrapper.findSplitPanel().findOpenPanelBottom().toSelector()) + ).resolves.toBe(true); + await expect( + page.isDisplayedInViewport(wrapper.findSplitPanel().findOpenPanelBottom().toSelector()) + ).resolves.toBe(true); + }, + { headerDescription: true } + ) + ); + }); +}); diff --git a/src/split-panel/__tests__/common.tsx b/src/split-panel/__tests__/common.tsx new file mode 100644 index 0000000000..0960d023d8 --- /dev/null +++ b/src/split-panel/__tests__/common.tsx @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import TestI18nProvider from '../../../lib/components/i18n/testing'; +import { + SplitPanelContextProps, + SplitPanelContextProvider, +} from '../../../lib/components/internal/context/split-panel-context'; +import SplitPanel, { SplitPanelProps } from '../../../lib/components/split-panel'; +import createWrapper from '../../../lib/components/test-utils/dom'; +import { defaultSplitPanelContextProps } from './helpers'; + +const i18nStrings = { + closeButtonAriaLabel: 'closeButtonAriaLabel', + openButtonAriaLabel: 'openButtonAriaLabel', + preferencesTitle: 'preferencesTitle', + preferencesPositionLabel: 'preferencesPositionLabel', + preferencesPositionDescription: 'preferencesPositionDescription', + preferencesPositionSide: 'preferencesPositionSide', + preferencesPositionBottom: 'preferencesPositionBottom', + preferencesConfirm: 'preferencesConfirm', + preferencesCancel: 'preferencesCancel', + resizeHandleAriaLabel: 'resizeHandleAriaLabel', +}; + +export const defaultProps: SplitPanelProps = { + header: 'Split panel header', + children:

Split panel content

, + hidePreferencesButton: undefined, + i18nStrings, +}; + +export function renderSplitPanel({ + props, + contextProps, + messages = {}, + modalMessages = {}, +}: { + props?: Partial; + contextProps?: Partial; + messages?: Record; + modalMessages?: Record; +} = {}) { + const { container } = render( + + + + + + ); + const wrapper = createWrapper(container).findSplitPanel(); + return { wrapper }; +} diff --git a/src/split-panel/__tests__/header.test.tsx b/src/split-panel/__tests__/header.test.tsx new file mode 100644 index 0000000000..9f8eeee17c --- /dev/null +++ b/src/split-panel/__tests__/header.test.tsx @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { describeEachAppLayout } from '../../app-layout/__tests__/utils'; +import { renderSplitPanel } from './common'; + +describe('Split panel: Header slots', () => { + describeEachAppLayout({ sizes: ['desktop', 'mobile'] }, () => { + test('renders headerActions', () => { + const { wrapper } = renderSplitPanel({ + props: { headerActions: }, + }); + expect(wrapper!.findHeaderActions()).not.toBeNull(); + expect(wrapper!.findHeaderActions()!.getElement()).toHaveTextContent('Action'); + }); + + test('renders headerDescription', () => { + const { wrapper } = renderSplitPanel({ + props: { headerDescription: 'Header description' }, + }); + expect(wrapper!.findHeaderDescription()).not.toBeNull(); + expect(wrapper!.findHeaderDescription()!.getElement()).toHaveTextContent('Header description'); + }); + + test('renders headerInfo', () => { + const { wrapper } = renderSplitPanel({ + props: { headerInfo: Info }, + }); + expect(wrapper!.findHeaderInfo()).not.toBeNull(); + expect(wrapper!.findHeaderInfo()!.getElement()).toHaveTextContent('Info'); + }); + + test('does not render header properties when not provided', () => { + const { wrapper } = renderSplitPanel(); + expect(wrapper!.findHeaderActions()).toBeNull(); + expect(wrapper!.findHeaderDescription()).toBeNull(); + expect(wrapper!.findHeaderInfo()).toBeNull(); + }); + }); +}); diff --git a/src/split-panel/__tests__/split-panel.test.tsx b/src/split-panel/__tests__/split-panel.test.tsx index e72e5bf85e..a71666c985 100644 --- a/src/split-panel/__tests__/split-panel.test.tsx +++ b/src/split-panel/__tests__/split-panel.test.tsx @@ -5,15 +5,11 @@ import { fireEvent, render } from '@testing-library/react'; import { KeyCode } from '@cloudscape-design/test-utils-core/utils'; -import TestI18nProvider from '../../../lib/components/i18n/testing'; -import { - SplitPanelContextProps, - SplitPanelContextProvider, -} from '../../../lib/components/internal/context/split-panel-context'; -import SplitPanel, { SplitPanelProps } from '../../../lib/components/split-panel'; +import SplitPanel from '../../../lib/components/split-panel'; import createWrapper from '../../../lib/components/test-utils/dom'; import { testIf } from '../../__tests__/utils'; import { describeEachAppLayout } from '../../app-layout/__tests__/utils'; +import { defaultProps, renderSplitPanel } from './common'; import { defaultSplitPanelContextProps } from './helpers'; import styles from '../../../lib/components/split-panel/styles.css.js'; @@ -29,48 +25,6 @@ jest.mock('../../../lib/components/app-layout/utils/use-pointer-events', () => ( usePointerEvents: () => onSliderPointerDown, })); -const i18nStrings = { - closeButtonAriaLabel: 'closeButtonAriaLabel', - openButtonAriaLabel: 'openButtonAriaLabel', - preferencesTitle: 'preferencesTitle', - preferencesPositionLabel: 'preferencesPositionLabel', - preferencesPositionDescription: 'preferencesPositionDescription', - preferencesPositionSide: 'preferencesPositionSide', - preferencesPositionBottom: 'preferencesPositionBottom', - preferencesConfirm: 'preferencesConfirm', - preferencesCancel: 'preferencesCancel', - resizeHandleAriaLabel: 'resizeHandleAriaLabel', -}; - -const defaultProps: SplitPanelProps = { - header: 'Split panel header', - children:

Split panel content

, - hidePreferencesButton: undefined, - i18nStrings, -}; - -function renderSplitPanel({ - props, - contextProps, - messages = {}, - modalMessages = {}, -}: { - props?: Partial; - contextProps?: Partial; - messages?: Record; - modalMessages?: Record; -} = {}) { - const { container } = render( - - - - - - ); - const wrapper = createWrapper(container).findSplitPanel(); - return { wrapper }; -} - afterEach(() => { jest.clearAllMocks(); }); diff --git a/src/split-panel/bottom.tsx b/src/split-panel/bottom.tsx index 62346c2cfa..9291155ecc 100644 --- a/src/split-panel/bottom.tsx +++ b/src/split-panel/bottom.tsx @@ -19,6 +19,7 @@ import testUtilStyles from './test-classes/styles.css.js'; interface SplitPanelContentBottomProps extends SplitPanelContentProps { appLayoutMaxWidth: React.CSSProperties | undefined; closeBehavior: SplitPanelProps['closeBehavior']; + hasCustomElements?: boolean; } export function SplitPanelContentBottom({ @@ -33,6 +34,7 @@ export function SplitPanelContentBottom({ appLayoutMaxWidth, panelHeaderId, onToggle, + hasCustomElements, }: SplitPanelContentBottomProps) { const isRefresh = useVisualRefresh(); const isToolbar = useAppLayoutToolbarDesignEnabled(); @@ -74,13 +76,14 @@ export function SplitPanelContentBottom({ [sharedStyles['with-motion-vertical']]: !animationDisabled, [testUtilStyles['open-position-bottom']]: isOpen, [styles['drawer-closed']]: !isOpen, + [styles['drawer-clickable']]: !hasCustomElements, [styles['drawer-mobile']]: isMobile, [styles['drawer-disable-content-paddings']]: disableContentPaddings, [styles.refresh]: isRefresh, [styles['with-toolbar']]: isToolbar, [styles.hidden]: closeBehavior === 'hide' && !isOpen, })} - onClick={() => !isOpen && onToggle()} + onClick={() => !isOpen && !hasCustomElements && onToggle()} style={{ insetBlockEnd: bottomOffset, insetInlineStart: leftOffset, diff --git a/src/split-panel/implementation.tsx b/src/split-panel/implementation.tsx index 5723f35afc..2b50d2fa43 100644 --- a/src/split-panel/implementation.tsx +++ b/src/split-panel/implementation.tsx @@ -36,6 +36,9 @@ export function SplitPanelImplementation({ hidePreferencesButton, closeBehavior, i18nStrings = {}, + headerActions, + headerDescription, + headerInfo, ...restProps }: SplitPanelImplementationProps) { const isRefresh = useVisualRefresh(); @@ -90,51 +93,83 @@ export function SplitPanelImplementation({ const panelHeaderId = useUniqueId('split-panel-header'); + const hasCustomElements = !!headerActions || !!headerInfo; + + const showDescription = headerDescription && isOpen; + const wrappedHeader = (
-

- {header} -

-
- {!hidePreferencesButton && isOpen && ( - <> +
+
+
+

+ {header} +

+ {headerInfo && ( + {headerInfo} + )} +
+ {headerActions && ( +
{headerActions}
+ )} +
+
+ {!hidePreferencesButton && isOpen && ( + <> + setPreferencesOpen(true)} + formAction="none" + ariaLabel={i18nStrings.preferencesTitle} + ref={refs.preferences} + /> + + + )} + + {isOpen ? ( setPreferencesOpen(true)} + onClick={onToggle} formAction="none" - ariaLabel={i18nStrings.preferencesTitle} - ref={refs.preferences} + ariaLabel={i18nStrings.closeButtonAriaLabel} + ariaExpanded={isOpen} /> - - - )} - - {isOpen ? ( - - ) : position === 'side' || closeBehavior === 'hide' ? null : ( - - )} + ) : position === 'side' || closeBehavior === 'hide' ? null : ( + + )} +
+ + {showDescription && ( +

{headerDescription}

+ )}
); @@ -230,6 +265,7 @@ export function SplitPanelImplementation({ panelHeaderId={panelHeaderId} appLayoutMaxWidth={appLayoutMaxWidth} closeBehavior={closeBehavior} + hasCustomElements={hasCustomElements} > {children} diff --git a/src/split-panel/index.tsx b/src/split-panel/index.tsx index 06ba2088f6..377030aafd 100644 --- a/src/split-panel/index.tsx +++ b/src/split-panel/index.tsx @@ -19,6 +19,11 @@ export default function SplitPanel({ }: SplitPanelProps) { const { __internalRootRef } = useBaseComponent('SplitPanel', { props: { closeBehavior, hidePreferencesButton }, + metadata: { + hasHeaderActions: Boolean(restProps.headerActions), + hasHeaderDescription: Boolean(restProps.headerDescription), + hasHeaderInfo: Boolean(restProps.headerInfo), + }, }); const i18n = useInternalI18n('split-panel'); const i18nModal = useInternalI18n('modal'); diff --git a/src/split-panel/interfaces.ts b/src/split-panel/interfaces.ts index 97a6b2a624..ac7c9cebc5 100644 --- a/src/split-panel/interfaces.ts +++ b/src/split-panel/interfaces.ts @@ -33,6 +33,21 @@ export interface SplitPanelProps extends BaseComponentProps { * @i18n */ i18nStrings?: SplitPanelProps.I18nStrings; + + /** + * Actions for the header. Available only if you specify the `header` property. + */ + headerActions?: React.ReactNode; + + /** + * Supplementary text below the heading. + */ + headerDescription?: React.ReactNode; + + /** + * The area next to the heading, used to display an Info link. + */ + headerInfo?: React.ReactNode; } export namespace SplitPanelProps { diff --git a/src/split-panel/styles.scss b/src/split-panel/styles.scss index 1fd95cb935..148466557d 100644 --- a/src/split-panel/styles.scss +++ b/src/split-panel/styles.scss @@ -71,8 +71,9 @@ $app-layout-drawer-width: calc(#{awsui.$space-layout-toggle-diameter} + 2 * #{aw &.drawer-closed { overflow: hidden; } - &.drawer-closed:hover { + &.drawer-closed.drawer-clickable:hover { background: awsui.$color-background-layout-panel-hover; + cursor: pointer; } & > .drawer-content-bottom > [aria-hidden='true'] { display: none; @@ -227,11 +228,6 @@ $app-layout-drawer-width: calc(#{awsui.$space-layout-toggle-diameter} + 2 * #{aw } .header { - display: flex; - flex: auto; - flex-direction: row; - align-items: flex-start; - justify-content: space-between; inline-size: 100%; margin-block: awsui.$size-vertical-panel-icon-offset; margin-inline: 0; @@ -239,17 +235,60 @@ $app-layout-drawer-width: calc(#{awsui.$space-layout-toggle-diameter} + 2 * #{aw margin-block: constants.$toolbar-vertical-panel-icon-offset; } + &-main-row, + &-main-content { + display: flex; + } + + &-main-row { + align-items: flex-start; + } + + &-main-content { + flex: auto; + flex-direction: row; + flex-wrap: wrap; + column-gap: awsui.$space-scaled-xs; + row-gap: awsui.$space-scaled-xxs; + align-items: center; + justify-content: space-between; + } + + &-text-and-info { + $vertical-margin: calc(#{awsui.$space-scaled-xxs} + 1px); + margin-block-start: $vertical-margin; + min-block-size: calc(#{awsui.$font-panel-header-line-height} + #{$vertical-margin}); + // The line height of the header text might not be respected in non-high pixel density screens + // unless it is set to a smaller value in its parent container. + line-height: awsui.$line-height-body-s; + } + + &-text, + &-info { + display: inline; + } + &-text { @include styles.font-panel-header; + flex-grow: 1; padding-block: 0; padding-inline: 0; - margin-block: 0; margin-inline: 0; - margin-block-start: calc(#{awsui.$space-scaled-xxs} + 1px); + margin-block: 0; + &.with-info { + margin-inline-end: awsui.$space-scaled-xs; + } + } + + &-description { + color: awsui.$color-text-heading-secondary; + @include styles.font-body-m; + margin-block-start: awsui.$space-scaled-xxxs; + margin-block-end: 0; } } -.header-actions { +.header-buttons { display: flex; flex-direction: row; justify-content: space-between; diff --git a/src/split-panel/test-classes/styles.scss b/src/split-panel/test-classes/styles.scss index d977bcc68e..f2fd3e5335 100644 --- a/src/split-panel/test-classes/styles.scss +++ b/src/split-panel/test-classes/styles.scss @@ -4,6 +4,9 @@ */ .root, +.header-actions, +.header-description, +.header-info, .header-text, .open-button, .close-button, diff --git a/src/test-utils/dom/split-panel/index.ts b/src/test-utils/dom/split-panel/index.ts index b7a329f368..6a0399c3c5 100644 --- a/src/test-utils/dom/split-panel/index.ts +++ b/src/test-utils/dom/split-panel/index.ts @@ -30,6 +30,18 @@ export default class SplitPanelWrapper extends ComponentWrapper { return this.findByClassName(testUtilStyles.slider); } + findHeaderActions() { + return this.findByClassName(testUtilStyles['header-actions']); + } + + findHeaderDescription() { + return this.findByClassName(testUtilStyles['header-description']); + } + + findHeaderInfo() { + return this.findByClassName(testUtilStyles['header-info']); + } + /** * Returns the same panel if it's currently open in bottom position. If not, it returns null. * Use this method to assert the panel position.