Skip to content

Commit

Permalink
feat: Introduce flag to remove high-contrast header (#1883)
Browse files Browse the repository at this point in the history
Co-authored-by: Francesco Longo <[email protected]>
  • Loading branch information
fralongo and Francesco Longo authored Jan 18, 2024
1 parent 16dc073 commit d1f6584
Show file tree
Hide file tree
Showing 20 changed files with 165 additions and 18 deletions.
2 changes: 2 additions & 0 deletions pages/app/app-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface AppUrlParams {
direction: 'ltr' | 'rtl';
visualRefresh: boolean;
motionDisabled: boolean;
removeHighContrastHeader: boolean;
}

export interface AppContextType<T = unknown> {
Expand All @@ -29,6 +30,7 @@ const appContextDefaults: AppContextType = {
direction: 'ltr',
visualRefresh: THEME === 'default',
motionDisabled: false,
removeHighContrastHeader: false,
},
setMode: () => {},
setUrlParams: () => {},
Expand Down
2 changes: 1 addition & 1 deletion pages/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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('; ');
Expand Down
14 changes: 13 additions & 1 deletion pages/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/app-layout/visual-refresh/background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -20,7 +21,7 @@ export default function Background() {
}

return (
<div className={clsx(styles.background, 'awsui-context-content-header')}>
<div className={clsx(styles.background, contentHeaderClassName)}>
<div className={styles['scrolling-background']} />

{!isMobile && hasStickyBackground && (
Expand Down
3 changes: 2 additions & 1 deletion src/app-layout/visual-refresh/breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,7 +22,7 @@ export default function Breadcrumbs() {
{
[styles['has-sticky-background']]: hasStickyBackground,
},
'awsui-context-content-header'
contentHeaderClassName
)}
>
{breadcrumbs}
Expand Down
3 changes: 2 additions & 1 deletion src/app-layout/visual-refresh/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -21,7 +22,7 @@ export default function Header() {
[styles['has-notifications-content']]: hasNotificationsContent,
[styles.unfocusable]: hasDrawerViewportOverlay,
},
'awsui-context-content-header'
contentHeaderClassName
)}
>
{contentHeader}
Expand Down
2 changes: 1 addition & 1 deletion src/app-layout/visual-refresh/mobile-toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/app-layout/visual-refresh/mobile-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,7 +45,7 @@ export default function MobileToolbar() {
[styles.unfocusable]: hasDrawerViewportOverlay,
},
testutilStyles['mobile-bar'],
'awsui-context-content-header'
contentHeaderClassName
)}
>
{!navigationHide && (
Expand Down
3 changes: 2 additions & 1 deletion src/app-layout/visual-refresh/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -29,7 +30,7 @@ export default function Notifications() {
[styles.unfocusable]: hasDrawerViewportOverlay,
},
testutilStyles.notifications,
'awsui-context-content-header'
contentHeaderClassName
)}
>
<div ref={notificationsElement}>{notifications}</div>
Expand Down
3 changes: 2 additions & 1 deletion src/container/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -165,7 +166,7 @@ export default function InternalContainer({
ref={headerMergedRef}
>
{__darkHeader ? (
<div className={clsx(styles['dark-header'], 'awsui-context-content-header')}>{header}</div>
<div className={clsx(styles['dark-header'], contentHeaderClassName)}>{header}</div>
) : (
header
)}
Expand Down
5 changes: 3 additions & 2 deletions src/content-layout/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 && <div className={clsx(styles.header, 'awsui-context-content-header')}>{header}</div>}
{header && <div className={clsx(styles.header, contentHeaderClassName)}>{header}</div>}

<div className={styles.content}>{children}</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/internal/components/dark-ribbon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -54,7 +55,7 @@ export default function DarkRibbon({ children, isRefresh, hasPlainStyling }: Dar
}

return (
<div ref={containerRef} className="awsui-context-content-header">
<div ref={containerRef} className={contentHeaderClassName}>
<div ref={fillRef} className={styles['background-fill']} />
<div className={styles.content}>{children}</div>
</div>
Expand Down
78 changes: 78 additions & 0 deletions src/internal/utils/__tests__/global-flags.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
7 changes: 7 additions & 0 deletions src/internal/utils/content-header-utils.ts
Original file line number Diff line number Diff line change
@@ -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';
35 changes: 35 additions & 0 deletions src/internal/utils/global-flags.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
5 changes: 3 additions & 2 deletions src/split-panel/icons/bottom-icon-refresh.tsx
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -48,8 +49,8 @@ const bottomPositionIcon = (
<g className="awsui-context-top-navigation">
<rect x="2" y="2" width="226" height="6" className={getClassName('layout-top')} />
</g>
<g className="awsui-context-content-header">
<path d="M0 8H230V23H0V8Z" className={getClassName('layout-main')} />
<g className={contentHeaderClassName}>
<path d="M2 8H228V23H2V8Z" className={getClassName('layout-main')} />
<g className={getClassName('default')}>
<path
d="M9 15.5C9 16.8807 7.88071 18 6.5 18C5.11929 18 4 16.8807 4 15.5C4 14.1193 5.11929 13 6.5 13C7.88071 13 9 14.1193 9 15.5Z"
Expand Down
3 changes: 2 additions & 1 deletion src/split-panel/icons/side-position-refresh.tsx
Original file line number Diff line number Diff line change
@@ -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 styles from '../styles.css.js';

export interface SVGTableRowProps {
Expand Down Expand Up @@ -54,7 +55,7 @@ const bottomPositionIcon = (
<g className="awsui-context-top-navigation">
<rect x="2" y="2" width="212" height="6" className={getClassName('layout-top')} />
</g>
<g className="awsui-context-content-header">
<g className={contentHeaderClassName}>
<path d="M2 8H214V23H2V8Z" className={getClassName('layout-main')} />
<g className={getClassName('default')}>
<path
Expand Down
3 changes: 2 additions & 1 deletion src/table/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { CollectionLabelContext } from '../internal/context/collection-label-con
import { useFunnelSubStep } from '../internal/analytics/hooks/use-funnel';
import { NoDataCell } from './no-data-cell';
import { usePerformanceMarks } from '../internal/hooks/use-performance-marks';
import { contentHeaderClassName } from '../internal/utils/content-header-utils';

const SELECTION_COLUMN_WIDTH = 54;
const selectionColumnId = Symbol('selection-column-id');
Expand Down Expand Up @@ -293,7 +294,7 @@ const InternalTable = React.forwardRef(
{hasHeader && (
<div
ref={overlapElement}
className={clsx(hasDynamicHeight && [styles['dark-header'], 'awsui-context-content-header'])}
className={clsx(hasDynamicHeight && [styles['dark-header'], contentHeaderClassName])}
>
<div
ref={toolsHeaderWrapper}
Expand Down
Loading

0 comments on commit d1f6584

Please sign in to comment.