UNPKG

react-floater

Version:
170 lines (142 loc) 3.87 kB
import * as React from 'react'; import { Modifier, Placement } from '@popperjs/core'; import { deepmerge } from 'deepmerge-ts'; import is from 'is-lite'; import { SetRequired } from 'type-fest'; import { LogOptions, PopperModifiers, Props } from '../types'; export const portalId = 'react-floater-portal'; export function canUseDOM(): boolean { return !!(typeof window !== 'undefined' && window.document?.createElement); } export function enhanceProps(props: Props) { return { autoOpen: false, debug: false, disableFlip: false, disableHoverToClick: false, event: 'click', eventDelay: 0.4, hideArrow: false, offset: 15, placement: 'bottom', showCloseButton: false, styles: {}, target: null, wrapperOptions: { position: false, }, ...props, } satisfies SetRequired<Props, 'wrapperOptions'>; } export function getFallbackPlacements(placement: Placement): Placement[] | undefined { if (placement.startsWith('left')) { return ['top', 'bottom']; } if (placement.startsWith('right')) { return ['bottom', 'top']; } return undefined; } export function getModifiers(modifiers?: PopperModifiers): PopperModifiers { const defaultOptions = { flip: { name: 'flip', enabled: true, options: { padding: 20, }, }, preventOverflow: { name: 'preventOverflow', enabled: true, options: { padding: 10, }, }, }; return deepmerge(defaultOptions, modifiers ?? {}); } export function isFixed(el: HTMLElement | null): boolean { if (!el) { return false; } const { nodeName } = el; if (nodeName === 'BODY' || nodeName === 'HTML') { return false; } if (getComputedStyle(el).position === 'fixed') { return true; } return el.parentNode instanceof HTMLElement ? isFixed(el.parentNode) : false; } export function isMobile(): boolean { return 'ontouchstart' in window && /Mobi/.test(navigator.userAgent); } export function isValidElement(object: unknown): object is React.ReactElement { return React.isValidElement(object); } /** * Log method calls if debug is enabled */ export function log({ data, debug = false, title }: LogOptions): void { /* eslint-disable no-console */ const logFn = console.log; if (debug && title && data) { console.groupCollapsed( `%creact-floater: ${title}`, 'color: #9b00ff; font-weight: bold; font-size: 12px;', ); if (Array.isArray(data)) { data.forEach(d => { if (is.plainObject(d) && d.key) { logFn.apply(console, [d.key, d.value]); } else { logFn.apply(console, [d]); } }); } else { logFn.apply(console, [data]); } console.groupEnd(); } /* eslint-enable */ } export function mergeModifier( modifier: Partial<Modifier<any, any>>, customModifier?: Partial<Modifier<any, any>>, ): Partial<Modifier<any, any>> { return deepmerge(modifier, customModifier ?? {}); } export function off( element: Element, eventType: string, handler: EventListener, options: boolean | EventListenerOptions = false, ): void { element.removeEventListener(eventType, handler, options); } export function on( element: HTMLElement, eventType: string, handler: EventListener, options: boolean | EventListenerOptions = false, ): void { element.addEventListener(eventType, handler, options); } export function once( element: HTMLElement, eventType: string, handler: EventListener, options: boolean | EventListenerOptions = false, ): void { let nextCB: EventListener; // eslint-disable-next-line prefer-const nextCB = event => { handler(event); off(element, eventType, nextCB); }; on(element, eventType, nextCB, options); } export function randomId(): string { return `rf-${Math.round(Math.random() * 1e5)}`; }