mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
refactor(component): render react element into lit (#6124)
See docs in https://insider.affine.pro/share/af3478a2-9c9c-4d16-864d-bffa1eb10eb6/oL1ifjA4rKv7HRn5nYzIF This PR also enables opening split view by ctrl-click a page link in a doc.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export { createComponent as createReactComponentFromLit } from './create-component';
|
||||
export * from './lit-portal';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
type ElementOrFactory,
|
||||
useLitPortal,
|
||||
useLitPortalFactory,
|
||||
} from './lite-portal';
|
||||
@@ -0,0 +1,148 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
type PortalEvent = {
|
||||
name: 'connectedCallback' | 'disconnectedCallback' | 'willUpdate';
|
||||
target: LitReactPortal;
|
||||
previousPortalId?: string;
|
||||
};
|
||||
|
||||
type PortalListener = (event: PortalEvent) => void;
|
||||
const listeners: Set<PortalListener> = new Set();
|
||||
|
||||
export function createLitPortalAnchor(callback: (event: PortalEvent) => void) {
|
||||
const id = nanoid();
|
||||
// todo: clean up listeners?
|
||||
listeners.add(event => {
|
||||
if (event.target.portalId !== id) {
|
||||
return;
|
||||
}
|
||||
callback(event);
|
||||
});
|
||||
return html`<lit-react-portal portalId=${id}></lit-react-portal>`;
|
||||
}
|
||||
|
||||
class LitReactPortal extends LitElement {
|
||||
portalId: string = '';
|
||||
|
||||
static override get properties() {
|
||||
return {
|
||||
portalId: { type: String },
|
||||
};
|
||||
}
|
||||
|
||||
// do not enable shadow root
|
||||
override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
listeners.forEach(l =>
|
||||
l({
|
||||
name: 'connectedCallback',
|
||||
target: this,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
listeners.forEach(l =>
|
||||
l({
|
||||
name: 'disconnectedCallback',
|
||||
target: this,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override willUpdate(changedProperties: any) {
|
||||
super.willUpdate(changedProperties);
|
||||
listeners.forEach(l =>
|
||||
l({
|
||||
name: 'willUpdate',
|
||||
target: this,
|
||||
previousPortalId: changedProperties.get('portalId'),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('lit-react-portal', LitReactPortal);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'lit-react-portal': LitReactPortal;
|
||||
}
|
||||
}
|
||||
|
||||
export type ElementOrFactory = React.ReactElement | (() => React.ReactElement);
|
||||
|
||||
type LitPortal = {
|
||||
id: string;
|
||||
portal: React.ReactPortal;
|
||||
};
|
||||
|
||||
// returns a factory function that renders a given element to a lit template
|
||||
export const useLitPortalFactory = () => {
|
||||
const [portals, setPortals] = useState<LitPortal[]>([]);
|
||||
|
||||
return [
|
||||
useCallback(
|
||||
(elementOrFactory: React.ReactElement | (() => React.ReactElement)) => {
|
||||
const element =
|
||||
typeof elementOrFactory === 'function'
|
||||
? elementOrFactory()
|
||||
: elementOrFactory;
|
||||
return createLitPortalAnchor(event => {
|
||||
const portalId = event.target.portalId;
|
||||
setPortals(portals => {
|
||||
const newPortals = portals.filter(
|
||||
p => p.id !== event.previousPortalId && p.id !== portalId
|
||||
);
|
||||
if (event.name !== 'disconnectedCallback') {
|
||||
newPortals.push({
|
||||
id: portalId,
|
||||
portal: ReactDOM.createPortal(element, event.target),
|
||||
});
|
||||
}
|
||||
return newPortals;
|
||||
});
|
||||
});
|
||||
},
|
||||
[setPortals]
|
||||
),
|
||||
portals,
|
||||
] as const;
|
||||
};
|
||||
|
||||
// render a react element to a lit template
|
||||
export const useLitPortal = (
|
||||
elementOrFactory: React.ReactElement | (() => React.ReactElement)
|
||||
) => {
|
||||
const [anchor, setAnchor] = useState<HTMLElement>();
|
||||
const template = useMemo(
|
||||
() =>
|
||||
createLitPortalAnchor(event => {
|
||||
if (event.name !== 'disconnectedCallback') {
|
||||
setAnchor(event.target as HTMLElement);
|
||||
} else {
|
||||
setAnchor(undefined);
|
||||
}
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const element = useMemo(
|
||||
() =>
|
||||
typeof elementOrFactory === 'function'
|
||||
? elementOrFactory()
|
||||
: elementOrFactory,
|
||||
[elementOrFactory]
|
||||
);
|
||||
return {
|
||||
template,
|
||||
portal: anchor ? ReactDOM.createPortal(element, anchor) : undefined,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user