@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
321 lines (320 loc) • 10.4 kB
JavaScript
"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