feat(infra): add ability to mount nodes to nearest FrameworkScope root (#7551)

![CleanShot 2024-07-19 at 12.52.08.gif](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/dc5b8dc6-b7b2-4db2-83d5-7601c3f966d8.gif)
This commit is contained in:
CatsJuice
2024-07-26 03:19:27 +00:00
parent 22c36102b9
commit 8646221ee8
4 changed files with 102 additions and 16 deletions

View File

@@ -4,6 +4,9 @@ import type { FrameworkProvider, Scope, Service } from '../core';
import { ComponentNotFoundError, Framework } from '../core';
import { parseIdentifier } from '../core/identifier';
import type { GeneralIdentifier, IdentifierType, Type } from '../core/types';
import { MountPoint } from './scope-root-components';
export { useMount } from './scope-root-components';
export const FrameworkStackContext = React.createContext<FrameworkProvider[]>([
Framework.EMPTY.provider(),
@@ -126,7 +129,7 @@ export const FrameworkScope = ({
return (
<FrameworkStackContext.Provider value={nextStack}>
{children}
<MountPoint>{children}</MountPoint>
</FrameworkStackContext.Provider>
);
};

View File

@@ -0,0 +1,74 @@
import React from 'react';
type NodesMap = Map<
number,
{
node: React.ReactNode;
debugKey?: string;
}
>;
const ScopeRootComponentsContext = React.createContext<{
nodes: NodesMap;
setNodes: React.Dispatch<React.SetStateAction<NodesMap>>;
}>({ nodes: new Map(), setNodes: () => {} });
let _id = 0;
/**
* A hook to add nodes to the nearest scope's root
*/
export const useMount = (debugKey?: string) => {
const [id] = React.useState(_id++);
const { setNodes } = React.useContext(ScopeRootComponentsContext);
const unmount = React.useCallback(() => {
setNodes(prev => {
if (!prev.has(id)) {
return prev;
}
const next = new Map(prev);
next.delete(id);
return next;
});
}, [id, setNodes]);
const mount = React.useCallback(
(node: React.ReactNode) => {
setNodes(prev => new Map(prev).set(id, { node, debugKey }));
return unmount;
},
[setNodes, id, debugKey, unmount]
);
return React.useMemo(() => {
return {
/**
* Add a node to the nearest scope root
* ```tsx
* const { mount } = useMount();
* useEffect(() => {
* const unmount = mount(<div>Node</div>);
* return unmount;
* }, [])
* ```
* @return A function to unmount the added node.
*/
mount,
};
}, [mount]);
};
export const MountPoint = ({ children }: React.PropsWithChildren) => {
const [nodes, setNodes] = React.useState<NodesMap>(new Map());
return (
<ScopeRootComponentsContext.Provider value={{ nodes, setNodes }}>
{children}
{Array.from(nodes.entries()).map(([id, { node, debugKey }]) => (
<div data-testid={debugKey} key={id} style={{ display: 'contents' }}>
{node}
</div>
))}
</ScopeRootComponentsContext.Provider>
);
};

View File

@@ -1,5 +1,6 @@
import { Modal } from '@affine/component';
import { useCallback, useState } from 'react';
import { useMount } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react';
import type { AllPageListConfig } from './edit-collection';
import { SelectPage } from './select-page';
@@ -20,8 +21,11 @@ export const useSelectPage = ({
const handleCancel = useCallback(() => {
close(false);
}, [close]);
return {
node: (
const { mount } = useMount('select-page-modal');
useEffect(() => {
return mount(
<Modal
open={!!value}
onOpenChange={close}
@@ -47,16 +51,22 @@ export const useSelectPage = ({
/>
) : null}
</Modal>
);
}, [allPageListConfig, close, handleCancel, mount, value]);
return {
open: useCallback(
(init: string[]): Promise<string[]> =>
new Promise<string[]>(res => {
onChange({
init,
onConfirm: list => {
close(false);
res(list);
},
});
}),
[close]
),
open: (init: string[]): Promise<string[]> =>
new Promise<string[]>(res => {
onChange({
init,
onConfirm: list => {
close(false);
res(list);
},
});
}),
};
};

View File

@@ -72,7 +72,7 @@ export const RulesMode = ({
allowListPages.push(meta);
}
});
const { node: selectPageNode, open } = useSelectPage({ allPageListConfig });
const { open } = useSelectPage({ allPageListConfig });
const openSelectPage = useCallback(() => {
open(collection.allowList).then(
ids => {
@@ -343,7 +343,6 @@ export const RulesMode = ({
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>{buttons}</div>
</div>
{selectPageNode}
</>
);
};