Модалки рендерятся порталом в body поверх шапки SharePoint
На живой странице оверлей модалки жил внутри канвы веб-части, чей контекст наложения ниже шапки SharePoint: верх высокого окна (например, «Новый шаг маршрута») уезжал под шапку. В workbench шапки нет, поэтому там было незаметно. Теперь Modal и ConfirmDialog рендерят подложку через ModalOverlay (портал в document.body) с z-index выше шапки; меню выпадающих списков подняты над ним. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './Modal.module.scss';
|
import styles from './Modal.module.scss';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import { ModalOverlay } from './ModalOverlay';
|
||||||
import { texts } from '../../texts';
|
import { texts } from '../../texts';
|
||||||
|
|
||||||
export interface IConfirmDialogProps {
|
export interface IConfirmDialogProps {
|
||||||
@@ -20,7 +21,7 @@ export const ConfirmDialog: React.FC<IConfirmDialogProps> = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
busy
|
busy
|
||||||
}) => (
|
}) => (
|
||||||
<div className={styles.overlay} onClick={e => { if (e.target === e.currentTarget) onCancel(); }}>
|
<ModalOverlay onBackdropClick={onCancel}>
|
||||||
<div className={`${styles.box} ${styles.narrow}`}>
|
<div className={`${styles.box} ${styles.narrow}`}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
<p className={styles.text}>{text}</p>
|
<p className={styles.text}>{text}</p>
|
||||||
@@ -29,5 +30,5 @@ export const ConfirmDialog: React.FC<IConfirmDialogProps> = ({
|
|||||||
<Button variant="danger" icon="trash" onClick={onConfirm} disabled={busy}>{confirmLabel}</Button>
|
<Button variant="danger" icon="trash" onClick={onConfirm} disabled={busy}>{confirmLabel}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ModalOverlay>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,9 +7,17 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 300;
|
// Выше шапки SharePoint, но ниже слоёв Fluent UI (1000000). Оверлей рендерится
|
||||||
|
// порталом в body (см. ModalOverlay), поэтому сравнивается с шапкой напрямую.
|
||||||
|
z-index: 100000;
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
// Портал вне корня веб-части — шрифт и метрики .shell (AdminApp.module.scss)
|
||||||
|
// сюда не наследуются, задаём заново.
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: $fs-body;
|
||||||
|
line-height: 1.55;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './Modal.module.scss';
|
import styles from './Modal.module.scss';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
|
import { ModalOverlay } from './ModalOverlay';
|
||||||
import { texts } from '../../texts';
|
import { texts } from '../../texts';
|
||||||
|
|
||||||
export interface IModalProps {
|
export interface IModalProps {
|
||||||
@@ -11,8 +12,7 @@ export interface IModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Modal: React.FC<IModalProps> = ({ title, onClose, footer, children }) => (
|
export const Modal: React.FC<IModalProps> = ({ title, onClose, footer, children }) => (
|
||||||
// Клик по подложке закрывает окно, клик внутри — нет (проверяем сам currentTarget).
|
<ModalOverlay onBackdropClick={onClose}>
|
||||||
<div className={styles.overlay} onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
|
|
||||||
<div className={styles.box}>
|
<div className={styles.box}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
@@ -21,5 +21,5 @@ export const Modal: React.FC<IModalProps> = ({ title, onClose, footer, children
|
|||||||
<div className={styles.body}>{children}</div>
|
<div className={styles.body}>{children}</div>
|
||||||
{footer && <div className={styles.footer}>{footer}</div>}
|
{footer && <div className={styles.footer}>{footer}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ModalOverlay>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
import styles from './Modal.module.scss';
|
||||||
|
|
||||||
|
export interface IModalOverlayProps {
|
||||||
|
/** Клик по подложке (не по содержимому окна). */
|
||||||
|
onBackdropClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Затемнение под модалкой — порталом в document.body, а не внутри веб-части.
|
||||||
|
* На живой странице SharePoint канва веб-части лежит в своём контексте наложения
|
||||||
|
* ниже шапки (suite bar), и position: fixed-оверлей изнутри неё оказывался ПОД
|
||||||
|
* шапкой: верх окна, центрированного по высоте экрана, уезжал за неё. В workbench
|
||||||
|
* шапки нет — там было незаметно.
|
||||||
|
*/
|
||||||
|
export const ModalOverlay: React.FC<IModalOverlayProps> = ({ onBackdropClick, children }) =>
|
||||||
|
ReactDOM.createPortal(
|
||||||
|
// Клик по подложке закрывает окно, клик внутри — нет (проверяем сам currentTarget).
|
||||||
|
<div className={styles.overlay} onClick={e => { if (e.target === e.currentTarget) onBackdropClick(); }}>
|
||||||
|
{children}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@import '../../../../styles/design-tokens';
|
@import '../../../../styles/design-tokens';
|
||||||
|
|
||||||
// Ниже Modal.module.scss/.overlay (z-index 300) — диалоги, которые может
|
// Ниже Modal.module.scss/.overlay (z-index 100000) — диалоги, которые может
|
||||||
// открыть сама длительная операция (например, разрешение конфликта), должны
|
// открыть сама длительная операция (например, разрешение конфликта), должны
|
||||||
// быть ПОВЕРХ этой подложки, а не под ней.
|
// быть ПОВЕРХ этой подложки, а не под ней.
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
box-shadow: $sh-modal;
|
box-shadow: $sh-modal;
|
||||||
border: 1px solid $gray-200;
|
border: 1px solid $gray-200;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
z-index: 400;
|
z-index: 100001; // над оверлеем модалки (Modal.module.scss)
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export * from './PrimaryCell';
|
|||||||
export * from './ActionsCell';
|
export * from './ActionsCell';
|
||||||
export * from './CellNote';
|
export * from './CellNote';
|
||||||
export * from './Modal';
|
export * from './Modal';
|
||||||
|
export * from './ModalOverlay';
|
||||||
export * from './ConfirmDialog';
|
export * from './ConfirmDialog';
|
||||||
export * from './FormModal';
|
export * from './FormModal';
|
||||||
export * from './FormGrid';
|
export * from './FormGrid';
|
||||||
|
|||||||
Reference in New Issue
Block a user