UNPKG

grommet

Version:

focus on the essential experience

356 lines (347 loc) 15.3 kB
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } /* eslint-disable no-nested-ternary */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { defaultSections, hasAnyValue, isoTimeToSections, normalizeToIsoTime, pad, sectionKey, sectionMax, sectionMin, sectionsToIsoTime, SECTION_HOUR, SECTION_MINUTE, SECTION_PERIOD, SECTION_SECOND } from './utils'; var separatorBeforeSection = function separatorBeforeSection(section) { return section === SECTION_PERIOD ? ' ' : ':'; }; var sectionToken = function sectionToken(section) { if (section === SECTION_HOUR) return 'hh'; if (section === SECTION_MINUTE) return 'mm'; if (section === SECTION_SECOND) return 'ss'; return 'aa'; }; var sectionPattern = function sectionPattern(section) { if (section === SECTION_PERIOD) return '(AM|PM)'; return '(\\d{1,2})'; }; var isNumericSection = function isNumericSection(section) { return section !== SECTION_PERIOD; }; var parseSectionsValue = function parseSectionsValue(value, format, sectionOrder) { if (!value || typeof value !== 'string') return undefined; var normalized = value.trim().toUpperCase(); if (!normalized || !sectionOrder.length) return undefined; var pattern = sectionOrder.map(function (section, index) { var sectionRegex = sectionPattern(section); if (index === 0) return sectionRegex; return separatorBeforeSection(section) + "\\s*" + sectionRegex; }).join(''); var regex = new RegExp("^" + pattern + "$"); var match = normalized.match(regex); if (!match) return undefined; var parsed = defaultSections(format); sectionOrder.forEach(function (section, index) { var key = sectionKey(section); var captured = match[index + 1]; if (section === SECTION_PERIOD) { parsed[key] = captured; } else { var next = Number(captured); if (!Number.isNaN(next)) parsed[key] = next; } }); for (var i = 0; i < sectionOrder.length; i += 1) { var section = sectionOrder[i]; if (isNumericSection(section)) { var key = sectionKey(section); var next = parsed[key]; if (next === undefined || next < sectionMin(section, format) || next > sectionMax(section, format)) { return undefined; } } } return parsed; }; var formatSectionsValue = function formatSectionsValue(_ref) { var sections = _ref.sections, sectionOrder = _ref.sectionOrder, _ref$includeTokens = _ref.includeTokens, includeTokens = _ref$includeTokens === void 0 ? false : _ref$includeTokens; return sectionOrder.map(function (section, index) { var key = sectionKey(section); var rawValue = sections[key]; var rendered = rawValue === undefined ? includeTokens ? sectionToken(section) : '' : section === SECTION_PERIOD ? rawValue : pad(rawValue); if (index === 0) return rendered; return "" + separatorBeforeSection(section) + rendered; }).join(''); }; export var useSectionedTimeField = function useSectionedTimeField(_ref2) { var format = _ref2.format, sectionOrder = _ref2.sectionOrder, _ref2$minuteStep = _ref2.minuteStep, minuteStep = _ref2$minuteStep === void 0 ? 1 : _ref2$minuteStep, value = _ref2.value, onCommit = _ref2.onCommit, onInvalid = _ref2.onInvalid; var editStateRef = useRef({ section: SECTION_HOUR, digits: 0, previousValue: undefined, firstDigit: undefined }); var preserveIncompleteSectionsRef = useRef(false); // Track pending single digit for display without committing var _useState = useState({}), pendingDigits = _useState[0], setPendingDigits = _useState[1]; var parsedValue = useMemo(function () { return isoTimeToSections(normalizeToIsoTime(value), format); }, [format, value]); var _useState2 = useState(parsedValue || defaultSections(format)), sections = _useState2[0], setSections = _useState2[1]; var _useState3 = useState(sectionOrder[0] || SECTION_HOUR), activeSection = _useState3[0], setActiveSection = _useState3[1]; useEffect(function () { if (!sectionOrder.includes(activeSection)) { setActiveSection(sectionOrder[0] || SECTION_HOUR); } }, [activeSection, sectionOrder]); useEffect(function () { if (!parsedValue && preserveIncompleteSectionsRef.current) { preserveIncompleteSectionsRef.current = false; return; } setSections(parsedValue || defaultSections(format)); }, [parsedValue, format]); var displayValue = useMemo(function () { if (!hasAnyValue(sections)) return ''; // Create a copy of sections with pending digits overlaid for display var displaySections = _extends({}, sections); var pendingSection = Object.keys(pendingDigits)[0]; if (pendingSection && pendingDigits[pendingSection] !== undefined) { displaySections[pendingSection] = pendingDigits[pendingSection]; } return formatSectionsValue({ sections: displaySections, sectionOrder: sectionOrder, includeTokens: true }); }, [sectionOrder, sections, pendingDigits]); var commitSections = useCallback(function (nextSections) { var _nextSections$second; setSections(nextSections); var complete = sectionOrder.every(function (section) { return nextSections[sectionKey(section)] !== undefined; }); // Check if all numeric sections are valid var allValid = sectionOrder.every(function (section) { if (!isNumericSection(section)) return true; // skip period var key = sectionKey(section); var sectionValue = nextSections[key]; var min = sectionMin(section, format); var max = sectionMax(section, format); return sectionValue >= min && sectionValue <= max; }); var nextValue = complete && sectionOrder.length && allValid ? sectionsToIsoTime(_extends({}, nextSections, { second: (_nextSections$second = nextSections.second) != null ? _nextSections$second : 0 }), format) : undefined; // Mark as incomplete if: sections incomplete OR some invalid var hasIncompleteSections = !complete || !allValid; preserveIncompleteSectionsRef.current = hasIncompleteSections && hasAnyValue(nextSections); onCommit(nextSections, nextValue); }, [onCommit, sectionOrder, format]); var setSectionValue = useCallback(function (section, rawValue) { var _extends2; var key = sectionKey(section); var next = _extends({}, sections, (_extends2 = {}, _extends2[key] = rawValue, _extends2)); commitSections(next); }, [commitSections, sections]); var moveSection = useCallback(function (direction) { if (!sectionOrder.length) return activeSection; var activeIndex = Math.max(0, sectionOrder.indexOf(activeSection)); var count = sectionOrder.length; var nextIndex = (activeIndex + direction + count) % count; var nextSection = sectionOrder[nextIndex]; setActiveSection(nextSection); return nextSection; }, [activeSection, sectionOrder]); var incrementSection = useCallback(function (section, delta) { if (section === SECTION_PERIOD) { setSectionValue(section, sections.period === 'AM' ? 'PM' : 'AM'); return; } var minValue = sectionMin(section, format); var maxValue = sectionMax(section, format); var key = sectionKey(section); var current = sections[key]; var step = section === SECTION_MINUTE ? minuteStep : section === SECTION_SECOND ? 1 : 1; var next; if (step > 1 && (section === SECTION_MINUTE || section === SECTION_SECOND)) { var options = Array.from({ length: Math.ceil((maxValue - minValue + 1) / step) }, function (_, index) { return minValue + index * step; }).filter(function (valueAtIndex) { return valueAtIndex <= maxValue; }); var currentIndex = options.indexOf(current); if (currentIndex !== -1) { var wrappedIndex = ((currentIndex + delta) % options.length + options.length) % options.length; next = options[wrappedIndex]; } else if (delta > 0) { var _options$find; next = (_options$find = options.find(function (option) { return option > (current != null ? current : minValue - 1); })) != null ? _options$find : options[0]; } else { var _descending$find; var descending = [].concat(options).reverse(); next = (_descending$find = descending.find(function (option) { return option < (current != null ? current : maxValue + 1); })) != null ? _descending$find : options[options.length - 1]; } } else { var base = current === undefined ? minValue : current; next = base + delta; if (next > maxValue) next = minValue; if (next < minValue) next = maxValue; } setSectionValue(section, next); }, [format, minuteStep, sections, setSectionValue]); var applyDigit = useCallback(function (digit) { if (activeSection === SECTION_PERIOD) return undefined; var minValue = sectionMin(activeSection, format); var maxValue = sectionMax(activeSection, format); var key = sectionKey(activeSection); var isSameSection = editStateRef.current.section === activeSection; var isSecondDigit = isSameSection && editStateRef.current.digits === 1; var currentRaw = sections[key] === undefined ? 0 : sections[key]; var nextValue; if (isSecondDigit) { var _editStateRef$current; // Use the stored first digit from editState var firstDigit = (_editStateRef$current = editStateRef.current.firstDigit) != null ? _editStateRef$current : currentRaw % 10; nextValue = firstDigit * 10 + digit; editStateRef.current = { section: activeSection, digits: 0, previousValue: editStateRef.current.previousValue, firstDigit: undefined }; // Clear pending digits since we're committing now setPendingDigits({}); } else { var _setPendingDigits; // First digit: store in pending for display only nextValue = digit; editStateRef.current = { section: activeSection, digits: 1, previousValue: sections[key], firstDigit: digit }; // Show the first digit in pending state setPendingDigits((_setPendingDigits = {}, _setPendingDigits[key] = digit, _setPendingDigits)); } // For 2nd digit: validate combined value, but allow invalid 1st digit // (allows "0X" patterns in formats where single "0" is invalid) if (isSecondDigit && (nextValue < minValue || nextValue > maxValue)) { var _editStateRef$current2; // Combined value invalid. Try to use first digit for current section // and apply second digit to next section (MUI behavior) var _firstDigit = (_editStateRef$current2 = editStateRef.current.firstDigit) != null ? _editStateRef$current2 : currentRaw % 10; if (_firstDigit >= minValue && _firstDigit <= maxValue) { setSectionValue(activeSection, _firstDigit); // Get next section and check if second digit is valid there var currentSectionIndex = sectionOrder.indexOf(activeSection); var nextSection = sectionOrder[currentSectionIndex + 1]; if (nextSection && isNumericSection(nextSection)) { var nextMinValue = sectionMin(nextSection, format); var nextMaxValue = sectionMax(nextSection, format); // If second digit is valid for next section, apply it there if (digit >= nextMinValue && digit <= nextMaxValue) { setSectionValue(nextSection, digit); // Now move to section after that return moveSection(2); } } // If second digit not valid for next section, just move to next return moveSection(1); } // If even first digit alone is invalid, reject the entry onInvalid == null || onInvalid(); setPendingDigits({}); return undefined; } // MUI Pattern: Determine if we should commit and move to next section // shouldGoToNextSection = nextValue * 10 > max || digitCount === 2 var shouldMoveToNextSection = nextValue * 10 > maxValue || isSecondDigit; setSectionValue(activeSection, nextValue); if (shouldMoveToNextSection) { // Don't move past the last section during digit entry // (Tab navigation can still wrap, but digit entry stops at last // section) var currentIndex = sectionOrder.indexOf(activeSection); var isLastSection = currentIndex === sectionOrder.length - 1; if (!isLastSection) { return moveSection(1); } } return activeSection; }, [activeSection, format, moveSection, onInvalid, sections, setSectionValue, setPendingDigits, sectionOrder]); var clearActiveSection = useCallback(function () { setSectionValue(activeSection, undefined); }, [activeSection, setSectionValue]); var parsePasted = useCallback(function (pasted) { var parsed = parseSectionsValue(pasted, format, sectionOrder); if (parsed) { return parsed; } var digits = pasted.replace(/\D/g, ''); var numericSections = sectionOrder.filter(function (section) { return section !== SECTION_PERIOD; }); var hasExplicitPeriod = /\b(AM|PM)\b/i.test(pasted); var shouldInferPeriod = format === '12' && sectionOrder.includes(SECTION_PERIOD) && !hasExplicitPeriod; if (digits.length >= numericSections.length * 2 && numericSections.length) { var next = _extends({}, sections); for (var i = 0; i < numericSections.length; i += 1) { var section = numericSections[i]; var parsedNumber = Number(digits.slice(i * 2, i * 2 + 2)); var minValue = section === SECTION_HOUR && shouldInferPeriod ? 0 : sectionMin(section, format); var maxValue = section === SECTION_HOUR && shouldInferPeriod ? 23 : sectionMax(section, format); if (Number.isNaN(parsedNumber) || parsedNumber < minValue || parsedNumber > maxValue) { return undefined; } next[sectionKey(section)] = parsedNumber; } if (sectionOrder.includes(SECTION_PERIOD)) { if (hasExplicitPeriod) { next.period = /\bPM\b/i.test(pasted) ? 'PM' : 'AM'; } else if (shouldInferPeriod) { var normalizedHour = next.hour; if (normalizedHour === 0) { next.hour = 12; next.period = 'AM'; } else if (normalizedHour === 12) { next.period = 'PM'; } else if (normalizedHour > 12) { next.hour = normalizedHour - 12; next.period = 'PM'; } else { next.period = 'AM'; } } } return next; } return undefined; }, [format, sectionOrder, sections]); return { activeSection: activeSection, clearActiveSection: clearActiveSection, displayValue: displayValue, incrementSection: incrementSection, moveSection: moveSection, parsePasted: parsePasted, sections: sections, setActiveSection: setActiveSection, setSectionValue: setSectionValue, applyDigit: applyDigit, commitSections: commitSections, pendingDigits: pendingDigits }; };