From d1f65846a8d200f79de1a533874a5ad3c6718bf2 Mon Sep 17 00:00:00 2001 From: fralongo <88311273+fralongo@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:01:31 +0100 Subject: [PATCH] feat: Introduce flag to remove high-contrast header (#1883) Co-authored-by: Francesco Longo --- pages/app/app-context.tsx | 2 + pages/app/index.html | 2 +- pages/app/index.tsx | 14 +++- src/app-layout/visual-refresh/background.tsx | 3 +- src/app-layout/visual-refresh/breadcrumbs.tsx | 3 +- src/app-layout/visual-refresh/header.tsx | 3 +- .../visual-refresh/mobile-toolbar.scss | 2 +- .../visual-refresh/mobile-toolbar.tsx | 3 +- .../visual-refresh/notifications.tsx | 3 +- src/container/internal.tsx | 3 +- src/content-layout/internal.tsx | 5 +- src/internal/components/dark-ribbon/index.tsx | 3 +- .../utils/__tests__/global-flags.test.ts | 78 +++++++++++++++++++ src/internal/utils/content-header-utils.ts | 7 ++ src/internal/utils/global-flags.ts | 35 +++++++++ src/split-panel/icons/bottom-icon-refresh.tsx | 5 +- .../icons/side-position-refresh.tsx | 3 +- src/table/internal.tsx | 3 +- src/wizard/internal.tsx | 3 +- src/wizard/wizard-form-header.tsx | 3 +- 20 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 src/internal/utils/__tests__/global-flags.test.ts create mode 100644 src/internal/utils/content-header-utils.ts create mode 100644 src/internal/utils/global-flags.ts diff --git a/pages/app/app-context.tsx b/pages/app/app-context.tsx index 252b101fea..901f7f2f71 100644 --- a/pages/app/app-context.tsx +++ b/pages/app/app-context.tsx @@ -11,6 +11,7 @@ interface AppUrlParams { direction: 'ltr' | 'rtl'; visualRefresh: boolean; motionDisabled: boolean; + removeHighContrastHeader: boolean; } export interface AppContextType { @@ -29,6 +30,7 @@ const appContextDefaults: AppContextType = { direction: 'ltr', visualRefresh: THEME === 'default', motionDisabled: false, + removeHighContrastHeader: false, }, setMode: () => {}, setUrlParams: () => {}, diff --git a/pages/app/index.html b/pages/app/index.html index 422e91d178..7134ea8c30 100644 --- a/pages/app/index.html +++ b/pages/app/index.html @@ -27,7 +27,7 @@ if (location.hash.includes('file-upload')) { csp['img-src'] = 'blob:'; } - + const cspString = Object.entries(csp) .map(([key, value]) => `${key} ${value}`) .join('; '); diff --git a/pages/app/index.tsx b/pages/app/index.tsx index 2079a6af0e..065691d558 100644 --- a/pages/app/index.tsx +++ b/pages/app/index.tsx @@ -17,9 +17,15 @@ import Header from './components/header'; import StrictModeWrapper from './components/strict-mode-wrapper'; import AppContext, { AppContextProvider, parseQuery } from './app-context'; +interface GlobalFlags { + removeHighContrastHeader?: boolean; +} const awsuiVisualRefreshFlag = Symbol.for('awsui-visual-refresh-flag'); +const awsuiGlobalFlagsSymbol = Symbol.for('awsui-global-flags'); + interface ExtendedWindow extends Window { [awsuiVisualRefreshFlag]?: () => boolean; + [awsuiGlobalFlagsSymbol]?: GlobalFlags; } declare const window: ExtendedWindow; @@ -77,10 +83,16 @@ function App() { } const history = createHashHistory(); -const { direction, visualRefresh } = parseQuery(history.location.search); +const { direction, visualRefresh, removeHighContrastHeader } = parseQuery(history.location.search); // The VR class needs to be set before any React rendering occurs. window[awsuiVisualRefreshFlag] = () => visualRefresh; +if (!window[awsuiGlobalFlagsSymbol]) { + window[awsuiGlobalFlagsSymbol] = {}; +} +if (removeHighContrastHeader) { + window[awsuiGlobalFlagsSymbol].removeHighContrastHeader = true; +} // Apply the direction value to the HTML element dir attribute document.documentElement.setAttribute('dir', direction); diff --git a/src/app-layout/visual-refresh/background.tsx b/src/app-layout/visual-refresh/background.tsx index 256d9f203f..ef3d6d1f7e 100644 --- a/src/app-layout/visual-refresh/background.tsx +++ b/src/app-layout/visual-refresh/background.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; import clsx from 'clsx'; +import { contentHeaderClassName } from '../../internal/utils/content-header-utils'; import { useAppLayoutInternals } from './context'; import styles from './styles.css.js'; @@ -20,7 +21,7 @@ export default function Background() { } return ( -
+
{!isMobile && hasStickyBackground && ( diff --git a/src/app-layout/visual-refresh/breadcrumbs.tsx b/src/app-layout/visual-refresh/breadcrumbs.tsx index 08d042eabf..99eb2465ee 100644 --- a/src/app-layout/visual-refresh/breadcrumbs.tsx +++ b/src/app-layout/visual-refresh/breadcrumbs.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; import clsx from 'clsx'; +import { contentHeaderClassName } from '../../internal/utils/content-header-utils'; import { useAppLayoutInternals } from './context'; import styles from './styles.css.js'; import testutilStyles from '../test-classes/styles.css.js'; @@ -21,7 +22,7 @@ export default function Breadcrumbs() { { [styles['has-sticky-background']]: hasStickyBackground, }, - 'awsui-context-content-header' + contentHeaderClassName )} > {breadcrumbs} diff --git a/src/app-layout/visual-refresh/header.tsx b/src/app-layout/visual-refresh/header.tsx index 135cd3e646..447dd743f8 100644 --- a/src/app-layout/visual-refresh/header.tsx +++ b/src/app-layout/visual-refresh/header.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; import clsx from 'clsx'; +import { contentHeaderClassName } from '../../internal/utils/content-header-utils'; import { useAppLayoutInternals } from './context'; import styles from './styles.css.js'; @@ -21,7 +22,7 @@ export default function Header() { [styles['has-notifications-content']]: hasNotificationsContent, [styles.unfocusable]: hasDrawerViewportOverlay, }, - 'awsui-context-content-header' + contentHeaderClassName )} > {contentHeader} diff --git a/src/app-layout/visual-refresh/mobile-toolbar.scss b/src/app-layout/visual-refresh/mobile-toolbar.scss index e1c720a449..f7c868551f 100644 --- a/src/app-layout/visual-refresh/mobile-toolbar.scss +++ b/src/app-layout/visual-refresh/mobile-toolbar.scss @@ -9,7 +9,7 @@ section.mobile-toolbar { align-items: center; - background-color: awsui.$color-background-home-header; + background-color: awsui.$color-background-layout-main; border-bottom: 1px solid awsui.$color-border-divider-default; box-shadow: awsui.$shadow-panel-toggle; box-sizing: border-box; diff --git a/src/app-layout/visual-refresh/mobile-toolbar.tsx b/src/app-layout/visual-refresh/mobile-toolbar.tsx index 9c7a6fc690..73e5b195de 100644 --- a/src/app-layout/visual-refresh/mobile-toolbar.tsx +++ b/src/app-layout/visual-refresh/mobile-toolbar.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; import clsx from 'clsx'; +import { contentHeaderClassName } from '../../internal/utils/content-header-utils'; import { InternalButton } from '../../button/internal'; import { MobileTriggers as DrawersMobileTriggers } from './drawers'; import { useAppLayoutInternals } from './context'; @@ -44,7 +45,7 @@ export default function MobileToolbar() { [styles.unfocusable]: hasDrawerViewportOverlay, }, testutilStyles['mobile-bar'], - 'awsui-context-content-header' + contentHeaderClassName )} > {!navigationHide && ( diff --git a/src/app-layout/visual-refresh/notifications.tsx b/src/app-layout/visual-refresh/notifications.tsx index a4758536e5..f70ed9a002 100644 --- a/src/app-layout/visual-refresh/notifications.tsx +++ b/src/app-layout/visual-refresh/notifications.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; import clsx from 'clsx'; +import { contentHeaderClassName } from '../../internal/utils/content-header-utils'; import { useAppLayoutInternals } from './context'; import styles from './styles.css.js'; import testutilStyles from '../test-classes/styles.css.js'; @@ -29,7 +30,7 @@ export default function Notifications() { [styles.unfocusable]: hasDrawerViewportOverlay, }, testutilStyles.notifications, - 'awsui-context-content-header' + contentHeaderClassName )} >
{notifications}
diff --git a/src/container/internal.tsx b/src/container/internal.tsx index 70ed2bed14..a73c6778d7 100644 --- a/src/container/internal.tsx +++ b/src/container/internal.tsx @@ -6,6 +6,7 @@ import { ContainerProps } from './interfaces'; import { getBaseProps } from '../internal/base-component'; import { useAppLayoutContext } from '../internal/context/app-layout-context'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; +import { contentHeaderClassName } from '../internal/utils/content-header-utils'; import { StickyHeaderContext, useStickyHeader } from './use-sticky-header'; import { useDynamicOverlap } from '../internal/hooks/use-dynamic-overlap'; import { useMergeRefs } from '../internal/hooks/use-merge-refs'; @@ -165,7 +166,7 @@ export default function InternalContainer({ ref={headerMergedRef} > {__darkHeader ? ( -
{header}
+
{header}
) : ( header )} diff --git a/src/content-layout/internal.tsx b/src/content-layout/internal.tsx index 3f719a11f4..48990234d1 100644 --- a/src/content-layout/internal.tsx +++ b/src/content-layout/internal.tsx @@ -4,6 +4,7 @@ import React, { useRef } from 'react'; import clsx from 'clsx'; import { ContentLayoutProps } from './interfaces'; import { getBaseProps } from '../internal/base-component'; +import { contentHeaderClassName } from '../internal/utils/content-header-utils'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import { useDynamicOverlap } from '../internal/hooks/use-dynamic-overlap'; import { useMergeRefs } from '../internal/hooks/use-merge-refs'; @@ -42,12 +43,12 @@ export default function InternalContentLayout({ className={clsx( styles.background, { [styles['is-overlap-disabled']]: isOverlapDisabled }, - 'awsui-context-content-header' + contentHeaderClassName )} ref={overlapElement} /> - {header &&
{header}
} + {header &&
{header}
}
{children}
diff --git a/src/internal/components/dark-ribbon/index.tsx b/src/internal/components/dark-ribbon/index.tsx index d325ebccce..7ea8092a61 100644 --- a/src/internal/components/dark-ribbon/index.tsx +++ b/src/internal/components/dark-ribbon/index.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useEffect, useRef } from 'react'; import { useMutationObserver } from '../../hooks/use-mutation-observer'; +import { contentHeaderClassName } from '../../utils/content-header-utils'; import styles from './styles.css.js'; import { useStableCallback } from '@cloudscape-design/component-toolkit/internal'; @@ -54,7 +55,7 @@ export default function DarkRibbon({ children, isRefresh, hasPlainStyling }: Dar } return ( -
+
{children}
diff --git a/src/internal/utils/__tests__/global-flags.test.ts b/src/internal/utils/__tests__/global-flags.test.ts new file mode 100644 index 0000000000..8f9c8c23e1 --- /dev/null +++ b/src/internal/utils/__tests__/global-flags.test.ts @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as globalFlags from '../global-flags'; +const { getGlobalFlag } = globalFlags; + +const awsuiGlobalFlagsSymbol = Symbol.for('awsui-global-flags'); +interface GlobalFlags { + removeHighContrastHeader?: boolean; +} + +interface ExtendedWindow extends Window { + [awsuiGlobalFlagsSymbol]?: GlobalFlags; +} +declare const window: ExtendedWindow; + +afterEach(() => { + delete window[awsuiGlobalFlagsSymbol]; + jest.restoreAllMocks(); +}); + +describe('getGlobalFlag', () => { + test('returns undefined if there global flags are not defined', () => { + expect(getGlobalFlag('removeHighContrastHeader')).toBeUndefined(); + }); + test('returns undefined if global flags are defined but flag is not set', () => { + window[awsuiGlobalFlagsSymbol] = {}; + expect(getGlobalFlag('removeHighContrastHeader')).toBeUndefined(); + }); + test('returns removeHighContrastHeader value', () => { + window[awsuiGlobalFlagsSymbol] = { removeHighContrastHeader: false }; + expect(getGlobalFlag('removeHighContrastHeader')).toBe(false); + window[awsuiGlobalFlagsSymbol].removeHighContrastHeader = true; + expect(getGlobalFlag('removeHighContrastHeader')).toBe(true); + }); + test('returns removeHighContrastHeader value when defined in top window', () => { + jest + .spyOn(globalFlags, 'getTopWindow') + .mockReturnValue({ [awsuiGlobalFlagsSymbol]: { removeHighContrastHeader: true } } as unknown as ExtendedWindow); + expect(getGlobalFlag('removeHighContrastHeader')).toBe(true); + jest.restoreAllMocks(); + + jest + .spyOn(globalFlags, 'getTopWindow') + .mockReturnValue({ [awsuiGlobalFlagsSymbol]: { removeHighContrastHeader: false } } as unknown as ExtendedWindow); + expect(getGlobalFlag('removeHighContrastHeader')).toBe(false); + }); + test('privileges values in the self window', () => { + jest + .spyOn(globalFlags, 'getTopWindow') + .mockReturnValue({ [awsuiGlobalFlagsSymbol]: { removeHighContrastHeader: false } } as unknown as ExtendedWindow); + window[awsuiGlobalFlagsSymbol] = { removeHighContrastHeader: true }; + expect(getGlobalFlag('removeHighContrastHeader')).toBe(true); + }); + test('returns top window value when not defined in the self window', () => { + jest + .spyOn(globalFlags, 'getTopWindow') + .mockReturnValue({ [awsuiGlobalFlagsSymbol]: { removeHighContrastHeader: true } } as unknown as ExtendedWindow); + window[awsuiGlobalFlagsSymbol] = {}; + expect(getGlobalFlag('removeHighContrastHeader')).toBe(true); + }); + test('returns undefined when top window is undefined', () => { + jest.spyOn(globalFlags, 'getTopWindow').mockReturnValue(undefined); + expect(getGlobalFlag('removeHighContrastHeader')).toBeUndefined(); + }); + test('returns undefined when an error is thrown and flag is not defined in own window', () => { + jest.spyOn(globalFlags, 'getTopWindow').mockImplementation(() => { + throw new Error('whatever'); + }); + expect(getGlobalFlag('removeHighContrastHeader')).toBeUndefined(); + }); + test('returns value when an error is thrown and flag is defined in own window', () => { + jest.spyOn(globalFlags, 'getTopWindow').mockImplementation(() => { + throw new Error('whatever'); + }); + window[awsuiGlobalFlagsSymbol] = { removeHighContrastHeader: true }; + expect(getGlobalFlag('removeHighContrastHeader')).toBe(true); + }); +}); diff --git a/src/internal/utils/content-header-utils.ts b/src/internal/utils/content-header-utils.ts new file mode 100644 index 0000000000..e8dc0a1774 --- /dev/null +++ b/src/internal/utils/content-header-utils.ts @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { getGlobalFlag } from './global-flags'; + +export const contentHeaderClassName: string = getGlobalFlag('removeHighContrastHeader') + ? '' + : 'awsui-context-content-header'; diff --git a/src/internal/utils/global-flags.ts b/src/internal/utils/global-flags.ts new file mode 100644 index 0000000000..6da2ae5351 --- /dev/null +++ b/src/internal/utils/global-flags.ts @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +export const awsuiGlobalFlagsSymbol = Symbol.for('awsui-global-flags'); + +interface GlobalFlags { + removeHighContrastHeader?: boolean; +} + +interface ExtendedWindow extends Window { + [awsuiGlobalFlagsSymbol]?: GlobalFlags; +} +declare const window: ExtendedWindow; + +export const getTopWindow = (): ExtendedWindow | undefined => { + return window.top as ExtendedWindow; +}; + +function readFlag(window: ExtendedWindow | undefined, flagName: keyof GlobalFlags) { + if (typeof window === 'undefined' || !window[awsuiGlobalFlagsSymbol]) { + return undefined; + } + return window[awsuiGlobalFlagsSymbol][flagName]; +} + +export const getGlobalFlag = (flagName: keyof GlobalFlags): GlobalFlags[keyof GlobalFlags] | undefined => { + try { + const ownFlag = readFlag(window, flagName); + if (ownFlag !== undefined) { + return ownFlag; + } + return readFlag(getTopWindow(), flagName); + } catch (e) { + return undefined; + } +}; diff --git a/src/split-panel/icons/bottom-icon-refresh.tsx b/src/split-panel/icons/bottom-icon-refresh.tsx index efa7c551e2..abefca7930 100644 --- a/src/split-panel/icons/bottom-icon-refresh.tsx +++ b/src/split-panel/icons/bottom-icon-refresh.tsx @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React from 'react'; +import { contentHeaderClassName } from '../../internal/utils/content-header-utils'; import { getClassName, SVGTableRowProps } from './side-position-refresh'; const TableRow = ({ offset, isHeader }: SVGTableRowProps) => { @@ -48,8 +49,8 @@ const bottomPositionIcon = ( - - + + - +
- {isVisualRefresh &&
} + {isVisualRefresh &&
}