UNPKG

reblend-ui

Version:

Utilities for creating robust overlay components

154 lines (119 loc) 3.61 kB
import css from 'dom-helpers/css'; import { dataAttr } from './DataKey'; import getBodyScrollbarWidth from './getScrollbarWidth'; export interface ModalInstance { dialog: Element; backdrop: Element; } export interface ModalManagerOptions { ownerDocument?: Document; handleContainerOverflow?: boolean; isRTL?: boolean; } export type ContainerState = { scrollBarWidth: number; style: Record<string, any>; [key: string]: any; }; export const OPEN_DATA_ATTRIBUTE = dataAttr('modal-open'); /** * Manages a stack of Modals as well as ensuring * body scrolling is is disabled and padding accounted for */ class ModalManager { readonly handleContainerOverflow: boolean; readonly isRTL: boolean; readonly modals: ModalInstance[]; protected state!: ContainerState; protected ownerDocument: Document | undefined; constructor({ ownerDocument, handleContainerOverflow = true, isRTL = false, }: ModalManagerOptions = {}) { this.handleContainerOverflow = handleContainerOverflow; this.isRTL = isRTL; this.modals = []; this.ownerDocument = ownerDocument; } getScrollbarWidth() { return getBodyScrollbarWidth(this.ownerDocument); } getElement() { return (this.ownerDocument || document).body; } setModalAttributes(_modal: ModalInstance) { // For overriding } removeModalAttributes(_modal: ModalInstance) { // For overriding } setContainerStyle(containerState: ContainerState) { const style: Partial<CSSStyleDeclaration> = { overflow: 'hidden' }; // we are only interested in the actual `style` here // because we will override it const paddingProp = this.isRTL ? 'paddingLeft' : 'paddingRight'; const container = this.getElement(); containerState.style = { overflow: container.style.overflow, [paddingProp]: container.style[paddingProp], }; if (containerState.scrollBarWidth) { // use computed style, here to get the real padding // to add our scrollbar width style[paddingProp] = `${ parseInt(css(container, paddingProp) || '0', 10) + containerState.scrollBarWidth }px`; } container.setAttribute(OPEN_DATA_ATTRIBUTE, ''); css(container, style as any); } reset() { [...this.modals].forEach((m) => this.remove(m)); } removeContainerStyle(containerState: ContainerState) { const container = this.getElement(); container.removeAttribute(OPEN_DATA_ATTRIBUTE); Object.assign(container.style, containerState.style); } add(modal: ModalInstance) { let modalIdx = this.modals.indexOf(modal); if (modalIdx !== -1) { return modalIdx; } modalIdx = this.modals.length; this.modals.push(modal); this.setModalAttributes(modal); if (modalIdx !== 0) { return modalIdx; } this.state = { scrollBarWidth: this.getScrollbarWidth(), style: {}, }; if (this.handleContainerOverflow) { this.setContainerStyle(this.state); } return modalIdx; } remove(modal: ModalInstance) { const modalIdx = this.modals.indexOf(modal); if (modalIdx === -1) { return; } this.modals.splice(modalIdx, 1); // if that was the last modal in a container, // clean up the container if (!this.modals.length && this.handleContainerOverflow) { this.removeContainerStyle(this.state); } this.removeModalAttributes(modal); } isTopModal(modal: ModalInstance) { return ( !!this.modals.length && this.modals[this.modals.length - 1] === modal ); } } export default ModalManager;