fix(techdocs): prevent content re-renders
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Dispatch } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Extension } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { SetStateAction } from 'react';
|
||||
@@ -24,6 +25,9 @@ export function createTechDocsAddonExtension<TComponentProps>(
|
||||
options: TechDocsAddonOptions<TComponentProps>,
|
||||
): Extension<(props: TComponentProps) => JSX.Element | null>;
|
||||
|
||||
// @public
|
||||
export const SHADOW_DOM_STYLE_LOAD_EVENT = 'TECH_DOCS_SHADOW_DOM_STYLE_LOAD';
|
||||
|
||||
// @public
|
||||
export type SyncResult = 'cached' | 'updated';
|
||||
|
||||
@@ -109,6 +113,19 @@ export type TechDocsReaderPageValue = {
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const TechDocsShadowDom: ({
|
||||
element,
|
||||
onAppend,
|
||||
children,
|
||||
}: TechDocsShadowDomProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TechDocsShadowDomProps = PropsWithChildren<{
|
||||
element: Element;
|
||||
onAppend?: (shadowRoot: ShadowRoot) => void;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export interface TechDocsStorageApi {
|
||||
// (undocumented)
|
||||
@@ -135,6 +152,9 @@ export interface TechDocsStorageApi {
|
||||
// @public
|
||||
export const techdocsStorageApiRef: ApiRef<TechDocsStorageApi>;
|
||||
|
||||
// @public
|
||||
export const useShadowDomStylesLoading: (element: Element | null) => boolean;
|
||||
|
||||
// @public
|
||||
export const useShadowRoot: () => ShadowRoot | undefined;
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/react-hooks": "^8.0.0",
|
||||
"@backstage/test-utils": "^1.1.0",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { render, waitFor, screen } from '@testing-library/react';
|
||||
import {
|
||||
SHADOW_DOM_STYLE_LOAD_EVENT,
|
||||
TechDocsShadowDom,
|
||||
TechDocsShadowDomProps,
|
||||
} from './component';
|
||||
|
||||
const createDom = (innerHTML: string) => {
|
||||
const newDom = document.createElement('html');
|
||||
newDom.innerHTML = innerHTML;
|
||||
return newDom;
|
||||
};
|
||||
|
||||
describe('TechDocsShadowDom', () => {
|
||||
it('Should render children', () => {
|
||||
const dom = createDom('<body><h1>Title</h1></body>');
|
||||
const onAppend = jest.fn();
|
||||
render(
|
||||
<TechDocsShadowDom element={dom} onAppend={onAppend}>
|
||||
Children
|
||||
</TechDocsShadowDom>,
|
||||
);
|
||||
expect(screen.getByText('Children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should re-render if props changes', async () => {
|
||||
const Component = ({
|
||||
onAppend,
|
||||
}: Pick<TechDocsShadowDomProps, 'onAppend'>) => {
|
||||
const [dom, setDom] = useState(createDom('<body><h1>Title1</h1></body>'));
|
||||
|
||||
useEffect(() => {
|
||||
setDom(createDom('<body><h1>Title2</h1></body>'));
|
||||
}, []);
|
||||
|
||||
return <TechDocsShadowDom element={dom} onAppend={onAppend} />;
|
||||
};
|
||||
|
||||
const onAppend = jest.fn();
|
||||
render(<Component onAppend={onAppend} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const shadowHost = screen.getByTestId('techdocs-native-shadowroot');
|
||||
const h1 = shadowHost.shadowRoot?.querySelector('h1');
|
||||
expect(h1).toHaveTextContent('Title2');
|
||||
});
|
||||
expect(onAppend).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Should show progress bar while styles are being loaded', async () => {
|
||||
const dom = createDom(
|
||||
'<head><link rel="stylesheet" src="styles.css"/></head><body><h1>Title</h1></body>',
|
||||
);
|
||||
const onAppend = jest.fn();
|
||||
dom.querySelector('link[rel="stylesheet"]')!.addEventListener = () => {};
|
||||
|
||||
render(
|
||||
<TechDocsShadowDom element={dom} onAppend={onAppend}>
|
||||
Children
|
||||
</TechDocsShadowDom>,
|
||||
);
|
||||
|
||||
await await waitFor(() => {
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should dispatch an event after all styles are loaded', async () => {
|
||||
const dom = createDom(
|
||||
'<head><link rel="stylesheet" src="styles.css"/></head><body><h1>Title</h1></body>',
|
||||
);
|
||||
let listener: EventListenerOrEventListenerObject = () => {};
|
||||
dom.querySelector('link')!.addEventListener = (
|
||||
_type: string,
|
||||
_listener: EventListenerOrEventListenerObject,
|
||||
) => {
|
||||
listener = _listener;
|
||||
};
|
||||
const handleStylesLoad = jest.fn();
|
||||
dom.addEventListener(SHADOW_DOM_STYLE_LOAD_EVENT, handleStylesLoad);
|
||||
|
||||
render(<TechDocsShadowDom element={dom}>Children</TechDocsShadowDom>);
|
||||
|
||||
await await waitFor(() => {
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
listener({} as Event);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(handleStylesLoad).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { create } from 'jss';
|
||||
import { StylesProvider, jssPreset } from '@material-ui/styles';
|
||||
|
||||
import { Progress } from '@backstage/core-components';
|
||||
|
||||
/**
|
||||
* Name for the event dispatched when ShadowRoot styles are loaded.
|
||||
* @public
|
||||
*/
|
||||
export const SHADOW_DOM_STYLE_LOAD_EVENT = 'TECH_DOCS_SHADOW_DOM_STYLE_LOAD';
|
||||
|
||||
/**
|
||||
* Dispatch style load event after all styles are loaded.
|
||||
* @param element - the ShadowRoot tree.
|
||||
*/
|
||||
const useShadowDomStylesEvents = (element: Element | null) => {
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const styles = element.querySelectorAll<HTMLElement>(
|
||||
'head > link[rel="stylesheet"]',
|
||||
);
|
||||
|
||||
let count = styles?.length ?? 0;
|
||||
const event = new CustomEvent(SHADOW_DOM_STYLE_LOAD_EVENT);
|
||||
|
||||
if (!count) {
|
||||
element.dispatchEvent(event);
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const handleLoad = () => {
|
||||
if (--count === 0) {
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
styles?.forEach(style => {
|
||||
style.addEventListener('load', handleLoad);
|
||||
});
|
||||
|
||||
return () => {
|
||||
styles?.forEach(style => {
|
||||
style.removeEventListener('load', handleLoad);
|
||||
});
|
||||
};
|
||||
}, [element]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the style's loading state.
|
||||
*
|
||||
* @example
|
||||
* Here's an example that updates the sidebar position only after styles are calculated:
|
||||
* ```jsx
|
||||
* import {
|
||||
* TechDocsShadowDom,
|
||||
* useShadowDomStylesLoading,
|
||||
* } from '@backstage/plugin-techdocs-react';
|
||||
*
|
||||
* export const TechDocsReaderPageContent = () => {
|
||||
* // ...
|
||||
* const dom = useTechDocsReaderDom(entity);
|
||||
* const isStyleLoading = useShadowDomStylesLoading(dom);
|
||||
*
|
||||
* const updateSidebarPosition = useCallback(() => {
|
||||
* //...
|
||||
* }, [dom]);
|
||||
*
|
||||
* useEffect(() => {
|
||||
* if (!isStyleLoading) {
|
||||
* updateSidebarPosition();
|
||||
* }
|
||||
* }, [isStyleLoading, updateSidebarPosition]);
|
||||
*
|
||||
* const handleDomAppend = useCallback(
|
||||
* (newShadowRoot: ShadowRoot) => {
|
||||
* setShadowRoot(newShadowRoot);
|
||||
* },
|
||||
* [setShadowRoot],
|
||||
* );
|
||||
*
|
||||
* return <TechDocsShadowDom element={dom} onAppend={handleDomAppend} />;
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @param element - which is the ShadowRoot tree.
|
||||
* @returns a boolean value, true if styles are being loaded.
|
||||
* @public
|
||||
*/
|
||||
export const useShadowDomStylesLoading = (element: Element | null) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) return () => {};
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const style = (element as HTMLElement).style;
|
||||
|
||||
style.setProperty('opacity', '0');
|
||||
|
||||
const handleLoad = () => {
|
||||
setLoading(false);
|
||||
style.setProperty('opacity', '1');
|
||||
};
|
||||
|
||||
element.addEventListener(SHADOW_DOM_STYLE_LOAD_EVENT, handleLoad);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(SHADOW_DOM_STYLE_LOAD_EVENT, handleLoad);
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
return loading;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsShadowDom}.
|
||||
*
|
||||
* @remarks
|
||||
* If you want to use portals to render Material UI components in the Shadow DOM,
|
||||
* you must render these portals as children because this component wraps its children in a Material UI StylesProvider
|
||||
* to ensure that Material UI styles are applied.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsShadowDomProps = PropsWithChildren<{
|
||||
/**
|
||||
* Element tree that is appended to ShadowRoot.
|
||||
*/
|
||||
element: Element;
|
||||
/**
|
||||
* Callback called when the element tree is appended in ShadowRoot.
|
||||
*/
|
||||
onAppend?: (shadowRoot: ShadowRoot) => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Renders a tree of elements in a Shadow DOM.
|
||||
*
|
||||
* @remarks
|
||||
* Centers the styles loaded event to avoid having multiple locations setting the opacity style in Shadow DOM causing the screen to flash multiple times,
|
||||
* so if you want to know when Shadow DOM styles are computed, you can listen for the "TECH_DOCS_SHADOW_DOM_STYLE_LOAD" event dispatched by the element tree.
|
||||
*
|
||||
* @example
|
||||
* Here is an example using this component and also listening for styles loaded event:
|
||||
*```jsx
|
||||
* import {
|
||||
* TechDocsShadowDom,
|
||||
* SHADOW_DOM_STYLE_LOAD_EVENT,
|
||||
* } from '@backstage/plugin-techdocs-react';
|
||||
*
|
||||
* export const TechDocsReaderPageContent = ({ entity }: TechDocsReaderPageContentProps) => {
|
||||
* // ...
|
||||
* const dom = useTechDocsReaderDom(entity);
|
||||
*
|
||||
* useEffect(() => {
|
||||
* const updateSidebarPosition = () => {
|
||||
* // ...
|
||||
* };
|
||||
* dom?.addEventListener(SHADOW_DOM_STYLE_LOAD_EVENT, updateSidebarPosition);
|
||||
* return () => {
|
||||
* dom?.removeEventListener(SHADOW_DOM_STYLE_LOAD_EVENT, updateSidebarPosition);
|
||||
* };
|
||||
* }, [dom]);
|
||||
*
|
||||
* const handleDomAppend = useCallback(
|
||||
* (newShadowRoot: ShadowRoot) => {
|
||||
* setShadowRoot(newShadowRoot);
|
||||
* },
|
||||
* [setShadowRoot],
|
||||
* );
|
||||
*
|
||||
* return <TechDocsShadowDom element={dom} onAppend={handleDomAppend} />;
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @param props - see {@link TechDocsShadowDomProps}.
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsShadowDom = ({
|
||||
element,
|
||||
onAppend,
|
||||
children,
|
||||
}: TechDocsShadowDomProps) => {
|
||||
const [jss, setJss] = useState(
|
||||
create({
|
||||
...jssPreset(),
|
||||
insertionPoint: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
useShadowDomStylesEvents(element);
|
||||
const loading = useShadowDomStylesLoading(element);
|
||||
|
||||
const ref = useCallback(
|
||||
(shadowHost: HTMLDivElement) => {
|
||||
if (!element || !shadowHost) return;
|
||||
|
||||
setJss(
|
||||
create({
|
||||
...jssPreset(),
|
||||
insertionPoint: element.querySelector('head') || undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
let shadowRoot = shadowHost.shadowRoot;
|
||||
|
||||
if (!shadowRoot) {
|
||||
shadowRoot = shadowHost.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
shadowRoot.replaceChildren(element);
|
||||
|
||||
if (typeof onAppend === 'function') {
|
||||
onAppend(shadowRoot);
|
||||
}
|
||||
},
|
||||
[element, onAppend],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <Progress />}
|
||||
{/* The sheetsManager={new Map()} is needed in order to deduplicate the injection of CSS in the page. */}
|
||||
<StylesProvider jss={jss} sheetsManager={new Map()}>
|
||||
<div ref={ref} data-testid="techdocs-native-shadowroot" />
|
||||
{children}
|
||||
</StylesProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -34,14 +34,20 @@ export type {
|
||||
TechDocsReaderPageProviderRenderFunction,
|
||||
TechDocsReaderPageValue,
|
||||
} from './context';
|
||||
export {
|
||||
useShadowRoot,
|
||||
useShadowRootElements,
|
||||
useShadowRootSelection,
|
||||
} from './hooks';
|
||||
export { TechDocsAddonLocations } from './types';
|
||||
export type {
|
||||
TechDocsEntityMetadata,
|
||||
TechDocsMetadata,
|
||||
TechDocsAddonOptions,
|
||||
} from './types';
|
||||
export type { TechDocsShadowDomProps } from './component';
|
||||
export {
|
||||
TechDocsShadowDom,
|
||||
useShadowDomStylesLoading,
|
||||
SHADOW_DOM_STYLE_LOAD_EVENT,
|
||||
} from './component';
|
||||
export {
|
||||
useShadowRoot,
|
||||
useShadowRootElements,
|
||||
useShadowRootSelection,
|
||||
} from './hooks';
|
||||
|
||||
+1
-19
@@ -14,22 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const mockStylesheetEventListener = (timeToCallbackMs: number): void => {
|
||||
HTMLLinkElement.prototype.addEventListener = (
|
||||
_eventName: string,
|
||||
eventCallback: any,
|
||||
) => {
|
||||
setTimeout(() => {
|
||||
eventCallback();
|
||||
}, timeToCallbackMs);
|
||||
};
|
||||
};
|
||||
|
||||
export const executeStylesheetEventListeners = (): void => {
|
||||
jest.runOnlyPendingTimers();
|
||||
};
|
||||
|
||||
export const clearStylesheetEventListeners = (): void => {
|
||||
HTMLLinkElement.prototype.addEventListener =
|
||||
Element.prototype.addEventListener;
|
||||
};
|
||||
import '@testing-library/jest-dom';
|
||||
-22
@@ -110,28 +110,6 @@ describe('<TechDocsReaderPageContent />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should render progress if there is no dom and reader state is checking', async () => {
|
||||
getEntityMetadata.mockResolvedValue(mockEntityMetadata);
|
||||
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
|
||||
useTechDocsReaderDom.mockReturnValue(undefined);
|
||||
useReaderState.mockReturnValue({ state: 'CHECKING' });
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.queryByTestId('techdocs-native-shadowroot'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(rendered.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render techdocs content if entity metadata is missing', async () => {
|
||||
getEntityMetadata.mockResolvedValue(undefined);
|
||||
useTechDocsReaderDom.mockReturnValue(document.createElement('html'));
|
||||
|
||||
+13
-78
@@ -14,15 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { create } from 'jss';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { makeStyles, Grid, Portal } from '@material-ui/core';
|
||||
import { StylesProvider, jssPreset } from '@material-ui/styles';
|
||||
import { makeStyles, Grid } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
useTechDocsAddons,
|
||||
TechDocsAddonLocations as locations,
|
||||
TechDocsShadowDom,
|
||||
useTechDocsReaderPage,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
@@ -33,6 +30,7 @@ import { TechDocsStateIndicator } from '../TechDocsStateIndicator';
|
||||
|
||||
import { useTechDocsReaderDom } from './dom';
|
||||
import { withTechDocsReaderProvider } from '../TechDocsReaderProvider';
|
||||
import { TechDocsReaderPageContentAddons } from './TechDocsReaderPageContentAddons';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
search: {
|
||||
@@ -71,79 +69,25 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider(
|
||||
(props: TechDocsReaderPageContentProps) => {
|
||||
const { withSearch = true, onReady } = props;
|
||||
const classes = useStyles();
|
||||
const addons = useTechDocsAddons();
|
||||
|
||||
const {
|
||||
entityMetadata: { value: entityMetadata, loading: entityMetadataLoading },
|
||||
entityRef,
|
||||
shadowRoot,
|
||||
setShadowRoot,
|
||||
} = useTechDocsReaderPage();
|
||||
|
||||
const dom = useTechDocsReaderDom(entityRef);
|
||||
|
||||
const [jss, setJss] = useState(
|
||||
create({
|
||||
...jssPreset(),
|
||||
insertionPoint: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const ref = useCallback(
|
||||
(shadowHost: HTMLDivElement) => {
|
||||
if (!dom || !shadowHost) return;
|
||||
|
||||
setJss(
|
||||
create({
|
||||
...jssPreset(),
|
||||
insertionPoint: dom.querySelector('head') || undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const newShadowRoot =
|
||||
shadowHost.shadowRoot ?? shadowHost.attachShadow({ mode: 'open' });
|
||||
newShadowRoot.innerHTML = '';
|
||||
newShadowRoot.appendChild(dom);
|
||||
const handleAppend = useCallback(
|
||||
(newShadowRoot: ShadowRoot) => {
|
||||
setShadowRoot(newShadowRoot);
|
||||
if (onReady instanceof Function) {
|
||||
onReady();
|
||||
}
|
||||
},
|
||||
[dom, setShadowRoot, onReady],
|
||||
[setShadowRoot, onReady],
|
||||
);
|
||||
|
||||
const contentElement = shadowRoot?.querySelector(
|
||||
'[data-md-component="content"]',
|
||||
);
|
||||
|
||||
const primarySidebarElement = shadowRoot?.querySelector(
|
||||
'div[data-md-component="sidebar"][data-md-type="navigation"], div[data-md-component="navigation"]',
|
||||
);
|
||||
let primarySidebarAddonLocation = primarySidebarElement?.querySelector(
|
||||
'[data-techdocs-addons-location="primary sidebar"]',
|
||||
);
|
||||
if (!primarySidebarAddonLocation) {
|
||||
primarySidebarAddonLocation = document.createElement('div');
|
||||
primarySidebarAddonLocation.setAttribute(
|
||||
'data-techdocs-addons-location',
|
||||
'primary sidebar',
|
||||
);
|
||||
primarySidebarElement?.prepend(primarySidebarAddonLocation);
|
||||
}
|
||||
|
||||
const secondarySidebarElement = shadowRoot?.querySelector(
|
||||
'div[data-md-component="sidebar"][data-md-type="toc"], div[data-md-component="toc"]',
|
||||
);
|
||||
let secondarySidebarAddonLocation = secondarySidebarElement?.querySelector(
|
||||
'[data-techdocs-addons-location="secondary sidebar"]',
|
||||
);
|
||||
if (!secondarySidebarAddonLocation) {
|
||||
secondarySidebarAddonLocation = document.createElement('div');
|
||||
secondarySidebarAddonLocation.setAttribute(
|
||||
'data-techdocs-addons-location',
|
||||
'secondary sidebar',
|
||||
);
|
||||
secondarySidebarElement?.prepend(secondarySidebarAddonLocation);
|
||||
}
|
||||
|
||||
// No entity metadata = 404. Don't render content at all.
|
||||
if (entityMetadataLoading === false && !entityMetadata)
|
||||
return <ErrorPage status="404" statusMessage="PAGE NOT FOUND" />;
|
||||
@@ -174,19 +118,10 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider(
|
||||
</Grid>
|
||||
)}
|
||||
<Grid xs={12} item>
|
||||
{/* sheetsManager={new Map()} is needed in order to deduplicate the injection of CSS in the page. */}
|
||||
<StylesProvider jss={jss} sheetsManager={new Map()}>
|
||||
<div ref={ref} data-testid="techdocs-native-shadowroot" />
|
||||
<Portal container={primarySidebarAddonLocation}>
|
||||
{addons.renderComponentsByLocation(locations.PrimarySidebar)}
|
||||
</Portal>
|
||||
<Portal container={contentElement}>
|
||||
{addons.renderComponentsByLocation(locations.Content)}
|
||||
</Portal>
|
||||
<Portal container={secondarySidebarAddonLocation}>
|
||||
{addons.renderComponentsByLocation(locations.SecondarySidebar)}
|
||||
</Portal>
|
||||
</StylesProvider>
|
||||
{/* Centers the styles loaded event to avoid having multiple locations setting the opacity style in Shadow Dom causing the screen to flash multiple times */}
|
||||
<TechDocsShadowDom element={dom} onAppend={handleAppend}>
|
||||
<TechDocsReaderPageContentAddons />
|
||||
</TechDocsShadowDom>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Portal } from '@material-ui/core';
|
||||
import {
|
||||
useTechDocsAddons,
|
||||
TechDocsAddonLocations as locations,
|
||||
useTechDocsReaderPage,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
export const TechDocsReaderPageContentAddons = () => {
|
||||
const addons = useTechDocsAddons();
|
||||
|
||||
const { shadowRoot } = useTechDocsReaderPage();
|
||||
|
||||
const contentElement = shadowRoot?.querySelector(
|
||||
'[data-md-component="content"]',
|
||||
);
|
||||
|
||||
const primarySidebarElement = shadowRoot?.querySelector(
|
||||
'div[data-md-component="sidebar"][data-md-type="navigation"], div[data-md-component="navigation"]',
|
||||
);
|
||||
let primarySidebarAddonLocation = primarySidebarElement?.querySelector(
|
||||
'[data-techdocs-addons-location="primary sidebar"]',
|
||||
);
|
||||
if (!primarySidebarAddonLocation) {
|
||||
primarySidebarAddonLocation = document.createElement('div');
|
||||
primarySidebarAddonLocation.setAttribute(
|
||||
'data-techdocs-addons-location',
|
||||
'primary sidebar',
|
||||
);
|
||||
primarySidebarElement?.prepend(primarySidebarAddonLocation);
|
||||
}
|
||||
|
||||
const secondarySidebarElement = shadowRoot?.querySelector(
|
||||
'div[data-md-component="sidebar"][data-md-type="toc"], div[data-md-component="toc"]',
|
||||
);
|
||||
let secondarySidebarAddonLocation = secondarySidebarElement?.querySelector(
|
||||
'[data-techdocs-addons-location="secondary sidebar"]',
|
||||
);
|
||||
if (!secondarySidebarAddonLocation) {
|
||||
secondarySidebarAddonLocation = document.createElement('div');
|
||||
secondarySidebarAddonLocation.setAttribute(
|
||||
'data-techdocs-addons-location',
|
||||
'secondary sidebar',
|
||||
);
|
||||
secondarySidebarElement?.prepend(secondarySidebarAddonLocation);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Portal container={primarySidebarAddonLocation}>
|
||||
{addons.renderComponentsByLocation(locations.PrimarySidebar)}
|
||||
</Portal>
|
||||
<Portal container={contentElement}>
|
||||
{addons.renderComponentsByLocation(locations.Content)}
|
||||
</Portal>
|
||||
<Portal container={secondarySidebarAddonLocation}>
|
||||
{addons.renderComponentsByLocation(locations.SecondarySidebar)}
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@
|
||||
import { useContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useTheme, Theme } from '@material-ui/core';
|
||||
import { Theme, useTheme, useMediaQuery } from '@material-ui/core';
|
||||
import { lighten, alpha } from '@material-ui/core/styles';
|
||||
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
@@ -26,7 +26,10 @@ import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { SidebarPinStateContext } from '@backstage/core-components';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
import { techdocsStorageApiRef } from '@backstage/plugin-techdocs-react';
|
||||
import {
|
||||
techdocsStorageApiRef,
|
||||
useShadowDomStylesLoading,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
import { useTechDocsReader } from '../TechDocsReaderProvider';
|
||||
|
||||
@@ -46,14 +49,22 @@ import {
|
||||
copyToClipboard,
|
||||
} from '../../transformers';
|
||||
|
||||
const MOBILE_MEDIA_QUERY = 'screen and (max-width: 76.1875em)';
|
||||
|
||||
type TypographyHeadings = Pick<
|
||||
Theme['typography'],
|
||||
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
>;
|
||||
|
||||
type TypographyHeadingsKeys = keyof TypographyHeadings;
|
||||
|
||||
const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
/**
|
||||
* Sidebar pinned status to be used in computing CSS style injections
|
||||
*/
|
||||
const useSidebar = () => useContext(SidebarPinStateContext);
|
||||
|
||||
/**
|
||||
* Hook that encapsulates the behavior of getting raw HTML and applying
|
||||
* transforms to it in order to make it function at a basic level in the
|
||||
@@ -63,83 +74,82 @@ export const useTechDocsReaderDom = (
|
||||
entityRef: CompoundEntityRef,
|
||||
): Element | null => {
|
||||
const navigate = useNavigate();
|
||||
const sidebar = useSidebar();
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
const isMobileMedia = useMediaQuery(MOBILE_MEDIA_QUERY);
|
||||
|
||||
const configApi = useApi(configApiRef);
|
||||
const techdocsStorageApi = useApi(techdocsStorageApiRef);
|
||||
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
|
||||
const techdocsSanitizer = useApi(configApiRef);
|
||||
const { namespace, kind, name } = entityRef;
|
||||
|
||||
const { state, path, content: rawPage } = useTechDocsReader();
|
||||
const isDarkTheme = theme.palette.type === 'dark';
|
||||
|
||||
const [sidebars, setSidebars] = useState<HTMLElement[]>();
|
||||
const [dom, setDom] = useState<HTMLElement | null>(null);
|
||||
|
||||
// sidebar pinned status to be used in computing CSS style injections
|
||||
const { isPinned } = useContext(SidebarPinStateContext);
|
||||
const isStyleLoading = useShadowDomStylesLoading(dom);
|
||||
|
||||
const updateSidebarPosition = useCallback(() => {
|
||||
if (!dom || !sidebars) return;
|
||||
// set sidebar height so they don't initially render in wrong position
|
||||
const mdTabs = dom.querySelector('.md-container > .md-tabs');
|
||||
const sidebarsCollapsed = window.matchMedia(
|
||||
'screen and (max-width: 76.1875em)',
|
||||
).matches;
|
||||
const newTop = Math.max(dom.getBoundingClientRect().top, 0);
|
||||
sidebars.forEach(sidebar => {
|
||||
if (sidebarsCollapsed) {
|
||||
sidebar.style.top = '0px';
|
||||
} else if (mdTabs) {
|
||||
sidebar.style.top = `${
|
||||
newTop + mdTabs.getBoundingClientRect().height
|
||||
}px`;
|
||||
if (!dom) return;
|
||||
|
||||
const sidebars = dom.querySelectorAll<HTMLElement>('.md-sidebar');
|
||||
|
||||
sidebars.forEach(element => {
|
||||
// set sidebar position to render in correct position
|
||||
if (isMobileMedia) {
|
||||
element.style.top = '0px';
|
||||
} else {
|
||||
sidebar.style.top = `${newTop}px`;
|
||||
const domTop = dom.getBoundingClientRect().top ?? 0;
|
||||
const tabs = dom.querySelector('.md-container > .md-tabs');
|
||||
const tabsHeight = tabs?.getBoundingClientRect().height ?? 0;
|
||||
element.style.top = `${Math.max(domTop, 0) + tabsHeight}px`;
|
||||
}
|
||||
// Show the sidebar only after updating its position
|
||||
sidebar.style.removeProperty('opacity');
|
||||
|
||||
// show the sidebar only after updating its position
|
||||
element.style.setProperty('opacity', '1');
|
||||
});
|
||||
}, [dom, sidebars]);
|
||||
}, [dom, isMobileMedia]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSidebarPosition();
|
||||
window.addEventListener('scroll', updateSidebarPosition, true);
|
||||
window.addEventListener('resize', updateSidebarPosition);
|
||||
window.addEventListener('scroll', updateSidebarPosition, true);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateSidebarPosition, true);
|
||||
window.removeEventListener('resize', updateSidebarPosition);
|
||||
window.removeEventListener('scroll', updateSidebarPosition, true);
|
||||
};
|
||||
// an update to "state" might lead to an updated UI so we include it as a trigger
|
||||
}, [updateSidebarPosition, state]);
|
||||
}, [dom, updateSidebarPosition]);
|
||||
|
||||
// dynamically set width of footer to accommodate for pinning of the sidebar
|
||||
const updateFooterWidth = useCallback(() => {
|
||||
if (!dom) return;
|
||||
const footer = dom.querySelector('.md-footer') as HTMLElement;
|
||||
const footer = dom.querySelector<HTMLElement>('.md-footer');
|
||||
if (footer) {
|
||||
footer.style.width = `${dom.getBoundingClientRect().width}px`;
|
||||
}
|
||||
}, [dom]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFooterWidth();
|
||||
window.addEventListener('resize', updateFooterWidth);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateFooterWidth);
|
||||
};
|
||||
});
|
||||
}, [dom, updateFooterWidth]);
|
||||
|
||||
// an update to "state" might lead to an updated UI so we include it as a trigger
|
||||
useEffect(() => {
|
||||
if (!isStyleLoading) {
|
||||
updateFooterWidth();
|
||||
updateSidebarPosition();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state, isStyleLoading, updateFooterWidth, updateSidebarPosition]);
|
||||
|
||||
// a function that performs transformations that are executed prior to adding it to the DOM
|
||||
const preRender = useCallback(
|
||||
(rawContent: string, contentPath: string) =>
|
||||
transformer(rawContent, [
|
||||
sanitizeDOM(techdocsSanitizer.getOptionalConfig('techdocs.sanitizer')),
|
||||
sanitizeDOM(configApi.getOptionalConfig('techdocs.sanitizer')),
|
||||
addBaseUrl({
|
||||
techdocsStorageApi,
|
||||
entityId: {
|
||||
kind,
|
||||
name,
|
||||
namespace,
|
||||
},
|
||||
entityId: entityRef,
|
||||
path: contentPath,
|
||||
}),
|
||||
rewriteDocLinks(),
|
||||
@@ -235,22 +245,22 @@ export const useTechDocsReaderDom = (
|
||||
--md-code-bg-color: ${theme.palette.background.paper};
|
||||
--md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)};
|
||||
--md-code-hl-keyword-color: ${
|
||||
isDarkTheme
|
||||
theme.palette.type === 'dark'
|
||||
? theme.palette.primary.light
|
||||
: theme.palette.primary.dark
|
||||
};
|
||||
--md-code-hl-function-color: ${
|
||||
isDarkTheme
|
||||
theme.palette.type === 'dark'
|
||||
? theme.palette.secondary.light
|
||||
: theme.palette.secondary.dark
|
||||
};
|
||||
--md-code-hl-string-color: ${
|
||||
isDarkTheme
|
||||
theme.palette.type === 'dark'
|
||||
? theme.palette.success.light
|
||||
: theme.palette.success.dark
|
||||
};
|
||||
--md-code-hl-number-color: ${
|
||||
isDarkTheme
|
||||
theme.palette.type === 'dark'
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.dark
|
||||
};
|
||||
@@ -269,17 +279,17 @@ export const useTechDocsReaderDom = (
|
||||
--md-typeset-a-color: var(--md-accent-fg-color);
|
||||
--md-typeset-table-color: ${theme.palette.text.primary};
|
||||
--md-typeset-del-color: ${
|
||||
isDarkTheme
|
||||
theme.palette.type === 'dark'
|
||||
? alpha(theme.palette.error.dark, 0.5)
|
||||
: alpha(theme.palette.error.light, 0.5)
|
||||
};
|
||||
--md-typeset-ins-color: ${
|
||||
isDarkTheme
|
||||
theme.palette.type === 'dark'
|
||||
? alpha(theme.palette.success.dark, 0.5)
|
||||
: alpha(theme.palette.success.light, 0.5)
|
||||
};
|
||||
--md-typeset-mark-color: ${
|
||||
isDarkTheme
|
||||
theme.palette.type === 'dark'
|
||||
? alpha(theme.palette.warning.dark, 0.5)
|
||||
: alpha(theme.palette.warning.light, 0.5)
|
||||
};
|
||||
@@ -465,7 +475,7 @@ export const useTechDocsReaderDom = (
|
||||
width: 12.1rem !important;
|
||||
z-index: 200;
|
||||
left: ${
|
||||
isPinned
|
||||
sidebar.isPinned
|
||||
? 'calc(-12.1rem + 242px)'
|
||||
: 'calc(-12.1rem + 72px)'
|
||||
} !important;
|
||||
@@ -611,19 +621,23 @@ export const useTechDocsReaderDom = (
|
||||
}
|
||||
|
||||
.highlight .nx {
|
||||
color: ${isDarkTheme ? '#ff53a3' : '#ec407a'};
|
||||
color: ${theme.palette.type === 'dark' ? '#ff53a3' : '#ec407a'};
|
||||
}
|
||||
|
||||
/* CODE HILITE */
|
||||
.codehilite .gd {
|
||||
background-color: ${
|
||||
isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'
|
||||
theme.palette.type === 'dark'
|
||||
? 'rgba(248,81,73,0.65)'
|
||||
: '#fdd'
|
||||
};
|
||||
}
|
||||
|
||||
.codehilite .gi {
|
||||
background-color: ${
|
||||
isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'
|
||||
theme.palette.type === 'dark'
|
||||
? 'rgba(46,160,67,0.65)'
|
||||
: '#dfd'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -678,15 +692,13 @@ export const useTechDocsReaderDom = (
|
||||
}),
|
||||
]),
|
||||
[
|
||||
kind,
|
||||
name,
|
||||
namespace,
|
||||
scmIntegrationsApi,
|
||||
techdocsSanitizer,
|
||||
techdocsStorageApi,
|
||||
// only add dependencies that are in state or memorized variables to avoid unnecessary calls between re-renders
|
||||
entityRef,
|
||||
theme,
|
||||
isDarkTheme,
|
||||
isPinned,
|
||||
sidebar,
|
||||
configApi,
|
||||
scmIntegrationsApi,
|
||||
techdocsStorageApi,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -723,33 +735,29 @@ export const useTechDocsReaderDom = (
|
||||
}
|
||||
},
|
||||
}),
|
||||
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
|
||||
onCssReady({
|
||||
docStorageUrl: await techdocsStorageApi.getApiOrigin(),
|
||||
onLoading: (renderedElement: Element) => {
|
||||
(renderedElement as HTMLElement).style.setProperty('opacity', '0');
|
||||
const renderedSidebars = Array.from(
|
||||
renderedElement.querySelectorAll<HTMLElement>('.md-sidebar'),
|
||||
);
|
||||
// Hide sidebars until your position is updated
|
||||
for (const sidebar of renderedSidebars) {
|
||||
sidebar.style.setProperty('opacity', '0');
|
||||
}
|
||||
},
|
||||
onLoaded: (renderedElement: Element) => {
|
||||
(renderedElement as HTMLElement).style.removeProperty('opacity');
|
||||
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
|
||||
renderedElement
|
||||
onLoading: () => {},
|
||||
onLoaded: () => {
|
||||
transformedElement
|
||||
.querySelector('.md-nav__title')
|
||||
?.removeAttribute('for');
|
||||
const renderedSidebars = Array.from(
|
||||
renderedElement.querySelectorAll<HTMLElement>('.md-sidebar'),
|
||||
);
|
||||
// Wait for styles to be calculated to update sidebar position
|
||||
setSidebars(renderedSidebars);
|
||||
},
|
||||
}),
|
||||
// hide sidebars until their positions are updated
|
||||
onCssReady({
|
||||
onLoading: () => {
|
||||
const sidebars = Array.from(
|
||||
transformedElement.querySelectorAll<HTMLElement>('.md-sidebar'),
|
||||
);
|
||||
sidebars.forEach(element => {
|
||||
element.style.setProperty('opacity', '0');
|
||||
});
|
||||
},
|
||||
onLoaded: () => {},
|
||||
}),
|
||||
]),
|
||||
[theme, navigate, techdocsStorageApi],
|
||||
[theme, navigate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
import { CircularProgress, Button, makeStyles } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
@@ -47,8 +46,6 @@ export const TechDocsStateIndicator = () => {
|
||||
buildLog,
|
||||
} = useTechDocsReader();
|
||||
|
||||
const ReaderProgress = state === 'CHECKING' ? <Progress /> : null;
|
||||
|
||||
if (state === 'INITIAL_BUILD') {
|
||||
StateAlert = (
|
||||
<Alert
|
||||
@@ -130,10 +127,5 @@ export const TechDocsStateIndicator = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ReaderProgress}
|
||||
{StateAlert}
|
||||
</>
|
||||
);
|
||||
return StateAlert;
|
||||
};
|
||||
|
||||
@@ -14,12 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
clearStylesheetEventListeners,
|
||||
createTestShadowDom,
|
||||
executeStylesheetEventListeners,
|
||||
mockStylesheetEventListener,
|
||||
} from '../../test-utils';
|
||||
import { createTestShadowDom } from '../../test-utils';
|
||||
import { onCssReady } from './onCssReady';
|
||||
|
||||
const docStorageUrl: string =
|
||||
@@ -31,22 +26,6 @@ const fixture = `
|
||||
`;
|
||||
|
||||
describe('onCssReady', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockStylesheetEventListener(100);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearStylesheetEventListeners();
|
||||
});
|
||||
|
||||
it('does not call onLoading and onLoaded without the onCssReady transformer', async () => {
|
||||
const onLoading = jest.fn();
|
||||
const onLoaded = jest.fn();
|
||||
@@ -57,7 +36,6 @@ describe('onCssReady', () => {
|
||||
});
|
||||
|
||||
expect(onLoading).not.toHaveBeenCalled();
|
||||
executeStylesheetEventListeners();
|
||||
expect(onLoaded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -69,7 +47,6 @@ describe('onCssReady', () => {
|
||||
preTransformers: [],
|
||||
postTransformers: [
|
||||
onCssReady({
|
||||
docStorageUrl,
|
||||
onLoading,
|
||||
onLoaded,
|
||||
}),
|
||||
@@ -77,12 +54,6 @@ describe('onCssReady', () => {
|
||||
});
|
||||
|
||||
expect(onLoading).toHaveBeenCalledTimes(1);
|
||||
expect(onLoading).toHaveBeenCalledWith(expect.any(Element));
|
||||
expect(onLoaded).not.toHaveBeenCalled();
|
||||
|
||||
executeStylesheetEventListeners();
|
||||
|
||||
expect(onLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(onLoaded).toHaveBeenCalledWith(expect.any(Element));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,40 +14,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SHADOW_DOM_STYLE_LOAD_EVENT } from '@backstage/plugin-techdocs-react';
|
||||
import type { Transformer } from './transformer';
|
||||
|
||||
type OnCssReadyOptions = {
|
||||
docStorageUrl: string;
|
||||
onLoading: (dom: Element) => void;
|
||||
onLoaded: (dom: Element) => void;
|
||||
onLoading: () => void;
|
||||
onLoaded: () => void;
|
||||
};
|
||||
|
||||
export const onCssReady = ({
|
||||
docStorageUrl,
|
||||
onLoading,
|
||||
onLoaded,
|
||||
}: OnCssReadyOptions): Transformer => {
|
||||
return dom => {
|
||||
const cssPages = Array.from(
|
||||
dom.querySelectorAll('head > link[rel="stylesheet"]'),
|
||||
).filter(elem => elem.getAttribute('href')?.startsWith(docStorageUrl));
|
||||
|
||||
let count = cssPages.length;
|
||||
|
||||
if (count > 0) {
|
||||
onLoading(dom);
|
||||
}
|
||||
|
||||
cssPages.forEach(cssPage =>
|
||||
cssPage.addEventListener('load', () => {
|
||||
count -= 1;
|
||||
|
||||
if (count === 0) {
|
||||
onLoaded(dom);
|
||||
}
|
||||
}),
|
||||
onLoading();
|
||||
dom.addEventListener(
|
||||
SHADOW_DOM_STYLE_LOAD_EVENT,
|
||||
function handleShadowDomStyleLoad() {
|
||||
onLoaded();
|
||||
dom.removeEventListener(
|
||||
SHADOW_DOM_STYLE_LOAD_EVENT,
|
||||
handleShadowDomStyleLoad,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return dom;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,5 +20,4 @@ export const FIXTURES = {
|
||||
FIXTURE_STANDARD_PAGE,
|
||||
};
|
||||
|
||||
export * from './stylesheets';
|
||||
export * from './shadowDom';
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SHADOW_DOM_STYLE_LOAD_EVENT } from '@backstage/plugin-techdocs-react';
|
||||
import type { Transformer } from '../reader/transformers';
|
||||
import { transform as transformer } from '../reader/transformers';
|
||||
|
||||
@@ -50,6 +51,9 @@ export const createTestShadowDom = async (
|
||||
await transformer(dom, opts.postTransformers);
|
||||
}
|
||||
|
||||
// Simulate event dispatched after all styles are loaded
|
||||
dom.dispatchEvent(new CustomEvent(SHADOW_DOM_STYLE_LOAD_EVENT));
|
||||
|
||||
return divElement.shadowRoot!;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user