Skip to content

Commit

Permalink
Merge pull request #1215 from core-ds/feat/portal_container
Browse files Browse the repository at this point in the history
feat/portal_container
  • Loading branch information
fulcanellee authored Jun 28, 2024
2 parents 7f79f9f + 09d8f24 commit 47f3263
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 45 deletions.
6 changes: 6 additions & 0 deletions .changeset/eleven-poems-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@alfalab/core-components-portal': minor
'@alfalab/core-components-shared': minor
---

Добавлена возможность переопределять рендер контейнер для группы элементов использующих Portal
3 changes: 2 additions & 1 deletion packages/portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"react-dom": "^16.9.0 || ^17.0.1 || ^18.0.0"
},
"dependencies": {
"tslib": "^2.4.0"
"tslib": "^2.4.0",
"@alfalab/core-components-shared": "^0.10.0"
}
}
22 changes: 21 additions & 1 deletion packages/portal/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';

import { Portal } from './index';
import { PORTAL_CONTAINER_ATTRIBUTE } from './utils';
import { PortalContext } from '@alfalab/core-components-shared';

describe('Portal tests', () => {
it('should render in a different node', () => {
Expand Down Expand Up @@ -63,4 +64,23 @@ describe('Portal tests', () => {

expect(document.querySelector('#portal-container')).toContainElement(portalChild);
});

it('render to element based on portal context', () => {
const textContent = 'Text content';
const getPortalContainer = () => document.querySelector('#portal-container');

render(
<>
<div data-test-id='portal-container' id='portal-container'></div>
<PortalContext.Provider value={getPortalContainer}>
<Portal>
<span>{textContent}</span>
</Portal>
</PortalContext.Provider>
</>,
);

const portalChild = screen.queryByText(textContent);
expect(screen.queryByTestId('portal-container')).toContainElement(portalChild);
});
});
55 changes: 29 additions & 26 deletions packages/portal/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { forwardRef, ReactNode, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

import { getDefaultPortalContainer, setRef } from './utils';
import { usePortalContainer } from './hooks/usePortalContainer';
import { setRef } from './utils';

export type PortalProps = {
/** Контент */
Expand All @@ -17,30 +18,32 @@ export type PortalProps = {
*/
immediateMount?: boolean;
};
export const Portal = forwardRef<Element, PortalProps>(
({ getPortalContainer = getDefaultPortalContainer, immediateMount = false, children }, ref) => {
const [mountNode, setMountNode] = useState<Element | null>(() =>
typeof window !== 'undefined' && immediateMount ? getPortalContainer() : null,
);

useEffect(() => {
setMountNode(getPortalContainer());
}, [getPortalContainer]);

useEffect(() => {
if (mountNode) {
setRef(ref, mountNode);

return () => {
setRef(ref, null);
};
}

return () => null;
}, [ref, mountNode]);

return mountNode ? createPortal(children, mountNode) : mountNode;
},
);
export const Portal = forwardRef<Element, PortalProps>((props, ref) => {
const portalContainer = usePortalContainer();

const { getPortalContainer = portalContainer, immediateMount = false, children } = props;

const [mountNode, setMountNode] = useState<Element | null>(() =>
typeof window !== 'undefined' && immediateMount ? getPortalContainer() : null,
);

useEffect(() => {
setMountNode(getPortalContainer());
}, [getPortalContainer]);

useEffect(() => {
if (mountNode) {
setRef(ref, mountNode);

return () => {
setRef(ref, null);
};
}

return () => null;
}, [ref, mountNode]);

return mountNode ? createPortal(children, mountNode) : mountNode;
});

Portal.displayName = 'Portal';
16 changes: 16 additions & 0 deletions packages/portal/src/docs/development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ import { Portal } from '../index';
import { Portal } from '@alfalab/core-components/portal';
```

## Глобальное переопределение контейнера для рендера

Если вам необходимо переопределить рендер контейнер для группы элементов, вы можете использовать следующую обертку:


```jsx
import {PortalContext} from '@alfalab/core-components/shared';

<PortalContext.Provider value={() => document.querySelector('.foobar')}>
...
</PortalContext.Provider>;
```

Все компоненты внутри обертки, которые используют Portal, будут ссылаться на один и тот же DOM элемент.
Таким образом, вам не потребуется отдельно переопределять контейнер с помощью пропса getPortalContainer.

## Свойства

<ArgTypes of={Portal} />
15 changes: 15 additions & 0 deletions packages/portal/src/hooks/usePortalContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useContext } from 'react';

import { PortalContext } from '@alfalab/core-components-shared';

import { getDefaultPortalContainer } from '../utils';

export const usePortalContainer = () => {
const getContextPortalContainer = useContext(PortalContext);

if (getContextPortalContainer()) {
return getContextPortalContainer;
}

return getDefaultPortalContainer;
};
5 changes: 4 additions & 1 deletion packages/portal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"compilerOptions": {
"outDir": "dist",
"rootDirs": ["src"]
}
},
"references": [
{ "path": "../shared" },
]
}
5 changes: 5 additions & 0 deletions packages/shared/src/context/PortalContext/PortalContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from 'react';

type PortalContextType = () => Element | null;

export const PortalContext = createContext<PortalContextType>(() => null);
1 change: 1 addition & 0 deletions packages/shared/src/context/PortalContext/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PortalContext } from './PortalContext';
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './hooks';
export * from './os';
export * from './dom';
export * from './exhaustiveCheck';
export * from './context/PortalContext';
38 changes: 22 additions & 16 deletions packages/toast-plate/src/component.screenshots.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ describe('ToastPlate | main props', () => {
componentName: 'ToastPlate',
knobs: {
children: 'Вам одобрено. Согласитесь на предложение',
badge: ['positive-checkmark',
'negative-cross',
'negative-alert',
'negative-block',
'attention-alert',
'neutral-information',
'neutral-operation',
'neutral-cross', ''],
badge: [
'positive-checkmark',
'negative-cross',
'negative-alert',
'negative-block',
'attention-alert',
'neutral-information',
'neutral-operation',
'neutral-cross',
'',
],
title: '',
hasCloser: [false, true],
},
Expand All @@ -44,14 +47,17 @@ describe('ToastPlate | main props', () => {
componentName: 'ToastPlate',
knobs: {
children: 'Вам одобрено. Согласитесь на предложение',
badge: ['positive-checkmark',
'negative-cross',
'negative-alert',
'negative-block',
'attention-alert',
'neutral-information',
'neutral-operation',
'neutral-cross', ''],
badge: [
'positive-checkmark',
'negative-cross',
'negative-alert',
'negative-block',
'attention-alert',
'neutral-information',
'neutral-operation',
'neutral-cross',
'',
],
title: 'Поздравляем, полный успех',
hasCloser: [false, true],
},
Expand Down

0 comments on commit 47f3263

Please sign in to comment.