Skip to content

Commit

Permalink
feat: State management for runtime drawers
Browse files Browse the repository at this point in the history
  • Loading branch information
georgylobko committed Sep 10, 2024
1 parent 3a8b4bc commit 2d4628e
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 74 deletions.
67 changes: 24 additions & 43 deletions pages/app-layout/utils/external-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,40 +94,11 @@ const Counter: React.FC = ({ children }) => {
);
};

class CounterStateManager {
private isPaused = false;
private pauseCallback: ((isPaused: boolean) => void) | null = null;

private onPauseStateChange() {
if (this.pauseCallback) {
this.pauseCallback(this.isPaused);
}
}

registerPauseCallback(callback: (isPaused: boolean) => void) {
this.pauseCallback = callback;
}

unregisterPauseCallback() {
this.pauseCallback = null;
}

pause() {
this.isPaused = true;
this.onPauseStateChange();
}

resume() {
this.isPaused = false;
this.onPauseStateChange();
}
}

const autoIncrementState = new CounterStateManager();

const AutoIncrementCounter: React.FC = ({ children }) => {
const AutoIncrementCounter: React.FC<{
onVisibilityChange?: (callback: (isVisible: boolean) => void) => () => void;
}> = ({ children, onVisibilityChange }) => {
const [count, setCount] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [isPaused, setIsPaused] = useState(true);

useEffect(() => {
const interval = setInterval(() => {
Expand All @@ -140,12 +111,16 @@ const AutoIncrementCounter: React.FC = ({ children }) => {
}, [isPaused]);

useEffect(() => {
autoIncrementState.registerPauseCallback(isPaused => {
setIsPaused(isPaused);
});

return () => autoIncrementState.unregisterPauseCallback();
}, []);
if (onVisibilityChange) {
const unsubscribe = onVisibilityChange((isVisible: boolean) => {
setIsPaused(!isVisible);
});

return () => {
unsubscribe();
};
}
}, [onVisibilityChange]);

return (
<div>
Expand All @@ -163,8 +138,6 @@ awsuiPlugins.appLayout.registerDrawer({
resizable: true,
defaultSize: 350,
preserveInactiveContent: true,
onShow: () => autoIncrementState.resume(),
onHide: () => autoIncrementState.pause(),

ariaLabels: {
closeButton: 'Close button',
Expand All @@ -184,8 +157,16 @@ awsuiPlugins.appLayout.registerDrawer({
console.log('resize', event.detail);
},

mountContent: container => {
ReactDOM.render(<AutoIncrementCounter>global widget content circle 1</AutoIncrementCounter>, container);
mountContent: (
container: HTMLElement,
onVisibilityChange?: (callback: (isVisible: boolean) => void) => () => void
) => {
ReactDOM.render(
<AutoIncrementCounter onVisibilityChange={onVisibilityChange}>
global widget content circle 1
</AutoIncrementCounter>,
container
);
},
unmountContent: container => unmountComponentAtNode(container),
});
Expand Down
17 changes: 13 additions & 4 deletions src/app-layout/__tests__/runtime-drawers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1065,17 +1065,26 @@ describe('toolbar mode only features', () => {
expect(globalDrawersWrapper.findDrawerById('global-drawer-1')!.isActive()).toBe(true);
});

test('should call onShow and onHide when global drawer with preserveInactiveContent is opened and closed', async () => {
test('should call visibilityChange callback when global drawer with preserveInactiveContent is opened and closed', async () => {
const onShow = jest.fn();
const onHide = jest.fn();
awsuiPlugins.appLayout.registerDrawer({
...drawerDefaults,
id: 'global-drawer-1',
type: 'global',
mountContent: container => (container.textContent = 'global drawer content 1'),
mountContent: (container, onVisibilityChange) => {
if (onVisibilityChange) {
onVisibilityChange((isVisible: boolean) => {
if (isVisible) {
onShow();
} else {
onHide();
}
});
}
container.textContent = 'global drawer content 1';
},
preserveInactiveContent: true,
onShow,
onHide,
});

const { wrapper, globalDrawersWrapper } = await renderComponent(<AppLayout drawers={[testDrawer]} />);
Expand Down
2 changes: 0 additions & 2 deletions src/app-layout/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,6 @@ export namespace AppLayoutProps {
defaultSize?: number;
onResize?: NonCancelableEventHandler<{ size: number }>;
preserveInactiveContent?: boolean;
onShow?: NonCancelableEventHandler;
onHide?: NonCancelableEventHandler;
}

export interface DrawerAriaLabels {
Expand Down
56 changes: 40 additions & 16 deletions src/app-layout/runtime-api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,64 @@
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { fireNonCancelableEvent } from '../internal/events';
import { fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/events';
import { DrawerConfig as RuntimeDrawerConfig } from '../internal/plugins/controllers/drawers';
import { RuntimeContentWrapper } from '../internal/plugins/helpers';
import { sortByPriority } from '../internal/plugins/helpers/utils';
import VisibilityStateManager from '../internal/plugins/helpers/visibility-state-manager';
import { AppLayoutProps } from './interfaces';

export interface DrawersLayout {
before: Array<AppLayoutProps.Drawer>;
after: Array<AppLayoutProps.Drawer>;
}

const visibilityStateManagerMap = new Map<string, VisibilityStateManager>();

export function convertRuntimeDrawers(drawers: Array<RuntimeDrawerConfig>): DrawersLayout {
const converted = drawers.map(
({
mountContent,
unmountContent,
trigger,
...runtimeDrawer
}): AppLayoutProps.Drawer & { orderPriority?: number } => ({
...runtimeDrawer,
ariaLabels: { drawerName: runtimeDrawer.ariaLabels.content ?? '', ...runtimeDrawer.ariaLabels },
trigger: {
iconSvg: (
// eslint-disable-next-line react/no-danger
<span dangerouslySetInnerHTML={{ __html: trigger.iconSvg }} />
}): AppLayoutProps.Drawer & {
orderPriority?: number;
onShow?: NonCancelableEventHandler;
onHide?: NonCancelableEventHandler;
} => {
let visibilityStateManager: VisibilityStateManager;
if (visibilityStateManagerMap.has(runtimeDrawer.id)) {
visibilityStateManager = visibilityStateManagerMap.get(runtimeDrawer.id)!;
} else {
visibilityStateManager = new VisibilityStateManager();
visibilityStateManagerMap.set(runtimeDrawer.id, visibilityStateManager);
}

return {
...runtimeDrawer,
ariaLabels: { drawerName: runtimeDrawer.ariaLabels.content ?? '', ...runtimeDrawer.ariaLabels },
trigger: {
iconSvg: (
// eslint-disable-next-line react/no-danger
<span dangerouslySetInnerHTML={{ __html: trigger.iconSvg }} />
),
},
content: (
<RuntimeContentWrapper
key={runtimeDrawer.id}
mountContent={mountContent}
unmountContent={unmountContent}
registerVisibilityCallback={visibilityStateManager.registerVisibilityCallback}
/>
),
},
content: (
<RuntimeContentWrapper key={runtimeDrawer.id} mountContent={mountContent} unmountContent={unmountContent} />
),
onResize: event => {
fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id });
},
})
onResize: event => {
fireNonCancelableEvent(runtimeDrawer.onResize, { size: event.detail.size, id: runtimeDrawer.id });
},
onShow: visibilityStateManager.show,
onHide: visibilityStateManager.hide,
};
}
);
const sorted = sortByPriority(converted);
return {
Expand Down
6 changes: 4 additions & 2 deletions src/app-layout/visual-refresh-toolbar/drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import clsx from 'clsx';

import { InternalButton } from '../../../button/internal';
import PanelResizeHandle from '../../../internal/components/panel-resize-handle';
import { fireNonCancelableEvent } from '../../../internal/events';
import { fireNonCancelableEvent, NonCancelableEventHandler } from '../../../internal/events';
import customCssProps from '../../../internal/generated/custom-css-properties';
import { usePrevious } from '../../../internal/hooks/use-previous';
import { createWidgetizedComponent } from '../../../internal/widgets';
Expand Down Expand Up @@ -131,7 +131,9 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD
interface AppLayoutGlobalDrawerImplementationProps {
appLayoutInternals: AppLayoutInternals;
show: boolean;
activeGlobalDrawer: AppLayoutProps.Drawer | undefined;
activeGlobalDrawer:
| (AppLayoutProps.Drawer & { onShow?: NonCancelableEventHandler; onHide?: NonCancelableEventHandler })
| undefined;
}

export function AppLayoutGlobalDrawerImplementation({
Expand Down
7 changes: 4 additions & 3 deletions src/internal/plugins/controllers/drawers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export interface DrawerConfig {
trigger: {
iconSvg: string;
};
mountContent: (container: HTMLElement) => void;
mountContent: (
container: HTMLElement,
onVisibilityChange?: (callback: (isVisible: boolean) => void) => () => void
) => void;
unmountContent: (container: HTMLElement) => void;
preserveInactiveContent?: boolean;
onShow?: NonCancelableEventHandler;
onHide?: NonCancelableEventHandler;
}

export type UpdateDrawerConfig = Pick<DrawerConfig, 'id' | 'badge' | 'resizable' | 'defaultSize'>;
Expand Down
17 changes: 13 additions & 4 deletions src/internal/plugins/helpers/runtime-content-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useRef } from 'react';

type VisibilityCallback = (isVisible: boolean) => void;

interface RuntimeContentWrapperProps {
mountContent: (container: HTMLElement) => void;
mountContent: (container: HTMLElement, visibilityCallback?: (callback: VisibilityCallback) => () => void) => void;
unmountContent: (container: HTMLElement) => void;
registerVisibilityCallback?: (callback: VisibilityCallback) => () => void;
}

export function RuntimeContentWrapper({ mountContent, unmountContent }: RuntimeContentWrapperProps) {
export function RuntimeContentWrapper({
mountContent,
unmountContent,
registerVisibilityCallback,
}: RuntimeContentWrapperProps) {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const container = ref.current!;
mountContent(container);
return () => unmountContent(container);
mountContent(container, registerVisibilityCallback);
return () => {
unmountContent(container);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down
34 changes: 34 additions & 0 deletions src/internal/plugins/helpers/visibility-state-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

/**
* This class is used to manage the visibility state of the runtime drawers.
*/
export default class VisibilityStateManager {
isVisible = false;
visibilityCallback: ((isPaused: boolean) => void) | null = null;

private onVisibleStateChange = () => {
if (this.visibilityCallback) {
this.visibilityCallback(this.isVisible);
}
};

registerVisibilityCallback = (callback: (isVisible: boolean) => void) => {
this.visibilityCallback = callback;

return () => {
this.visibilityCallback = null;
};
};

show = () => {
this.isVisible = true;
this.onVisibleStateChange();
};

hide = () => {
this.isVisible = false;
this.onVisibleStateChange();
};
}

0 comments on commit 2d4628e

Please sign in to comment.