比ant更豐富Modal組件功能實現(xiàn)示例詳解
有哪些比ant更豐富的功能
普通的modal組件如下:
我們寫的modal額外支持,后面沒有蒙版,并且Modal框能夠拖拽
還支持渲染在文檔流里,上面的都是fixed布局,我們這個正常渲染到文檔下面:
render部分
<RenderDialog {...restState} visible={visible} prefixCls={prefixCls} header={renderHeader} attach={attach} closeBtn={renderCloseIcon()} classPrefix={classPrefix} onClose={onClose} onConfirm={onConfirm} footer={footer === true ? defaultFooter() : footer} ref={dialogDom} />
大家記住這個RenderDialog,接下來都是上面?zhèn)鲄⒌慕忉專?/p>
resetState: 是對象,一堆屬性的集合,哪些屬性呢,我們往下看
// 其實默認參數(shù)寫到這里并不科學,因為react有個靜態(tài)屬性defaultProps屬性支持合并props const [state, setState] = useSetState<DialogProps>({ width: 520, // 默認寬度是520 visible: false, // 默認visible是false zIndex: 2500, // 默認zIndex 2500 placement: 'center', // 默認渲染到屏幕中間 mode: 'modal', // 默認的模式是modal是ant那種渲染結果,其他模式我們下面談 showOverlay: true, // 是否展示透明黑色蒙版 destroyOnClose: false, // 關閉彈窗的時候是否銷毀里面的內容 draggable: false, // 是否能拖拽modal preventScrollThrough: true, // 防止?jié)L動穿透 ...props, });
restState在下面,除了state上某些屬性。
const { visible, // 控制對話框是否顯示 attach, // 對話框掛載的節(jié)點,默認掛在組件本身的位置。數(shù)據(jù)類型為 String 時,會被當作選擇器處理,進行節(jié)點查詢。示例:'body' 或 () => document.body closeBtn, // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,// 底部操作欄,默認會有“確認”和“取消”兩個按鈕。值為 true 顯示默認操作按鈕,值為 false 不顯示任何內容,值類型為 Function 表示自定義底部內容 footer = true, // 如果“取消”按鈕存在,則點擊“取消”按鈕時觸發(fā),同時觸發(fā)關閉事件 onCancel = noop, // 如果“確認”按鈕存在,則點擊“確認”按鈕時觸發(fā),或者鍵盤按下回車鍵時觸發(fā) onConfirm = noop, // 如果“確認”按鈕存在,則點擊“確認”按鈕時觸發(fā),或者鍵盤按下回車鍵時觸發(fā) cancelBtn = cancelText, // 取消按鈕,可自定義。值為 null 則不顯示取消按鈕。值類型為字符串,則表示自定義按鈕文本,值類型為 Object 則表示透傳 Button 組件屬性。 confirmBtn = confirmText, // 確認按鈕。值為 null 則不顯示確認按鈕。值類型為字符串,則表示自定義按鈕文本,值類型為 Object 則表示透傳 Button 組件屬性。 onClose = noop, // 關閉事件,點擊取消按鈕、點擊關閉按鈕、點擊蒙層、按下 ESC 等場景下觸發(fā) ...restState } = state;
說了這么多,我們接著看RenderDialog組件上傳入的屬性。
prefixCls不講了,是css屬性前綴,一個字符串,接著看header屬性被包裝為renderHeader
const renderHeader = useMemo(() => { if (!state.header) return null; const iconMap = { info: <InfoCircleFilledIcon className={`${classPrefix}-is-info`} />, warning: <InfoCircleFilledIcon className={`${classPrefix}-is-warning`} />, error: <InfoCircleFilledIcon className={`${classPrefix}-is-error`} />, success: <CheckCircleFilledIcon className={`${classPrefix}-is-success`} />, }; return ( <div className={`${prefixCls}__header-content`}> {iconMap[state.theme]} {state.header} </div> ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.header, state.theme, prefixCls, classPrefix]);
其實就是在header的文字前面多了一個icon,比如成功的彈窗如下:
接著看closeBtn屬性
const renderCloseIcon = () => { if (closeBtn === false) return null; if (closeBtn === true) return <CloseIcon style={{ verticalAlign: 'unset' }} />; return closeBtn || <CloseIcon style={{ verticalAlign: 'unset' }} />; };
這個是右上角關閉按鈕的Icon,很簡單,如果是false,什么都不許安然,如果是undefined或者true渲染這個icon。
好了,我們把整個代碼放到下面,有代碼注釋,沒寫注釋的是上面咋們已經講過的內容,接著就要進入RenderDialog這個組件內部了。
import 的部分省略了 // 渲染 footer的button方法 const renderDialogButton = (btn: TdDialogProps['cancelBtn'], defaultProps: ButtonProps) => { let result = null; if (isString(btn)) { result = <Button {...defaultProps}>{btn}</Button>; } else if (isFunction(btn)) { result = btn(); } return result; }; const Dialog = forwardRef((props: DialogProps, ref: React.Ref<DialogInstance>) => { // 這部分忽略就好,用來獲取全局配置的css前綴字符串 const { classPrefix } = useConfig(); // 這個也忽略,獲取icon組件的 const { CloseIcon, InfoCircleFilledIcon, CheckCircleFilledIcon } = useGlobalIcon({ CloseIcon: TdCloseIcon, InfoCircleFilledIcon: TdInfoCircleFilledIcon, CheckCircleFilledIcon: TdCheckCircleFilledIcon, }); // 用來引用dialog彈框的dom const dialogDom = useRef<HTMLDivElement>(); const [state, setState] = useSetState<DialogProps>({ width: 520, visible: false, zIndex: 2500, placement: 'center', mode: 'modal', showOverlay: true, destroyOnClose: false, draggable: false, preventScrollThrough: true, ...props, }); // 國際化有關的 const [local, t] = useLocaleReceiver('dialog'); const confirmText = t(local.confirm); const cancelText = t(local.cancel); const { visible, attach, closeBtn, footer = true, onCancel = noop, onConfirm = noop, cancelBtn = cancelText, confirmBtn = confirmText, onClose = noop, ...restState } = state; useEffect(() => { setState((prevState) => ({ ...prevState, ...props, })); }, [props, setState, isPlugin]); const prefixCls = `${classPrefix}-dialog`; const renderCloseIcon = () => { if (closeBtn === false) return null; if (closeBtn === true) return <CloseIcon style={{ verticalAlign: 'unset' }} />; return closeBtn || <CloseIcon style={{ verticalAlign: 'unset' }} />; }; // 這里把一些外部方法暴露給調用者,只需要傳入ref就可以獲取 React.useImperativeHandle(ref, () => ({ show() { setState({ visible: true }); }, hide() { setState({ visible: false }); }, destroy() { setState({ visible: false, destroyOnClose: true }); }, update(newOptions) { setState((prevState) => ({ ...prevState, ...(newOptions as DialogProps), })); }, })); const renderHeader = useMemo(() => { if (!state.header) return null; const iconMap = { info: <InfoCircleFilledIcon className={`${classPrefix}-is-info`} />, warning: <InfoCircleFilledIcon className={`${classPrefix}-is-warning`} />, error: <InfoCircleFilledIcon className={`${classPrefix}-is-error`} />, success: <CheckCircleFilledIcon className={`${classPrefix}-is-success`} />, }; return ( <div className={`${prefixCls}__header-content`}> {iconMap[state.theme]} {state.header} </div> ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.header, state.theme, prefixCls, classPrefix]); // 渲染footer的時候,點擊取消按鈕會用到 const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => { onCancel({ e }); onClose({ e, trigger: 'cancel' }); }; // 渲染footer的時候,點擊確認按鈕會用到 const handleConfirm = (e: React.MouseEvent<HTMLButtonElement>) => { onConfirm({ e }); }; const defaultFooter = () => { const renderCancelBtn = renderDialogButton(cancelBtn, { variant: 'outline' }); const renderConfirmBtn = renderDialogButton(confirmBtn, { theme: 'primary' }); return ( <> {renderCancelBtn && React.cloneElement(renderCancelBtn, { onClick: handleCancel, ...renderCancelBtn.props, })} {renderConfirmBtn && React.cloneElement(renderConfirmBtn, { onClick: handleConfirm, ...renderConfirmBtn.props, })} </> ); }; return ( <RenderDialog {...restState} visible={visible} prefixCls={prefixCls} header={renderHeader} attach={attach} closeBtn={renderCloseIcon()} classPrefix={classPrefix} onClose={onClose} onConfirm={onConfirm} footer={footer === true ? defaultFooter() : footer} ref={dialogDom} /> ); }); Dialog.displayName = 'Dialog'; Dialog.defaultProps = dialogDefaultProps; export default Dialog;
接著,我們要渲染的部分其實很簡單,包括
- 背后的黑色蒙層
- 彈框
- 彈框的標題
- 彈框的內容區(qū)域
- 彈框的footer
- 還需要彈框動畫,比如zoom或者fade
渲染黑色蒙層
代碼如下,很簡單
const renderMask = () => { let maskElement; if (showOverlay) { maskElement = ( <CSSTransition in={visible} appear timeout={transitionTime} classNames={`${prefixCls}-fade`} mountOnEnter unmountOnExit nodeRef={maskRef} > <div ref={maskRef} className={`${prefixCls}__mask`} /> </CSSTransition> ); } return maskElement; };
首先介紹一下CSSTransition,這是react-transition-group動畫庫的一個組件,用來幫助我們實現(xiàn)css動畫的。 其中一些屬性說明如下:
- in: ture就是開始動畫,false就是停止動畫
- appear:boolean,為
false
時當CSSTransition
控件加載完畢后不執(zhí)行動畫,為true
時控件加載完畢則立即執(zhí)行動畫。如果要組件初次渲染就有動畫,則需要設成true
。 - timeout 動畫時間
- classNames:動畫的類名,比如classNames:'demo',會自動在進入動畫的時候幫你把類名改為 demo-enter-active, demo-enter-done, 在退出動畫同樣會有類名的改變。
- mountOnEnter:一進來的時候不顯示dom元素
- unmountOnExit:boolean,為
true
時組件將移除處于隱藏狀態(tài)的元素,為false
時組件保持動畫結束時的狀態(tài)而不移除元素。一般要設成true
。 - nodeRef,獲取蒙層的ref
蒙層主要靠css實現(xiàn),我們看下css
position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; background: var(--td-mask-active); pointer-events: auto;
渲染彈框主體
也非常簡單啊,我們把注釋寫在下面的代碼里了,其中有一個需要小小注意的功能就是拖拽功能
// 渲染Dialog主體 const renderDialog = () => { const dest: any = {}; // 把width變?yōu)橛衟x結尾的字符串 if (props.width !== undefined) { dest.width = GetCSSValue(props.width); } // normal 場景下,需要設置 zindex 為auto 避免出現(xiàn)多個 dialog,normal 出現(xiàn)在最上層 if (props.mode === 'normal') { dest.zIndex = 'auto'; } // 獲取footer const footer = props.footer ? <div className={`${prefixCls}__footer`}>{props.footer}</div> : null; // 獲取header const { header } = props; // 獲取Dialog body const body = <div className={`${prefixCls}__body`}>{props.body || props.children}</div>; // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,如:“關閉”。 const closer = closeBtn && ( <span onClick={handleCloseBtnClick} className={`${prefixCls}__close`}> {closeBtn} </span> ); const validWindow = typeof window === 'object'; // 獲取屏幕高度 const screenHeight = validWindow ? window.innerHeight || document.documentElement.clientHeight : undefined; // 獲取屏幕寬度 const screenWidth = validWindow ? window.innerWidth || document.documentElement.clientWidth : undefined; // 設置style const style = { ...dest, ...props.style }; let dialogOffset = { x: 0, y: 0 }; // 拖拽代碼實現(xiàn)部分 const onDialogMove = (e: MouseEvent) => { // offsetWidth是指元素的寬 + padding + border的總和 const { style, offsetWidth, offsetHeight } = dialog.current; // diffX是指彈框部分距離body左邊部分 let diffX = e.clientX - dialogOffset.x; let diffY = e.clientY - dialogOffset.y; // 拖拽上左邊界限制 if (diffX < 0) diffX = 0; if (diffY < 0) diffY = 0; // 右邊的限制 if (screenWidth - offsetWidth - diffX < 0) diffX = screenWidth - offsetWidth; // 下邊的限制 if (screenHeight - offsetHeight - diffY < 0) diffY = screenHeight - offsetHeight; style.position = 'absolute'; style.left = `${diffX}px`; style.top = `${diffY}px`; }; const onDialogMoveEnd = () => { // 恢復指針樣式為默認,并且注銷mousemove, mouseup事件 dialog.current.style.cursor = 'default'; document.removeEventListener('mousemove', onDialogMove); document.removeEventListener('mouseup', onDialogMoveEnd); }; // 拖拽開始,對應mouseDown事件 const onDialogMoveStart = (e: React.MouseEvent<HTMLDivElement>) => { contentClickRef.current = true; // 阻止事件冒泡, mode === 'modeless才能拖拽 if (canDraggable && e.currentTarget === e.target) { const { offsetLeft, offsetTop, offsetHeight, offsetWidth } = dialog.current; // 如果彈出框超出屏幕范圍 不能進行拖拽 if (offsetWidth > screenWidth || offsetHeight > screenHeight) return; // 拖拽樣式設置為move dialog.current.style.cursor = 'move'; // 計算鼠標 e.clientX是鼠標在屏幕的坐標,offsetLeft是Dialog主體跟body的距離 // 所以e.clientX - offsetLeft就是鼠標在是Dialog主體上的橫坐標 const diffX = e.clientX - offsetLeft; const diffY = e.clientY - offsetTop; dialogOffset = { x: diffX, y: diffY, }; // 此時把mousemove和mouseup事件也綁定一下,其實不建議綁定在這里直接操作dom document.addEventListener('mousemove', onDialogMove); document.addEventListener('mouseup', onDialogMoveEnd); } }; // 頂部定位實現(xiàn) const positionStyle: any = {}; if (props.top) { const topValue = GetCSSValue(props.top); positionStyle.paddingTop = topValue; } // 此處獲取定位方式 top 優(yōu)先級較高 存在時 默認使用 top 定位 const positionClass = classnames( `${prefixCls}__position`, { [`${prefixCls}--top`]: !!props.top }, `${props.placement && !props.top ? `${prefixCls}--${props.placement}` : ''}`, ); // 然后就是用css去渲染header body和footer const dialogElement = ( <div className={isNormal ? '' : `${prefixCls}__wrap`}> <div className={isNormal ? '' : positionClass} style={positionStyle} onClick={onMaskClick} ref={dialogPosition}> <div ref={dialog} style={style} className={classnames(`${prefixCls}`, `${prefixCls}--default`)} onMouseDown={onDialogMoveStart} > <div className={classnames(`${prefixCls}__header`)}> {header} {closer} </div> {body} {footer} </div> </div> </div> ); return ( <CSSTransition in={props.visible} appear mountOnEnter unmountOnExit={destroyOnClose} timeout={transitionTime} classNames={`${prefixCls}-zoom`} onEntered={props.onOpened} onExited={onAnimateLeave} nodeRef={dialog} > {dialogElement} </CSSTransition> ); };
我們這里貼一下css部分:
header:
.t-dialog__header { color: var(--td-text-color-primary); font: var(--td-font-title-medium); font-weight: 600; display: flex; align-items: flex-start; word-break: break-word; }
這里注意下:word-wrap:break-word
它會把整個單詞看成一個整體,如果該行末端寬度不夠顯示整個單詞,它會自動把整個單詞放到下一行,而不會把單詞截斷掉的。
body
.t-dialog__body { padding: 16px 0; color: var(--td-text-color-secondary); font: var(--td-font-body-medium); overflow: auto; word-break: break-word; }
footer
width: 100%; text-align: right; padding: 16px 0 0 0;
好了,我們結合一下彈框和蒙層,看下render函數(shù)
const render = () => { // 。。。省略css部分 // 如果不是 modal 模式 默認沒有 mask 也就沒有相關點擊 mask 事件 const dialog = ( <div ref={wrap} className={wrapClass} style={wrapStyle} onKeyDown={handleKeyDown} tabIndex={0}> {mode === 'modal' && renderMask()} {dialogBody} // 這里就是我們上面講的renderDialog </div> ); return dialog; };
設置body overflow:hiiden
為啥要設置body overflow:hiiden這個屬性呢,你打開modal彈窗的時候,如果此時body還有滾動條,那么你滾動鼠標滾輪還可以向下滑動,但是一般情況下,我們打開彈框,是希望用戶目標鎖定在當前交互,此時最好不要允許用戶滾動界面。
當然你也可以允許用戶滾動,我們用一個preventScrollThrough參數(shù)控制。
先記住當前body的css樣式,以及body的overflow的值,代碼如下
useLayoutEffect(() => { bodyOverflow.current = document.body.style.overflow; bodyCssTextRef.current = document.body.style.cssText; }, []);
const isModal = mode === 'modal'; useLayoutEffect(() => { // 只有modal數(shù)量小于1的時候才重置樣式,因為可能出現(xiàn)多個彈框,那么關閉一個彈框就出現(xiàn)滾動條明顯不對 if (isModal) { const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`); if (openDialogDom.length < 1) { document.body.style.cssText = bodyCssTextRef.current; } // 組件銷毀后重置 body 樣式 return () => { if (isModal) { // 此處只能查詢 mode 模式的 dialog 個數(shù) 因為 modeless 會點擊透傳 normal 是正常文檔流 const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`); if (openDialogDom.length < 1) { document.body.style.cssText = bodyCssTextRef.current; document.body.style.overflow = bodyOverflow.current; } } }; }, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]);
上面的代碼還有一個問題,就是我們需要preventScrollThrough這個參數(shù)去控制是否可以body滾動頁面,這個也是算比ant更豐富的功能。
const isModal = mode === 'modal'; useLayoutEffect(() => { // 處于顯示態(tài) if (visible) { // isModal表示是否是普通彈框,就是帶黑色蒙層的 // bodyOverflow.current 引用的是body的overflow屬性 // preventScrollThrough是代表是否可以滾動body // !showInAttachedElement表示不掛載到其他dom上 if (isModal && bodyOverflow.current !== 'hidden' && preventScrollThrough && !showInAttachedElement) { // 求出滾動條的寬度 const scrollWidth = window.innerWidth - document.body.offsetWidth; // 減少回流 if (bodyCssTextRef.current === '') { let bodyCssText = 'overflow: hidden;'; if (scrollWidth > 0) { bodyCssText += `position: relative;width: calc(100% - ${scrollWidth}px);`; } document.body.style.cssText = bodyCssText; } else { if (scrollWidth > 0) { document.body.style.width = `calc(100% - ${scrollWidth}px)`; document.body.style.position = 'relative'; } document.body.style.overflow = 'hidden'; } } // 剛進頁面就focus到彈框組件上 if (wrap.current) { wrap.current.focus(); } } else if (isModal) { const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`); if (openDialogDom.length < 1) { document.body.style.cssText = bodyCssTextRef.current; } } // 組件銷毀后重置 body 樣式 return () => { if (isModal) { // 此處只能查詢 mode 模式的 dialog 個數(shù) 因為 modeless 會點擊透傳 normal 是正常文檔流 const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`); if (openDialogDom.length < 1) { document.body.style.cssText = bodyCssTextRef.current; document.body.style.overflow = bodyOverflow.current; } } else { document.body.style.cssText = bodyCssTextRef.current; document.body.style.overflow = bodyOverflow.current; } }; }, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]);
其實還有一個邏輯,是把彈窗渲染到任意dom里,需要一個Portal組件,我們這里就不說了,后續(xù)將Popup或者叫trigger組件的時候我們講吧。一篇文檔內容太多不好消化。
好了,主邏輯已經寫完了,很簡單吧!
接下來看下完整代碼,沒有注釋的部分是上面已經講過的
//省去了import // 把css的數(shù)字轉為有px結尾的字符串,,這里其實應該寫到一個utils文件夾里,不應該跟主代碼混在一起 function GetCSSValue(v: string | number) { return Number.isNaN(Number(v)) ? v : `${Number(v)}px`; } // 動畫執(zhí)行時間,這里其實應該寫到一個constants文件里,不應該跟主代碼混在一起 const transitionTime = 300; const RenderDialog = forwardRef((props: RenderDialogProps, ref: React.Ref<HTMLDivElement>) => { // 這里不用看,跟國際化有關 const [local] = useLocaleReceiver('dialog'); const { prefixCls, attach, // 對話框掛載的節(jié)點,默認掛在組件本身的位置。數(shù)據(jù)類型為 String 時,會被當作選擇器處理,進行節(jié)點查詢。示例:'body' 或 () => document.body visible, // 控制對話框是否顯示 mode, // 對話框類型,有三種:模態(tài)對話框、非模態(tài)對話框和普通對話框。彈出「模態(tài)對話框」時,只能操作對話框里面的內容,不能操作其他內容。彈出「非模態(tài)對話框」時,則可以操作頁面內所有內容。「普通對話框」是指沒有脫離文檔流的對話框,可以在這個基礎上開發(fā)更多的插件 zIndex, // 對話框層級,Web 側樣式默認為 2500,移動端和小程序樣式默認為 1500 showOverlay, // 是否顯示遮罩層 onEscKeydown = noop,// 按下 ESC 時觸發(fā)事件 onClosed = noop, // 對話框消失動畫效果結束后觸發(fā) onClose = noop, // 關閉事件,點擊取消按鈕、點擊關閉按鈕、點擊蒙層、按下 ESC 等場景下觸發(fā) onCloseBtnClick = noop, // 點擊右上角關閉按鈕時觸發(fā) onOverlayClick = noop, // 如果蒙層存在,點擊蒙層時觸發(fā) onConfirm = noop, // 如果“確認”按鈕存在,則點擊“確認”按鈕時觸發(fā),或者鍵盤按下回車鍵時觸發(fā) preventScrollThrough, // 防止?jié)L動穿透 closeBtn, // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,如:“關閉”。值類型為 TNode,則表示呈現(xiàn)自定義按鈕示例 closeOnEscKeydown, // 按下 ESC 時是否觸發(fā)對話框關閉事件 confirmOnEnter, // 是否在按下回車鍵時,觸發(fā)確認事件 closeOnOverlayClick, // 點擊蒙層時是否觸發(fā)關閉事件 destroyOnClose, // 是否在關閉彈框的時候銷毀子元素 showInAttachedElement, // 僅在掛載元素中顯示抽屜,默認在瀏覽器可視區(qū)域顯示。父元素需要有定位屬性,如:position: relative } = props; const wrap = useRef<HTMLDivElement>(); // 掛載到包裹彈框的dom上,包裹了好幾層。。。 const dialog = useRef<HTMLDivElement>(); // 引用彈窗dom const dialogPosition = useRef<HTMLDivElement>(); // 包裹彈窗,用于定位的dom引用 const maskRef = useRef<HTMLDivElement>(); // 蒙層的dom引用 const bodyOverflow = useRef<string>(); const bodyCssTextRef = useRef<string>(); const contentClickRef = useRef(false); const isModal = mode === 'modal'; const isNormal = mode === 'normal'; const canDraggable = props.draggable && mode === 'modeless'; const dialogOpenClass = `${prefixCls}__${mode}`; useLayoutEffect(() => { bodyOverflow.current = document.body.style.overflow; bodyCssTextRef.current = document.body.style.cssText; }, []); useLayoutEffect(() => { if (visible) { if (isModal && bodyOverflow.current !== 'hidden' && preventScrollThrough && !showInAttachedElement) { const scrollWidth = window.innerWidth - document.body.offsetWidth; // 減少回流 if (bodyCssTextRef.current === '') { let bodyCssText = 'overflow: hidden;'; if (scrollWidth > 0) { bodyCssText += `position: relative;width: calc(100% - ${scrollWidth}px);`; } document.body.style.cssText = bodyCssText; } else { if (scrollWidth > 0) { document.body.style.width = `calc(100% - ${scrollWidth}px)`; document.body.style.position = 'relative'; } document.body.style.overflow = 'hidden'; } } if (wrap.current) { wrap.current.focus(); } } else if (isModal) { const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`); if (openDialogDom.length < 1) { document.body.style.cssText = bodyCssTextRef.current; } } // 組件銷毀后重置 body 樣式 return () => { if (isModal) { // 此處只能查詢 mode 模式的 dialog 個數(shù) 因為 modeless 會點擊透傳 normal 是正常文檔流 const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`); if (openDialogDom.length < 1) { document.body.style.cssText = bodyCssTextRef.current; document.body.style.overflow = bodyOverflow.current; } } else { document.body.style.cssText = bodyCssTextRef.current; document.body.style.overflow = bodyOverflow.current; } }; }, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]); const onAnimateLeave = () => { if (wrap.current) { wrap.current.style.display = 'none'; } if (isModal && preventScrollThrough) { // 還原 body 的滾動條 const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`); if (isModal && openDialogDom.length < 1) { document.body.style.overflow = bodyOverflow.current; } } if (!isModal) { // 關閉彈窗 清空拖拽設置的相關 css const { style } = dialog.current; style.position = 'relative'; style.left = 'unset'; style.top = 'unset'; } onClosed && onClosed(); }; const onMaskClick = (e: React.MouseEvent<HTMLDivElement>) => { if (showOverlay && (closeOnOverlayClick ?? local.closeOnOverlayClick)) { // 判斷點擊事件初次點擊是否為內容區(qū)域 if (contentClickRef.current) { contentClickRef.current = false; } else if (e.target === dialogPosition.current) { onOverlayClick({ e }); onClose({ e, trigger: 'overlay' }); } } }; const handleCloseBtnClick = (e: React.MouseEvent<HTMLDivElement>) => { onCloseBtnClick({ e }); onClose({ e, trigger: 'close-btn' }); }; const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode if (e.key === 'Escape') { e.stopPropagation(); onEscKeydown({ e }); if (closeOnEscKeydown ?? local.closeOnEscKeydown) { onClose({ e, trigger: 'esc' }); } } else if (e.key === 'Enter' || e.key === 'NumpadEnter') { // 回車鍵觸發(fā)點擊確認事件 e.stopPropagation(); if (confirmOnEnter) { onConfirm({ e }); } } }; // 渲染Dialog主體 const renderDialog = () => { const dest: any = {}; // 把width變?yōu)橛衟x結尾的字符串 if (props.width !== undefined) { dest.width = GetCSSValue(props.width); } // normal 場景下,需要設置 zindex 為auto 避免出現(xiàn)多個 dialog,normal 出現(xiàn)在最上層 if (props.mode === 'normal') { dest.zIndex = 'auto'; } // 獲取footer const footer = props.footer ? <div className={`${prefixCls}__footer`}>{props.footer}</div> : null; // 獲取header const { header } = props; // 獲取Dialog body const body = <div className={`${prefixCls}__body`}>{props.body || props.children}</div>; // 關閉按鈕,可以自定義。值為 true 顯示默認關閉按鈕,值為 false 不顯示關閉按鈕。值類型為 string 則直接顯示值,如:“關閉”。 const closer = closeBtn && ( <span onClick={handleCloseBtnClick} className={`${prefixCls}__close`}> {closeBtn} </span> ); const validWindow = typeof window === 'object'; // 獲取屏幕高度 const screenHeight = validWindow ? window.innerHeight || document.documentElement.clientHeight : undefined; // 獲取屏幕寬度 const screenWidth = validWindow ? window.innerWidth || document.documentElement.clientWidth : undefined; // 設置style const style = { ...dest, ...props.style }; let dialogOffset = { x: 0, y: 0 }; // 拖拽代碼實現(xiàn)部分 const onDialogMove = (e: MouseEvent) => { // offsetWidth是指元素的寬 + padding + border的總和 const { style, offsetWidth, offsetHeight } = dialog.current; // diffX是指彈框部分距離body左邊部分 let diffX = e.clientX - dialogOffset.x; let diffY = e.clientY - dialogOffset.y; // 拖拽上左邊界限制 if (diffX < 0) diffX = 0; if (diffY < 0) diffY = 0; // 右邊的限制 if (screenWidth - offsetWidth - diffX < 0) diffX = screenWidth - offsetWidth; // 下邊的限制 if (screenHeight - offsetHeight - diffY < 0) diffY = screenHeight - offsetHeight; style.position = 'absolute'; style.left = `${diffX}px`; style.top = `${diffY}px`; }; const onDialogMoveEnd = () => { // 恢復指針樣式為默認,并且注銷mousemove, mouseup事件 dialog.current.style.cursor = 'default'; document.removeEventListener('mousemove', onDialogMove); document.removeEventListener('mouseup', onDialogMoveEnd); }; // 拖拽開始,對應mouseDown事件 const onDialogMoveStart = (e: React.MouseEvent<HTMLDivElement>) => { contentClickRef.current = true; // 阻止事件冒泡, mode === 'modeless才能拖拽 if (canDraggable && e.currentTarget === e.target) { const { offsetLeft, offsetTop, offsetHeight, offsetWidth } = dialog.current; // 如果彈出框超出屏幕范圍 不能進行拖拽 if (offsetWidth > screenWidth || offsetHeight > screenHeight) return; // 拖拽樣式設置為move dialog.current.style.cursor = 'move'; // 計算鼠標 e.clientX是鼠標在屏幕的坐標,offsetLeft是Dialog主體跟body的距離 // 所以e.clientX - offsetLeft就是鼠標在是Dialog主體上的橫坐標 const diffX = e.clientX - offsetLeft; const diffY = e.clientY - offsetTop; dialogOffset = { x: diffX, y: diffY, }; // 此時把mousemove和mouseup事件也綁定一下,其實不建議綁定在這里直接操作dom document.addEventListener('mousemove', onDialogMove); document.addEventListener('mouseup', onDialogMoveEnd); } }; // 頂部定位實現(xiàn) const positionStyle: any = {}; if (props.top) { const topValue = GetCSSValue(props.top); positionStyle.paddingTop = topValue; } // 此處獲取定位方式 top 優(yōu)先級較高 存在時 默認使用 top 定位 const positionClass = classnames( `${prefixCls}__position`, { [`${prefixCls}--top`]: !!props.top }, `${props.placement && !props.top ? `${prefixCls}--${props.placement}` : ''}`, ); const dialogElement = ( <div className={isNormal ? '' : `${prefixCls}__wrap`}> <div className={isNormal ? '' : positionClass} style={positionStyle} onClick={onMaskClick} ref={dialogPosition}> <div ref={dialog} style={style} className={classnames(`${prefixCls}`, `${prefixCls}--default`)} onMouseDown={onDialogMoveStart} > <div className={classnames(`${prefixCls}__header`)}> {header} {closer} </div> {body} {footer} </div> </div> </div> ); return ( <CSSTransition in={props.visible} appear mountOnEnter unmountOnExit={destroyOnClose} timeout={transitionTime} classNames={`${prefixCls}-zoom`} onEntered={props.onOpened} onExited={onAnimateLeave} nodeRef={dialog} > {dialogElement} </CSSTransition> ); }; const renderMask = () => { let maskElement; if (showOverlay) { maskElement = ( <CSSTransition in={visible} appear timeout={transitionTime} classNames={`${prefixCls}-fade`} mountOnEnter unmountOnExit nodeRef={maskRef} > <div ref={maskRef} className={`${prefixCls}__mask`} /> </CSSTransition> ); } return maskElement; }; const render = () => { const style: CSSProperties = {}; if (visible) { style.display = 'block'; } const wrapStyle = { ...style, zIndex, }; const dialogBody = renderDialog(); const wrapClass = classnames( props.className, `${prefixCls}__ctx`, !isNormal ? `${prefixCls}__ctx--fixed` : '', visible ? dialogOpenClass : '', isModal && showInAttachedElement ? `${prefixCls}__ctx--absolute` : '', props.mode === 'modeless' ? `${prefixCls}__ctx--modeless` : '', ); // 如果不是 modal 模式 默認沒有 mask 也就沒有相關點擊 mask 事件 const dialog = ( <div ref={wrap} className={wrapClass} style={wrapStyle} onKeyDown={handleKeyDown} tabIndex={0}> {mode === 'modal' && renderMask()} {dialogBody} </div> ); let dom = null; if (visible || wrap.current) { // normal 模式 attach 無效 if (attach === '' || isNormal) { dom = dialog; } else { dom = ( <CSSTransition in={visible} appear timeout={transitionTime} mountOnEnter unmountOnExit={destroyOnClose} nodeRef={portalRef} > <Portal attach={attach} ref={portalRef}> {dialog} </Portal> </CSSTransition> ); } } return dom; }; return render(); }); RenderDialog.defaultProps = dialogDefaultProps; export default RenderDialog;
結束,react組件庫繼續(xù)搞起!
以上就是比ant更豐富Modal組件功能實現(xiàn)示例詳解的詳細內容,更多關于ant Modal組件功能的資料請關注腳本之家其它相關文章!
相關文章
解決React報錯Property 'X' does not 
這篇文章主要為大家介紹了解決React報錯Property 'X' does not exist on type 'HTMLElement',有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12React native ListView 增加頂部下拉刷新和底下點擊刷新示例
這篇文章主要介紹了React native ListView 增加頂部下拉刷新和底下點擊刷新示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04React useMemo與useCallabck有什么區(qū)別
useCallback和useMemo是一樣的東西,只是入?yún)⒂兴煌?,useCallback緩存的是回調函數(shù),如果依賴項沒有更新,就會使用緩存的回調函數(shù);useMemo緩存的是回調函數(shù)的return,如果依賴項沒有更新,就會使用緩存的return2022-12-12React?TypeScript?應用中便捷使用Redux?Toolkit方法詳解
這篇文章主要為大家介紹了React?TypeScript?應用中便捷使用Redux?Toolkit方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11react render props模式實現(xiàn)組件復用示例
本文主要介紹了react render props模式實現(xiàn)組件復用示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07