UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

321 lines (320 loc) 10.4 kB
"use client"; import React, { useContext, useRef, useState, useCallback } from 'react'; import useMountEffect from "../../shared/helpers/useMountEffect.js"; import { useIsomorphicLayoutEffect } from "../../shared/helpers/useIsomorphicLayoutEffect.js"; import clsx from 'clsx'; import withComponentMarkers from "../../shared/helpers/withComponentMarkers.js"; import Context from "../../shared/Context.js"; import useId from "../../shared/helpers/useId.js"; import { warn, validateDOMAttributes, convertJsxToString, extendExistingPropsWithContext, extendDeep, detectOutsideClick, isTouchDevice, removeUndefinedProps } from "../../shared/component-helper.js"; import { hasSelectedText, IS_IOS } from "../../shared/helpers.js"; import { applySpacing } from "../space/SpacingUtils.js"; import { skeletonDOMAttributes, createSkeletonClass } from "../skeleton/SkeletonHelper.js"; import Tooltip, { injectTooltipSemantic } from "../tooltip/Tooltip.js"; import { runIOSSelectionFix, formatNumber, formatCurrency } from "./utils/index.js"; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; export const COPY_TOOLTIP_TIMEOUT = 3000; const numberFormatDefaultProps = { id: null, value: null, locale: null, prefix: null, suffix: null, currency: null, currencyDisplay: null, currencyPosition: null, compact: null, monospace: false, options: null, decimals: null, selectAll: true, alwaysSelectAll: false, copySelection: true, cleanCopyValue: false, rounding: null, clean: null, srLabel: null, element: 'span', tooltip: null, skeleton: null, className: null, children: null }; let hasiOSFix = false; function runFix(comp, className) { if (typeof comp === 'function') { comp = comp(); } if (React.isValidElement(comp)) { const elemProps = comp.props; return React.createElement(comp.type, { ...elemProps, className: clsx(elemProps.className, className) }); } return _jsx("span", { className: className, children: comp }); } function NumberFormat(ownProps) { const context = useContext(Context); const propsWithDefaults = { ...numberFormatDefaultProps, ...removeUndefinedProps({ ...ownProps }) }; const propsWithDefaultsRef = useRef(propsWithDefaults); propsWithDefaultsRef.current = propsWithDefaults; const elRef = useRef(null); const selectionRef = useRef(null); const generatedId = useId(propsWithDefaults.id); const copyTooltipTimeoutRef = useRef(null); const outsideClickRef = useRef(null); const cleanedValueRef = useRef(undefined); const [selected, setSelected] = useState(false); const [hover, setHover] = useState(false); const [copyTooltipActive, setCopyTooltipActive] = useState(false); const [copyTooltipText, setCopyTooltipText] = useState(null); const needsFocusRef = useRef(false); useMountEffect(() => { if (IS_IOS && !hasiOSFix) { hasiOSFix = true; runIOSSelectionFix(); } return () => { outsideClickRef.current?.remove(); if (copyTooltipTimeoutRef.current) { clearTimeout(copyTooltipTimeoutRef.current); } }; }); const clearCopyTooltipTimeout = useCallback(() => { if (copyTooltipTimeoutRef.current) { clearTimeout(copyTooltipTimeoutRef.current); copyTooltipTimeoutRef.current = null; } }, []); const onBlurHandler = useCallback(() => { setSelected(false); }, []); const doSelectAll = useCallback(() => { try { const elem = selectionRef.current || elRef.current; if (elem) { const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(elem); selection.removeAllRanges(); selection.addRange(range); } } catch (e) { warn(e); } }, []); const setFocus = useCallback(() => { if (isTouchDevice()) { return; } needsFocusRef.current = true; setSelected(true); }, []); useIsomorphicLayoutEffect(() => { if (selected && needsFocusRef.current) { needsFocusRef.current = false; selectionRef.current?.focus({ preventScroll: true }); doSelectAll(); if (!propsWithDefaults.copySelection) { outsideClickRef.current = detectOutsideClick(elRef.current, onBlurHandler); } } }, [selected, doSelectAll, onBlurHandler, propsWithDefaults.copySelection]); const showCopyTooltip = useCallback(message => { const translations = context.getTranslation?.(propsWithDefaultsRef.current)?.NumberFormat; const label = message || translations?.clipboardCopy; if (!label) { return; } clearCopyTooltipTimeout(); setCopyTooltipActive(true); setCopyTooltipText(label); copyTooltipTimeoutRef.current = setTimeout(() => { setCopyTooltipActive(false); }, COPY_TOOLTIP_TIMEOUT); }, [context, clearCopyTooltipTimeout]); const shortcutHandler = useCallback(() => { const label = context.getTranslation?.(propsWithDefaultsRef.current)?.NumberFormat?.clipboardCopy; showCopyTooltip(label); }, [context, showCopyTooltip]); const onContextMenuHandler = useCallback(() => { if (!hasSelectedText()) { setFocus(); } }, [setFocus]); const onClickHandler = useCallback(() => { if ((propsWithDefaults.selectAll || propsWithDefaults.alwaysSelectAll) && !hasSelectedText()) { setFocus(); } }, [propsWithDefaults.selectAll, propsWithDefaults.alwaysSelectAll, setFocus]); const onMouseEnter = useCallback(() => { setHover(true); }, []); const onMouseLeave = useCallback(() => { setHover(false); }, []); const translations = context.getTranslation?.(propsWithDefaults)?.NumberFormat; const props = extendExistingPropsWithContext(propsWithDefaults, numberFormatDefaultProps, translations, context?.NumberFormat); const { id, value: _value, prefix, suffix, children, currency, currencyDisplay, currencyPosition, compact, monospace, tooltip, skeleton, options, locale, decimals, rounding, signDisplay, clean, selectAll: selectAllProp, copySelection, cleanCopyValue, srLabel, element, className, alwaysSelectAll, __format, ..._rest } = props; let rest = _rest; let value = _value !== null && _value !== void 0 ? _value : null; if (value === null && children !== null) { value = children; } let usedCurrencyPosition = currencyPosition; if (currencyDisplay === 'code' && !usedCurrencyPosition) { usedCurrencyPosition = 'before'; } const formatOptions = { locale, currency, currencyDisplay, currencyPosition: usedCurrencyPosition, compact, decimals, rounding, signDisplay, options, clean: clean, cleanCopyValue: cleanCopyValue, returnAria: true, invalidAriaText: locale && locale !== context.locale ? null : translations?.notAvailable }; const useCtx = extendDeep({ locale: null, currency: null }, context); if (useCtx) { if (useCtx.locale && !locale) { formatOptions.locale = useCtx.locale; } if (useCtx.currency && currency === true) { formatOptions.options = formatOptions.options ? { ...formatOptions.options } : {}; formatOptions.options.currency = useCtx.currency; } } const formatter = __format !== null && __format !== void 0 ? __format : currency === true || typeof currency === 'string' ? formatCurrency : formatNumber; const result = formatter(value, formatOptions); const { cleanedValue, locale: lang } = result; let { aria } = result; let display = result.number; cleanedValueRef.current = cleanedValue; if (prefix) { display = _jsxs(_Fragment, { children: [runFix(prefix, 'dnb-number-format__prefix'), " ", display] }); aria = String(`${convertJsxToString(runFix(prefix, 'dnb-number-format__prefix'))} ${aria}`); } if (suffix) { const suffixElement = runFix(suffix, 'dnb-number-format__suffix'); const suffixStartsWithSlash = typeof suffix === 'string' && suffix.startsWith('/'); const suffixSpace = suffixStartsWithSlash ? '' : ' '; display = _jsxs(_Fragment, { children: [display, suffixSpace, suffixElement] }); aria = `${aria}${suffixSpace}${convertJsxToString(suffixElement)}`; } if (tooltip) { rest = injectTooltipSemantic(rest); } const attributes = applySpacing(ownProps, { lang, ref: elRef, className: clsx('dnb-number-format', className, (currency === true || typeof currency === 'string') && 'dnb-number-format--currency', selectAllProp && 'dnb-number-format--select-all', selected && 'dnb-number-format--selected', monospace && 'dnb-number-format--monospace'), onMouseEnter, onMouseLeave, ...rest }); const displayParams = {}; if (selectAllProp || copySelection) { displayParams.onClick = onClickHandler; displayParams.onContextMenu = onContextMenuHandler; } validateDOMAttributes(ownProps, attributes); skeletonDOMAttributes(attributes, skeleton, context); const Element = element; return _jsxs(Element, { ...attributes, children: [_jsx("span", { className: clsx('dnb-number-format__visible', createSkeletonClass('font', skeleton, context)), "aria-hidden": !hover, ...displayParams, children: display }), _jsx("span", { className: "dnb-sr-only", "data-text": srLabel ? `${convertJsxToString(srLabel)}${'\u00a0'}${aria}` : aria }), copySelection && _jsx("span", { className: "dnb-number-format__selection dnb-no-focus", ref: selectionRef, tabIndex: -1, onBlur: onBlurHandler, onCopy: shortcutHandler, "aria-hidden": true, children: selected && cleanedValue }), tooltip && _jsx(Tooltip, { id: generatedId + '-tooltip', targetElement: elRef, tooltip: tooltip }), copyTooltipActive && _jsx(Tooltip, { open: copyTooltipActive, targetElement: elRef, showDelay: 0, hideDelay: 0, triggerOffset: 8, children: copyTooltipText })] }); } const MemoizedNumberFormat = React.memo(NumberFormat); withComponentMarkers(MemoizedNumberFormat, { _supportsSpacingProps: true }); export default MemoizedNumberFormat; //# sourceMappingURL=NumberFormatBase.js.map