Skip to content

Commit

Permalink
Hide alerts during initial render
Browse files Browse the repository at this point in the history
  • Loading branch information
gethinwebster committed Oct 16, 2024
1 parent 31cf7ec commit d726b37
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 28 deletions.
30 changes: 20 additions & 10 deletions pages/alert/runtime-content.page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useContext, useMemo, useState } from 'react';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';

import {
Expand All @@ -20,7 +20,9 @@ import awsuiPlugins from '~components/internal/plugins';
import AppContext, { AppContextType } from '../app/app-context';
import ScreenshotArea from '../utils/screenshot-area';

type PageContext = React.Context<AppContextType<{ loading: boolean; hidden: boolean; type: AlertProps.Type }>>;
type PageContext = React.Context<
AppContextType<{ loading: boolean; hidden: boolean; type: AlertProps.Type; autofocus: boolean }>
>;

awsuiPlugins.alertContent.registerContentReplacer({
id: 'awsui/alert-test-action',
Expand Down Expand Up @@ -61,19 +63,15 @@ awsuiPlugins.alertContent.registerContentReplacer({
};
},
initialCheck(context) {
const found = context.type === 'error' && context.contentText?.match('Access denied');
return {
header: found ? 'remove' : 'original',
content: found ? 'replaced' : 'original',
};
return context.type === 'error' && !!context.contentText?.match('Access denied');
},
});

const alertTypeOptions = ['error', 'warning', 'info', 'success'].map(type => ({ value: type }));

export default function () {
const {
urlParams: { loading = false, hidden = false, type = 'error' },
urlParams: { loading = false, hidden = false, type = 'error', autofocus = false },
setUrlParams,
} = useContext(AppContext as PageContext);
const [unrelatedState, setUnrelatedState] = useState(false);
Expand All @@ -82,6 +80,14 @@ export default function () {
const content1 = useMemo(() => (loading ? <Box>Loading...</Box> : <Box>Content</Box>), [loading]);
const content2 = loading ? <Box>Loading...</Box> : <Box>There was an error: Access denied because of XYZ</Box>;

const alertRef = useRef<AlertProps.Ref>(null);

useEffect(() => {
if (autofocus && !hidden) {
alertRef.current?.focus();
}
}, [autofocus, hidden]);

return (
<Box margin="m">
<h1>Alert runtime actions</h1>
Expand All @@ -99,6 +105,9 @@ export default function () {
<Checkbox onChange={e => setContentSwapped(e.detail.checked)} checked={contentSwapped}>
Swap content
</Checkbox>
<Checkbox onChange={e => setUrlParams({ autofocus: e.detail.checked })} checked={autofocus}>
Auto-focus alert
</Checkbox>
<FormField label="Alert type">
<Select
options={alertTypeOptions}
Expand All @@ -113,22 +122,23 @@ export default function () {
<ScreenshotArea gutters={false}>
{hidden ? null : (
<SpaceBetween size="m">
{/* <Alert
<Alert
type={type}
statusIconAriaLabel={type}
dismissAriaLabel="Dismiss"
header="Header"
action={<Button>Action</Button>}
>
{!contentSwapped ? content1 : content2}
</Alert> */}
</Alert>

<Alert
type={type}
statusIconAriaLabel={type}
dismissAriaLabel="Dismiss"
header="Header"
action={<Button>Action</Button>}
ref={alertRef}
>
{!contentSwapped ? content2 : content1}
</Alert>
Expand Down
7 changes: 6 additions & 1 deletion src/alert/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const InternalAlert = React.forwardRef(

const { discoveredActions, headerRef: headerRefAction, contentRef: contentRefAction } = useDiscoveredAction(type);
const {
initialHidden,
headerReplacementType,
contentReplacementType,
headerRef: headerRefContent,
Expand Down Expand Up @@ -100,7 +101,11 @@ const InternalAlert = React.forwardRef(
{...baseProps}
{...analyticsAttributes}
aria-hidden={!visible}
className={clsx(styles.root, { [styles.hidden]: !visible }, baseProps.className)}
className={clsx(
styles.root,
{ [styles.hidden]: !visible, [styles['initial-hidden']]: initialHidden },
baseProps.className
)}
ref={mergedRef}
>
<LinkDefaultVariantContext.Provider value={{ defaultVariant: 'primary' }}>
Expand Down
8 changes: 8 additions & 0 deletions src/alert/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@
.hidden {
display: none;
}
// visibly hidden, but focusable
.initial-hidden {
// TODO: decide if it should take up space:
opacity: 0;
// or be entirely hidden:
// overflow: hidden;
// height: 0;
}

.header,
.header-replacement {
Expand Down
18 changes: 5 additions & 13 deletions src/internal/plugins/controllers/alert-flash-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,10 @@ export interface AlertFlashContentResult {
unmount: (containers: { replacementHeaderContainer: HTMLElement; replacementContentContainer: HTMLElement }) => void;
}

export interface AlertFlashContentInitialResult {
header: ReplacementType;
content: ReplacementType;
}

export interface AlertFlashContentConfig {
id: string;
runReplacer: (context: AlertFlashContentContext, replacementApi: ReplacementApi) => AlertFlashContentResult;
initialCheck?: (context: AlertFlashContentInitialContext) => AlertFlashContentInitialResult;
initialCheck?: (context: AlertFlashContentInitialContext) => boolean;
}

export type AlertFlashContentRegistrationListener = (provider: AlertFlashContentConfig) => () => void;
Expand All @@ -65,7 +60,7 @@ export interface AlertFlashContentApiPublic {
export interface AlertFlashContentApiInternal {
clearRegisteredReplacer(): void;
onContentRegistered(listener: AlertFlashContentRegistrationListener): () => void;
initialSyncRender(context: AlertFlashContentInitialContextRaw): AlertFlashContentInitialResult;
initialCheck(context: AlertFlashContentInitialContextRaw): boolean;
}

const nodeAsString = (node: ReactNode) =>
Expand Down Expand Up @@ -107,7 +102,7 @@ export class AlertFlashContentController {
this.#provider = undefined;
};

initialSyncRender = (context: AlertFlashContentInitialContextRaw): AlertFlashContentInitialResult => {
initialCheck = (context: AlertFlashContentInitialContextRaw): boolean => {
if (this.#provider?.initialCheck) {
const processedContext: AlertFlashContentInitialContext = {

Check warning on line 107 in src/internal/plugins/controllers/alert-flash-content.ts

View check run for this annotation

Codecov / codecov/patch

src/internal/plugins/controllers/alert-flash-content.ts#L107

Added line #L107 was not covered by tests
type: context.type,
Expand All @@ -116,10 +111,7 @@ export class AlertFlashContentController {
};
return this.#provider.initialCheck(processedContext);

Check warning on line 112 in src/internal/plugins/controllers/alert-flash-content.ts

View check run for this annotation

Codecov / codecov/patch

src/internal/plugins/controllers/alert-flash-content.ts#L112

Added line #L112 was not covered by tests
}
return {
header: 'original',
content: 'original',
};
return false;
};

onContentRegistered = (listener: AlertFlashContentRegistrationListener) => {
Expand All @@ -145,7 +137,7 @@ export class AlertFlashContentController {
installInternal(internalApi: Partial<AlertFlashContentApiInternal> = {}): AlertFlashContentApiInternal {
internalApi.clearRegisteredReplacer ??= this.clearRegisteredReplacer;
internalApi.onContentRegistered ??= this.onContentRegistered;
internalApi.initialSyncRender ??= this.initialSyncRender;
internalApi.initialCheck ??= this.initialCheck;
return internalApi as AlertFlashContentApiInternal;
}
}
11 changes: 7 additions & 4 deletions src/internal/plugins/helpers/use-discovered-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,22 @@ export function createUseDiscoveredContent(componentName: string, controller: Al
const contentRef = useRef<HTMLDivElement>(null);
const replacementHeaderRef = useRef<HTMLDivElement>(null);
const replacementContentRef = useRef<HTMLDivElement>(null);
const [initialState] = useState(() =>
controller.initialSyncRender({
const [initialHidden, setInitialHidden] = useState(() =>
controller.initialCheck({
type,
header,
content: children,
})
);
const [headerReplacementType, setFoundHeaderReplacement] = useState<ReplacementType>(initialState.header);
const [contentReplacementType, setFoundContentReplacement] = useState<ReplacementType>(initialState.content);
const [headerReplacementType, setFoundHeaderReplacement] = useState<ReplacementType>('original');
const [contentReplacementType, setFoundContentReplacement] = useState<ReplacementType>('original');
const mountedProvider = useRef<AlertFlashContentResult | undefined>();

useEffect(() => {
const context = { type, headerRef, contentRef };

setInitialHidden(false);

return controller.onContentRegistered(provider => {
let mounted = true;

Expand Down Expand Up @@ -99,6 +101,7 @@ export function createUseDiscoveredContent(componentName: string, controller: Al
}, [type, header, children]);

return {
initialHidden,
headerReplacementType,
contentReplacementType,
headerRef: headerRef as React.Ref<HTMLDivElement>,
Expand Down

0 comments on commit d726b37

Please sign in to comment.