@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
300 lines (299 loc) • 11.6 kB
JavaScript
"use client";
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import clsx from 'clsx';
import withComponentMarkers from "../../../shared/helpers/withComponentMarkers.js";
import useId from "../../../shared/helpers/useId.js";
import Input from "../../Input.js";
import FormLabel from "../../FormLabel.js";
import { applySpacing } from "../../space/SpacingUtils.js";
import { useSegmentedFieldValues } from "../hooks/useSegmentedFieldValues.js";
import SegmentedFieldSection from "./SegmentedFieldSection.js";
import { ensureTextNode, listAllSections } from "./dom.js";
import { joinValues } from "./utils.js";
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
function SegmentedField(props) {
const fallbackId = useId(props === null || props === void 0 ? void 0 : props.id);
const fallbackFieldsetRef = useRef(null);
const {
id = fallbackId,
label,
labelDirection = 'vertical',
inputs,
delimiter,
onChange: onChangeExternal,
disabled,
status,
statusState,
values: defaultValues,
className,
stretch,
_omitInputShellClass,
scopeRef = fallbackFieldsetRef,
size,
suffix,
onBlur,
onFocus,
overwriteMode = 'shift',
optionsEnhancer,
...rest
} = props;
const hasExternalScopeRef = Boolean(props.scopeRef);
const [values, onChangeBase] = useSegmentedFieldValues({
inputs,
defaultValues
});
const valuesRef = useRef(values);
const sectionRefs = useRef({});
const caretPositionsRef = useRef({});
const sectionSelectionModeRef = useRef({});
const areInputsInFocus = useRef(false);
const [wholeGroupSelectionUi, setWholeGroupSelectionUi] = useState(false);
const wholeGroupSelectionUiRef = useRef(false);
wholeGroupSelectionUiRef.current = wholeGroupSelectionUi;
valuesRef.current = values;
useEffect(() => {
optionsEnhancer === null || optionsEnhancer === void 0 || optionsEnhancer({
overwriteMode
});
}, [optionsEnhancer, overwriteMode]);
const onChange = useCallback((inputId, value) => {
const updatedValues = {
...valuesRef.current,
[inputId]: value
};
valuesRef.current = updatedValues;
onChangeBase(updatedValues);
if (typeof onChangeExternal === 'function') {
onChangeExternal(updatedValues);
}
}, [onChangeBase, onChangeExternal]);
const clearGroupSelection = useCallback(() => {
if (wholeGroupSelectionUiRef.current) {
flushSync(() => {
setWholeGroupSelectionUi(false);
});
}
}, []);
const clearSectionSelection = useCallback(() => {
const selection = window.getSelection();
selection === null || selection === void 0 || selection.removeAllRanges();
inputs.forEach(({
id
}) => {
var _section$textContent$, _section$textContent;
const inputId = String(id);
const section = sectionRefs.current[inputId];
const length = (_section$textContent$ = section === null || section === void 0 || (_section$textContent = section.textContent) === null || _section$textContent === void 0 ? void 0 : _section$textContent.length) !== null && _section$textContent$ !== void 0 ? _section$textContent$ : 0;
sectionSelectionModeRef.current[inputId] = 'caret';
caretPositionsRef.current[inputId] = length;
});
}, [inputs]);
const selectSection = useCallback(inputId => {
const section = sectionRefs.current[inputId];
if (!section) {
return;
}
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(section);
selection === null || selection === void 0 || selection.removeAllRanges();
selection === null || selection === void 0 || selection.addRange(range);
sectionSelectionModeRef.current[inputId] = 'all';
caretPositionsRef.current[inputId] = 0;
}, []);
const setSectionCaret = useCallback((inputId, position) => {
var _section$textContent$2, _section$textContent2;
const section = sectionRefs.current[inputId];
if (!section) {
return;
}
const safePosition = Math.max(0, Math.min(position, (_section$textContent$2 = (_section$textContent2 = section.textContent) === null || _section$textContent2 === void 0 ? void 0 : _section$textContent2.length) !== null && _section$textContent$2 !== void 0 ? _section$textContent$2 : 0));
const textNode = ensureTextNode(section);
if (!textNode) {
return;
}
const selection = window.getSelection();
const range = document.createRange();
range.setStart(textNode, safePosition);
range.collapse(true);
selection === null || selection === void 0 || selection.removeAllRanges();
selection === null || selection === void 0 || selection.addRange(range);
sectionSelectionModeRef.current[inputId] = 'caret';
caretPositionsRef.current[inputId] = safePosition;
}, []);
const selectWholeGroup = useCallback(targetInputId => {
var _lastTextNode$textCon, _lastTextNode$textCon2;
const currentSection = sectionRefs.current[targetInputId];
const currentGroup = currentSection === null || currentSection === void 0 ? void 0 : currentSection.closest('.dnb-segmented-field__group');
const sections = listAllSections(currentGroup || undefined);
if (sections.length === 0) {
return;
}
const firstSection = sections[0];
const lastSection = sections[sections.length - 1];
if (!firstSection || !lastSection) {
return;
}
flushSync(() => {
setWholeGroupSelectionUi(true);
});
const firstTextNode = ensureTextNode(firstSection);
const lastTextNode = ensureTextNode(lastSection);
if (!firstTextNode || !lastTextNode) {
clearGroupSelection();
return;
}
const selection = window.getSelection();
const range = document.createRange();
range.setStart(firstTextNode, 0);
range.setEnd(lastTextNode, (_lastTextNode$textCon = (_lastTextNode$textCon2 = lastTextNode.textContent) === null || _lastTextNode$textCon2 === void 0 ? void 0 : _lastTextNode$textCon2.length) !== null && _lastTextNode$textCon !== void 0 ? _lastTextNode$textCon : 0);
selection === null || selection === void 0 || selection.removeAllRanges();
selection === null || selection === void 0 || selection.addRange(range);
sections.forEach(section => {
const sectionId = section.dataset.segmentedInputId;
if (!sectionId) {
return;
}
sectionSelectionModeRef.current[sectionId] = 'all';
caretPositionsRef.current[sectionId] = 0;
});
}, [clearGroupSelection, caretPositionsRef, sectionRefs, sectionSelectionModeRef]);
const focusSection = useCallback((inputId, mode) => {
var _section$textContent3;
const section = sectionRefs.current[inputId];
if (!section) {
return;
}
section.focus();
if (mode === 'all') {
selectSection(inputId);
return;
}
const displayValue = (_section$textContent3 = section.textContent) !== null && _section$textContent3 !== void 0 ? _section$textContent3 : '';
setSectionCaret(inputId, mode === 'end' ? displayValue.length : 0);
}, [selectSection, setSectionCaret]);
const focusFirstSection = useCallback(event => {
var _inputs$;
const firstId = (_inputs$ = inputs[0]) === null || _inputs$ === void 0 ? void 0 : _inputs$.id;
if (disabled || !firstId) {
return;
}
focusSection(String(firstId), 'all');
}, [disabled, focusSection, inputs]);
const onLegendClick = useCallback(() => {
focusFirstSection();
}, [focusFirstSection]);
const WrapperElement = label ? 'fieldset' : 'div';
const hiddenInputValue = joinValues(values, delimiter);
const inputElement = _jsxs(_Fragment, {
children: [_jsx("div", {
className: "dnb-segmented-field__group",
role: "group",
"data-segmented-selection": wholeGroupSelectionUi ? 'all' : undefined,
children: inputs.map(({
id: inputId,
onFocus: _a,
onBlur: _b,
...itemProps
}, index) => {
var _values$inputId;
return _jsx(SegmentedFieldSection, {
groupId: id,
inputId: String(inputId),
itemProps: itemProps,
value: String((_values$inputId = values[inputId]) !== null && _values$inputId !== void 0 ? _values$inputId : ''),
overwriteMode: overwriteMode,
delimiter: index !== inputs.length - 1 ? delimiter : undefined,
groupDelimiter: delimiter,
disabled: Boolean(disabled),
valuesRef: valuesRef,
inputs: inputs.map(({
id,
mask
}) => ({
id: String(id),
mask
})),
scopeRef: scopeRef,
sectionRefs: sectionRefs,
caretPositionsRef: caretPositionsRef,
sectionSelectionModeRef: sectionSelectionModeRef,
wholeGroupSelectionUi: wholeGroupSelectionUi,
clearGroupSelection: clearGroupSelection,
clearSectionSelection: clearSectionSelection,
selectWholeGroup: selectWholeGroup,
selectSection: selectSection,
setSectionCaret: setSectionCaret,
focusSection: focusSection,
onChange: onChange,
onGroupFocus: () => {
if (!areInputsInFocus.current) {
onFocus === null || onFocus === void 0 || onFocus(valuesRef.current);
}
areInputsInFocus.current = true;
},
onGroupBlur: event => {
var _event$relatedTarget;
if (!((_event$relatedTarget = event.relatedTarget) !== null && _event$relatedTarget !== void 0 && (_event$relatedTarget = _event$relatedTarget.id) !== null && _event$relatedTarget !== void 0 && _event$relatedTarget.startsWith(`${id}-`))) {
const run = () => onBlur === null || onBlur === void 0 ? void 0 : onBlur(valuesRef.current);
window.requestAnimationFrame(run);
areInputsInFocus.current = false;
clearGroupSelection();
clearSectionSelection();
}
},
...rest
}, String(inputId));
})
}), _jsx("input", {
id: id,
className: "dnb-segmented-field__hidden-input dnb-sr-only",
value: hiddenInputValue,
onFocus: focusFirstSection,
readOnly: true,
tabIndex: -1,
"aria-hidden": true
})]
});
const labelElement = label && _jsx(FormLabel, {
element: "legend",
forId: id,
disabled: disabled,
labelDirection: labelDirection,
onClick: onLegendClick,
children: label
});
const wrapperProps = applySpacing(rest, {
ref: element => {
if (!hasExternalScopeRef && !scopeRef.current) {
scopeRef.current = element;
}
},
className: 'dnb-segmented-field__fieldset' + (labelDirection === 'horizontal' ? " dnb-segmented-field__fieldset--horizontal" : "")
});
return _jsx(WrapperElement, {
...wrapperProps,
children: _jsx(Input, {
...rest,
id: id,
label: labelElement,
className: clsx('dnb-segmented-field', className),
size: size,
labelDirection: labelDirection,
disabled: disabled,
status: status,
statusState: statusState,
suffix: suffix,
stretch: stretch,
inputElement: inputElement,
_omitInputShellClass: _omitInputShellClass
})
});
}
export default SegmentedField;
withComponentMarkers(SegmentedField, {
_formElement: true,
_supportsSpacingProps: true
});
//# sourceMappingURL=SegmentedField.js.map