1
0
Fork 0

Added Statistics calculation

Statistics now show calculated values
This commit is contained in:
Techognito 2025-09-04 17:30:00 +02:00
parent fe87374e47
commit fc0f69dacb
2147 changed files with 141321 additions and 39 deletions

View file

@ -0,0 +1,15 @@
import { FieldSection, MuiPickersAdapter, PickerValidDate } from "../../../models/index.js";
import { PickersLocaleText } from "../../../locales/index.js";
interface BuildSectionsFromFormatParameters {
adapter: MuiPickersAdapter;
format: string;
formatDensity: 'dense' | 'spacious';
isRtl: boolean;
shouldRespectLeadingZeros: boolean;
localeText: PickersLocaleText;
localizedDigits: string[];
date: PickerValidDate | null;
enableAccessibleFieldDOMStructure: boolean;
}
export declare const buildSectionsFromFormat: (parameters: BuildSectionsFromFormatParameters) => FieldSection[];
export {};

View file

@ -0,0 +1,263 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.buildSectionsFromFormat = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _useField = require("./useField.utils");
const expandFormat = ({
adapter,
format
}) => {
// Expand the provided format
let formatExpansionOverflow = 10;
let prevFormat = format;
let nextFormat = adapter.expandFormat(format);
while (nextFormat !== prevFormat) {
prevFormat = nextFormat;
nextFormat = adapter.expandFormat(prevFormat);
formatExpansionOverflow -= 1;
if (formatExpansionOverflow < 0) {
throw new Error('MUI X: The format expansion seems to be in an infinite loop. Please open an issue with the format passed to the component.');
}
}
return nextFormat;
};
const getEscapedPartsFromFormat = ({
adapter,
expandedFormat
}) => {
const escapedParts = [];
const {
start: startChar,
end: endChar
} = adapter.escapedCharacters;
const regExp = new RegExp(`(\\${startChar}[^\\${endChar}]*\\${endChar})+`, 'g');
let match = null;
// eslint-disable-next-line no-cond-assign
while (match = regExp.exec(expandedFormat)) {
escapedParts.push({
start: match.index,
end: regExp.lastIndex - 1
});
}
return escapedParts;
};
const getSectionPlaceholder = (adapter, localeText, sectionConfig, sectionFormat) => {
switch (sectionConfig.type) {
case 'year':
{
return localeText.fieldYearPlaceholder({
digitAmount: adapter.formatByString(adapter.date(undefined, 'default'), sectionFormat).length,
format: sectionFormat
});
}
case 'month':
{
return localeText.fieldMonthPlaceholder({
contentType: sectionConfig.contentType,
format: sectionFormat
});
}
case 'day':
{
return localeText.fieldDayPlaceholder({
format: sectionFormat
});
}
case 'weekDay':
{
return localeText.fieldWeekDayPlaceholder({
contentType: sectionConfig.contentType,
format: sectionFormat
});
}
case 'hours':
{
return localeText.fieldHoursPlaceholder({
format: sectionFormat
});
}
case 'minutes':
{
return localeText.fieldMinutesPlaceholder({
format: sectionFormat
});
}
case 'seconds':
{
return localeText.fieldSecondsPlaceholder({
format: sectionFormat
});
}
case 'meridiem':
{
return localeText.fieldMeridiemPlaceholder({
format: sectionFormat
});
}
default:
{
return sectionFormat;
}
}
};
const createSection = ({
adapter,
date,
shouldRespectLeadingZeros,
localeText,
localizedDigits,
now,
token,
startSeparator
}) => {
if (token === '') {
throw new Error('MUI X: Should not call `commitToken` with an empty token');
}
const sectionConfig = (0, _useField.getDateSectionConfigFromFormatToken)(adapter, token);
const hasLeadingZerosInFormat = (0, _useField.doesSectionFormatHaveLeadingZeros)(adapter, sectionConfig.contentType, sectionConfig.type, token);
const hasLeadingZerosInInput = shouldRespectLeadingZeros ? hasLeadingZerosInFormat : sectionConfig.contentType === 'digit';
const isValidDate = adapter.isValid(date);
let sectionValue = isValidDate ? adapter.formatByString(date, token) : '';
let maxLength = null;
if (hasLeadingZerosInInput) {
if (hasLeadingZerosInFormat) {
maxLength = sectionValue === '' ? adapter.formatByString(now, token).length : sectionValue.length;
} else {
if (sectionConfig.maxLength == null) {
throw new Error(`MUI X: The token ${token} should have a 'maxLength' property on it's adapter`);
}
maxLength = sectionConfig.maxLength;
if (isValidDate) {
sectionValue = (0, _useField.applyLocalizedDigits)((0, _useField.cleanLeadingZeros)((0, _useField.removeLocalizedDigits)(sectionValue, localizedDigits), maxLength), localizedDigits);
}
}
}
return (0, _extends2.default)({}, sectionConfig, {
format: token,
maxLength,
value: sectionValue,
placeholder: getSectionPlaceholder(adapter, localeText, sectionConfig, token),
hasLeadingZerosInFormat,
hasLeadingZerosInInput,
startSeparator,
endSeparator: '',
modified: false
});
};
const buildSections = parameters => {
const {
adapter,
expandedFormat,
escapedParts
} = parameters;
const now = adapter.date(undefined);
const sections = [];
let startSeparator = '';
// This RegExp tests if the beginning of a string corresponds to a supported token
const validTokens = Object.keys(adapter.formatTokenMap).sort((a, b) => b.length - a.length); // Sort to put longest word first
const regExpFirstWordInFormat = /^([a-zA-Z]+)/;
const regExpWordOnlyComposedOfTokens = new RegExp(`^(${validTokens.join('|')})*$`);
const regExpFirstTokenInWord = new RegExp(`^(${validTokens.join('|')})`);
const getEscapedPartOfCurrentChar = i => escapedParts.find(escapeIndex => escapeIndex.start <= i && escapeIndex.end >= i);
let i = 0;
while (i < expandedFormat.length) {
const escapedPartOfCurrentChar = getEscapedPartOfCurrentChar(i);
const isEscapedChar = escapedPartOfCurrentChar != null;
const firstWordInFormat = regExpFirstWordInFormat.exec(expandedFormat.slice(i))?.[1];
// The first word in the format is only composed of tokens.
// We extract those tokens to create a new sections.
if (!isEscapedChar && firstWordInFormat != null && regExpWordOnlyComposedOfTokens.test(firstWordInFormat)) {
let word = firstWordInFormat;
while (word.length > 0) {
const firstWord = regExpFirstTokenInWord.exec(word)[1];
word = word.slice(firstWord.length);
sections.push(createSection((0, _extends2.default)({}, parameters, {
now,
token: firstWord,
startSeparator
})));
startSeparator = '';
}
i += firstWordInFormat.length;
}
// The remaining format does not start with a token,
// We take the first character and add it to the current section's end separator.
else {
const char = expandedFormat[i];
// If we are on the opening or closing character of an escaped part of the format,
// Then we ignore this character.
const isEscapeBoundary = isEscapedChar && escapedPartOfCurrentChar?.start === i || escapedPartOfCurrentChar?.end === i;
if (!isEscapeBoundary) {
if (sections.length === 0) {
startSeparator += char;
} else {
sections[sections.length - 1].endSeparator += char;
sections[sections.length - 1].isEndFormatSeparator = true;
}
}
i += 1;
}
}
if (sections.length === 0 && startSeparator.length > 0) {
sections.push({
type: 'empty',
contentType: 'letter',
maxLength: null,
format: '',
value: '',
placeholder: '',
hasLeadingZerosInFormat: false,
hasLeadingZerosInInput: false,
startSeparator,
endSeparator: '',
modified: false
});
}
return sections;
};
const postProcessSections = ({
isRtl,
formatDensity,
sections
}) => {
return sections.map(section => {
const cleanSeparator = separator => {
let cleanedSeparator = separator;
if (isRtl && cleanedSeparator !== null && cleanedSeparator.includes(' ')) {
cleanedSeparator = `\u2069${cleanedSeparator}\u2066`;
}
if (formatDensity === 'spacious' && ['/', '.', '-'].includes(cleanedSeparator)) {
cleanedSeparator = ` ${cleanedSeparator} `;
}
return cleanedSeparator;
};
section.startSeparator = cleanSeparator(section.startSeparator);
section.endSeparator = cleanSeparator(section.endSeparator);
return section;
});
};
const buildSectionsFromFormat = parameters => {
let expandedFormat = expandFormat(parameters);
if (parameters.isRtl && parameters.enableAccessibleFieldDOMStructure) {
expandedFormat = expandedFormat.split(' ').reverse().join(' ');
}
const escapedParts = getEscapedPartsFromFormat((0, _extends2.default)({}, parameters, {
expandedFormat
}));
const sections = buildSections((0, _extends2.default)({}, parameters, {
expandedFormat,
escapedParts
}));
return postProcessSections((0, _extends2.default)({}, parameters, {
sections
}));
};
exports.buildSectionsFromFormat = buildSectionsFromFormat;

View file

@ -0,0 +1,4 @@
export { useField } from "./useField.js";
export type { UseFieldInternalProps, UseFieldParameters, UseFieldReturnValue, UseFieldProps, FieldValueManager, FieldChangeHandler, FieldChangeHandlerContext } from "./useField.types.js";
export { createDateStrForV7HiddenInputFromSections, createDateStrForV6InputFromSections } from "./useField.utils.js";
export { useFieldInternalPropsWithDefaults } from "./useFieldInternalPropsWithDefaults.js";

View file

@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "createDateStrForV6InputFromSections", {
enumerable: true,
get: function () {
return _useField2.createDateStrForV6InputFromSections;
}
});
Object.defineProperty(exports, "createDateStrForV7HiddenInputFromSections", {
enumerable: true,
get: function () {
return _useField2.createDateStrForV7HiddenInputFromSections;
}
});
Object.defineProperty(exports, "useField", {
enumerable: true,
get: function () {
return _useField.useField;
}
});
Object.defineProperty(exports, "useFieldInternalPropsWithDefaults", {
enumerable: true,
get: function () {
return _useFieldInternalPropsWithDefaults.useFieldInternalPropsWithDefaults;
}
});
var _useField = require("./useField");
var _useField2 = require("./useField.utils");
var _useFieldInternalPropsWithDefaults = require("./useFieldInternalPropsWithDefaults");

View file

@ -0,0 +1,9 @@
import { PickerValidValue } from "../../models/index.js";
import { UseFieldDOMGetters } from "./useField.types.js";
import { UseFieldStateReturnValue } from "./useFieldState.js";
export declare function syncSelectionToDOM<TValue extends PickerValidValue>(parameters: SyncSelectionToDOMParameters<TValue>): void;
export interface SyncSelectionToDOMParameters<TValue extends PickerValidValue> {
domGetters: UseFieldDOMGetters;
stateResponse: UseFieldStateReturnValue<TValue>;
focused: boolean;
}

View file

@ -0,0 +1,60 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.syncSelectionToDOM = syncSelectionToDOM;
var _ownerDocument = _interopRequireDefault(require("@mui/utils/ownerDocument"));
var _utils = require("../../utils/utils");
function syncSelectionToDOM(parameters) {
const {
focused,
domGetters,
stateResponse: {
// States and derived states
parsedSelectedSections,
state
}
} = parameters;
if (!domGetters.isReady()) {
return;
}
const selection = (0, _ownerDocument.default)(domGetters.getRoot()).getSelection();
if (!selection) {
return;
}
if (parsedSelectedSections == null) {
// If the selection contains an element inside the field, we reset it.
if (selection.rangeCount > 0 &&
// Firefox can return a Restricted object here
selection.getRangeAt(0).startContainer instanceof Node && domGetters.getRoot().contains(selection.getRangeAt(0).startContainer)) {
selection.removeAllRanges();
}
if (focused) {
domGetters.getRoot().blur();
}
return;
}
// On multi input range pickers we want to update selection range only for the active input
if (!domGetters.getRoot().contains((0, _utils.getActiveElement)(domGetters.getRoot()))) {
return;
}
const range = new window.Range();
let target;
if (parsedSelectedSections === 'all') {
target = domGetters.getRoot();
} else {
const section = state.sections[parsedSelectedSections];
if (section.type === 'empty') {
target = domGetters.getSectionContainer(parsedSelectedSections);
} else {
target = domGetters.getSectionContent(parsedSelectedSections);
}
}
range.selectNodeContents(target);
target.focus();
selection.removeAllRanges();
selection.addRange(range);
}

View file

@ -0,0 +1,3 @@
import { UseFieldParameters, UseFieldReturnValue, UseFieldProps } from "./useField.types.js";
import { PickerValidValue } from "../../models/index.js";
export declare const useField: <TValue extends PickerValidValue, TEnableAccessibleFieldDOMStructure extends boolean, TError, TValidationProps extends {}, TProps extends UseFieldProps<TEnableAccessibleFieldDOMStructure>>(parameters: UseFieldParameters<TValue, TEnableAccessibleFieldDOMStructure, TError, TValidationProps, TProps>) => UseFieldReturnValue<TEnableAccessibleFieldDOMStructure, TProps>;

View file

@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useField = void 0;
var _useFieldV7TextField = require("./useFieldV7TextField");
var _useFieldV6TextField = require("./useFieldV6TextField");
var _useNullableFieldPrivateContext = require("../useNullableFieldPrivateContext");
const useField = parameters => {
const fieldPrivateContext = (0, _useNullableFieldPrivateContext.useNullableFieldPrivateContext)();
const enableAccessibleFieldDOMStructure = parameters.props.enableAccessibleFieldDOMStructure ?? fieldPrivateContext?.enableAccessibleFieldDOMStructure ?? true;
const useFieldTextField = enableAccessibleFieldDOMStructure ? _useFieldV7TextField.useFieldV7TextField : _useFieldV6TextField.useFieldV6TextField;
return useFieldTextField(parameters);
};
exports.useField = useField;

View file

@ -0,0 +1,329 @@
import * as React from 'react';
import { FieldSectionType, FieldSection, FieldSelectedSections, MuiPickersAdapter, TimezoneProps, FieldSectionContentType, PickerValidDate, FieldRef, OnErrorProps, InferFieldSection, PickerManager, PickerValueType } from "../../../models/index.js";
import { InternalPropNames } from "../../../hooks/useSplitFieldProps.js";
import type { PickersSectionElement, PickersSectionListRef } from "../../../PickersSectionList/index.js";
import { FormProps, InferNonNullablePickerValue, PickerRangeValue, PickerValidValue } from "../../models/index.js";
export interface UseFieldParameters<TValue extends PickerValidValue, TEnableAccessibleFieldDOMStructure extends boolean, TError, TValidationProps extends {}, TProps extends UseFieldProps<TEnableAccessibleFieldDOMStructure>> {
manager: PickerManager<TValue, TEnableAccessibleFieldDOMStructure, TError, TValidationProps, any>;
props: TProps;
skipContextFieldRefAssignment?: boolean;
}
export interface UseFieldInternalProps<TValue extends PickerValidValue, TEnableAccessibleFieldDOMStructure extends boolean, TError> extends TimezoneProps, FormProps, OnErrorProps<TValue, TError> {
/**
* The selected value.
* Used when the component is controlled.
*/
value?: TValue;
/**
* The default value. Use when the component is not controlled.
*/
defaultValue?: TValue;
/**
* The date used to generate a part of the new value that is not present in the format when both `value` and `defaultValue` are empty.
* For example, on time fields it will be used to determine the date to set.
* @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. Value is rounded to the most granular section used.
*/
referenceDate?: TValue extends PickerRangeValue ? TValue | PickerValidDate : PickerValidDate;
/**
* Callback fired when the value changes.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @template TError The validation error type. It will be either `string` or a `null`. It can be in `[start, end]` format in case of range value.
* @param {TValue} value The new value.
* @param {FieldChangeHandlerContext<TError>} context The context containing the validation result of the current value.
*/
onChange?: FieldChangeHandler<TValue, TError>;
/**
* Format of the date when rendered in the input(s).
*/
format: string;
/**
* Density of the format when rendered in the input.
* Setting `formatDensity` to `"spacious"` will add a space before and after each `/`, `-` and `.` character.
* @default "dense"
*/
formatDensity?: 'dense' | 'spacious';
/**
* If `true`, the format will respect the leading zeroes (for example on dayjs, the format `M/D/YYYY` will render `8/16/2018`)
* If `false`, the format will always add leading zeroes (for example on dayjs, the format `M/D/YYYY` will render `08/16/2018`)
*
* Warning n°1: Luxon is not able to respect the leading zeroes when using macro tokens (for example "DD"), so `shouldRespectLeadingZeros={true}` might lead to inconsistencies when using `AdapterLuxon`.
*
* Warning n°2: When `shouldRespectLeadingZeros={true}`, the field will add an invisible character on the sections containing a single digit to make sure `onChange` is fired.
* If you need to get the clean value from the input, you can remove this character using `input.value.replace(/\u200e/g, '')`.
*
* Warning n°3: When used in strict mode, dayjs and moment require to respect the leading zeros.
* This mean that when using `shouldRespectLeadingZeros={false}`, if you retrieve the value directly from the input (not listening to `onChange`) and your format contains tokens without leading zeros, the value will not be parsed by your library.
*
* @default false
*/
shouldRespectLeadingZeros?: boolean;
/**
* The currently selected sections.
* This prop accepts four formats:
* 1. If a number is provided, the section at this index will be selected.
* 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected.
* 3. If `"all"` is provided, all the sections will be selected.
* 4. If `null` is provided, no section will be selected.
* If not provided, the selected sections will be handled internally.
*/
selectedSections?: FieldSelectedSections;
/**
* Callback fired when the selected sections change.
* @param {FieldSelectedSections} newValue The new selected sections.
*/
onSelectedSectionsChange?: (newValue: FieldSelectedSections) => void;
/**
* The ref object used to imperatively interact with the field.
*/
unstableFieldRef?: React.Ref<FieldRef<TValue>>;
/**
* @default true
*/
enableAccessibleFieldDOMStructure?: TEnableAccessibleFieldDOMStructure;
/**
* If `true`, the `input` element is focused during the first mount.
* @default false
*/
autoFocus?: boolean;
/**
* If `true`, the component is displayed in focused state.
*/
focused?: boolean;
}
export type UseFieldForwardedProps<TEnableAccessibleFieldDOMStructure extends boolean> = TEnableAccessibleFieldDOMStructure extends false ? {
clearable?: boolean;
error?: boolean;
placeholder?: string;
inputRef?: React.Ref<HTMLInputElement>;
onClick?: React.MouseEventHandler;
onFocus?: React.FocusEventHandler;
onKeyDown?: React.KeyboardEventHandler;
onBlur?: React.FocusEventHandler;
onPaste?: React.ClipboardEventHandler<HTMLDivElement>;
onClear?: React.MouseEventHandler;
} : {
clearable?: boolean;
error?: boolean;
focused?: boolean;
sectionListRef?: React.Ref<PickersSectionListRef>;
onClick?: React.MouseEventHandler;
onKeyDown?: React.KeyboardEventHandler;
onFocus?: React.FocusEventHandler;
onBlur?: React.FocusEventHandler;
onInput?: React.FormEventHandler<HTMLDivElement>;
onPaste?: React.ClipboardEventHandler<HTMLDivElement>;
onClear?: React.MouseEventHandler;
};
type UseFieldAdditionalProps<TEnableAccessibleFieldDOMStructure extends boolean> = TEnableAccessibleFieldDOMStructure extends false ? {
/**
* The aria label to set on the button that opens the Picker.
*/
openPickerAriaLabel: string;
enableAccessibleFieldDOMStructure: false;
focused: boolean | undefined;
inputMode: 'text' | 'numeric';
placeholder: string;
value: string;
onChange: React.ChangeEventHandler<HTMLInputElement>;
autoComplete: 'off';
} : {
/**
* The aria label to set on the button that opens the Picker.
*/
openPickerAriaLabel: string;
enableAccessibleFieldDOMStructure: true;
elements: PickersSectionElement[];
tabIndex: number | undefined;
contentEditable: boolean;
value: string;
onChange: React.ChangeEventHandler<HTMLInputElement>;
areAllSectionsEmpty: boolean;
focused: boolean;
};
export type UseFieldReturnValue<TEnableAccessibleFieldDOMStructure extends boolean, TProps extends UseFieldProps<TEnableAccessibleFieldDOMStructure>> = Required<Pick<UseFieldInternalProps<any, any, any>, 'disabled' | 'readOnly' | 'autoFocus'>> & Required<UseFieldForwardedProps<TEnableAccessibleFieldDOMStructure>> & UseFieldAdditionalProps<TEnableAccessibleFieldDOMStructure> & Omit<TProps, InternalPropNames<PickerValueType>>;
export type FieldSectionValueBoundaries<SectionType extends FieldSectionType> = {
minimum: number;
maximum: number;
} & (SectionType extends 'day' ? {
longestMonth: PickerValidDate;
} : {});
export type FieldSectionsValueBoundaries = { [SectionType in FieldSectionType]: (params: {
currentDate: PickerValidDate | null;
format: string;
contentType: FieldSectionContentType;
}) => FieldSectionValueBoundaries<SectionType> };
export type FieldSectionsBoundaries = { [SectionType in FieldSectionType]: {
minimum: number;
maximum: number;
} };
export type FieldChangeHandler<TValue extends PickerValidValue, TError> = (value: TValue, context: FieldChangeHandlerContext<TError>) => void;
export interface FieldChangeHandlerContext<TError> {
validationError: TError;
}
export type FieldParsedSelectedSections = number | 'all' | null;
export interface FieldValueManager<TValue extends PickerValidValue> {
/**
* Creates the section list from the current value.
* The `prevSections` are used on the range fields to avoid losing the sections of a partially filled date when editing the other date.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {TValue} value The current value to generate sections from.
* @param {(date: PickerValidDate | null) => FieldSection[]} getSectionsFromDate Returns the sections of the given date.
* @returns {InferFieldSection<TValue>[]} The new section list.
*/
getSectionsFromValue: (value: TValue, getSectionsFromDate: (date: PickerValidDate | null) => FieldSection[]) => InferFieldSection<TValue>[];
/**
* Creates the string value to render in the input based on the current section list.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {InferFieldSection<TValue>[]} sections The current section list.
* @param {string} localizedDigits The conversion table from localized to 0-9 digits.
* @param {boolean} isRtl `true` if the current orientation is "right to left"
* @returns {string} The string value to render in the input.
*/
getV6InputValueFromSections: (sections: InferFieldSection<TValue>[], localizedDigits: string[], isRtl: boolean) => string;
/**
* Creates the string value to render in the input based on the current section list.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {InferFieldSection<TValue>[]} sections The current section list.
* @returns {string} The string value to render in the input.
*/
getV7HiddenInputValueFromSections: (sections: InferFieldSection<TValue>[]) => string;
/**
* Parses a string version (most of the time coming from the input).
* This method should only be used when the change does not come from a single section.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {string} valueStr The string value to parse.
* @param {TValue} referenceValue The reference value currently stored in state.
* @param {(dateStr: string, referenceDate: PickerValidDate) => PickerValidDate | null} parseDate A method to convert a string date into a parsed one.
* @returns {TValue} The new parsed value.
*/
parseValueStr: (valueStr: string, referenceValue: InferNonNullablePickerValue<TValue>, parseDate: (dateStr: string, referenceDate: PickerValidDate) => PickerValidDate | null) => TValue;
/**
* Update the reference value with the new value.
* This method must make sure that no date inside the returned `referenceValue` is invalid.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {MuiPickersAdapter} adapter The adapter to manipulate the date.
* @param {TValue} value The new value from which we want to take all valid dates in the `referenceValue` state.
* @param {TValue} prevReferenceValue The previous reference value. It is used as a fallback for invalid dates in the new value.
* @returns {TValue} The new reference value with no invalid date.
*/
updateReferenceValue: (adapter: MuiPickersAdapter, value: TValue, prevReferenceValue: InferNonNullablePickerValue<TValue>) => InferNonNullablePickerValue<TValue>;
/**
* Extract from the given value the date that contains the given section.
* @param {TValue} value The value to extract the date from.
* @param {InferFieldSection<TValue>} section The section to get the date from.
* @returns {PickerValidDate | null} The date that contains the section.
*/
getDateFromSection: (value: TValue, section: InferFieldSection<TValue>) => PickerValidDate | null;
/**
* Get the sections of the date that contains the given section.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {InferFieldSection<TValue>[]} sections The sections of the full value.
* @param {InferFieldSection<TValue>} section A section of the date from which we want to get all the sections.
* @returns {InferFieldSection<TValue>[]} The sections of the date that contains the section.
*/
getDateSectionsFromValue: (sections: InferFieldSection<TValue>[], section: InferFieldSection<TValue>) => InferFieldSection<TValue>[];
/**
* Creates a new value based on the provided date and the current value.
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {TValue} value The value to update the date in.
* @param {InferFieldSection<TValue>} section A section of the date we want to update in the value.
* @param {PickerValidDate | null} date The date that contains the section.
* @returns {TValue} The updated value.
*/
updateDateInValue: (value: TValue, section: InferFieldSection<TValue>, date: PickerValidDate | null) => TValue;
/**
* @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value.
* @param {InferFieldSection<TValue>[]} sections The sections of the full value.
* @param {InferFieldSection<TValue>} section A section of the date from which we want to clear all the sections.
* @returns {InferFieldSection<TValue>[]} The sections of the full value with all the sections of the target date cleared.
*/
clearDateSections: (sections: InferFieldSection<TValue>[], section: InferFieldSection<TValue>) => InferFieldSection<TValue>[];
}
export interface UseFieldState<TValue extends PickerValidValue> {
/**
* Last value returned by `useControlledValue`.
*/
lastExternalValue: TValue;
/**
* Last set of parameters used to generate the sections.
*/
lastSectionsDependencies: {
format: string;
isRtl: boolean;
locale: any;
};
/**
* Non-nullable value used to keep trace of the timezone and the date parts not present in the format.
* It is updated whenever we have a valid date (for the Range Pickers we update only the portion of the range that is valid).
*/
referenceValue: InferNonNullablePickerValue<TValue>;
/**
* Sections currently displayed in the field.
*/
sections: InferFieldSection<TValue>[];
/**
* Android `onChange` behavior when the input selection is not empty is quite different from a desktop behavior.
* There are two `onChange` calls:
* 1. A call with the selected content removed.
* 2. A call with the key pressed added to the value.
**
* For instance, if the input value equals `month / day / year` and `day` is selected.
* The pressing `1` will have the following behavior:
* 1. A call with `month / / year`.
* 2. A call with `month / 1 / year`.
*
* But if you don't update the input with the value passed on the first `onChange`.
* Then the second `onChange` will add the key press at the beginning of the selected value.
* 1. A call with `month / / year` that we don't set into state.
* 2. A call with `month / 1day / year`.
*
* The property below allows us to set the first `onChange` value into state waiting for the second one.
*/
tempValueStrAndroid: string | null;
/**
* The current query when editing the field using letters or digits.
*/
characterQuery: CharacterEditingQuery | null;
}
export type SectionNeighbors = {
[sectionIndex: number]: {
/**
* Index of the next section displayed on the left. `null` if it's the leftmost section.
*/
leftIndex: number | null;
/**
* Index of the next section displayed on the right. `null` if it's the rightmost section.
*/
rightIndex: number | null;
};
};
export type SectionOrdering = {
/**
* For each section index provide the index of the section displayed on the left and on the right.
*/
neighbors: SectionNeighbors;
/**
* Index of the section displayed on the far left
*/
startIndex: number;
/**
* Index of the section displayed on the far right
*/
endIndex: number;
};
export interface CharacterEditingQuery {
value: string;
sectionIndex: number;
sectionType: FieldSectionType;
}
export type UseFieldProps<TEnableAccessibleFieldDOMStructure extends boolean> = UseFieldForwardedProps<TEnableAccessibleFieldDOMStructure> & {
enableAccessibleFieldDOMStructure?: boolean;
};
export interface UseFieldDOMGetters {
isReady: () => boolean;
getRoot: () => HTMLElement;
getSectionContainer: (sectionIndex: number) => HTMLElement;
getSectionContent: (sectionIndex: number) => HTMLElement;
getSectionIndexFromDOMElement: (element: Element | null | undefined) => number | null;
}
export {};

View file

@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});

View file

@ -0,0 +1,36 @@
import { FieldSectionsValueBoundaries, SectionOrdering, FieldSectionValueBoundaries, FieldParsedSelectedSections } from "./useField.types.js";
import { FieldSectionType, FieldSection, MuiPickersAdapter, FieldSectionContentType, PickersTimezone, PickerValidDate, FieldSelectedSections, PickerValueType, InferFieldSection } from "../../../models/index.js";
import { PickerValidValue } from "../../models/index.js";
export declare const getDateSectionConfigFromFormatToken: (adapter: MuiPickersAdapter, formatToken: string) => Pick<FieldSection, "type" | "contentType"> & {
maxLength: number | undefined;
};
export declare const getDaysInWeekStr: (adapter: MuiPickersAdapter, format: string) => string[];
export declare const getLetterEditingOptions: (adapter: MuiPickersAdapter, timezone: PickersTimezone, sectionType: FieldSectionType, format: string) => string[];
export declare const FORMAT_SECONDS_NO_LEADING_ZEROS = "s";
export declare const getLocalizedDigits: (adapter: MuiPickersAdapter) => string[];
export declare const removeLocalizedDigits: (valueStr: string, localizedDigits: string[]) => string;
export declare const applyLocalizedDigits: (valueStr: string, localizedDigits: string[]) => string;
export declare const isStringNumber: (valueStr: string, localizedDigits: string[]) => boolean;
/**
* Make sure the value of a digit section have the right amount of leading zeros.
* E.g.: `03` => `3`
* Warning: Should only be called with non-localized digits. Call `removeLocalizedDigits` with your value if needed.
*/
export declare const cleanLeadingZeros: (valueStr: string, size: number) => string;
export declare const cleanDigitSectionValue: (adapter: MuiPickersAdapter, value: number, sectionBoundaries: FieldSectionValueBoundaries<any>, localizedDigits: string[], section: Pick<FieldSection, "format" | "type" | "contentType" | "hasLeadingZerosInFormat" | "hasLeadingZerosInInput" | "maxLength">) => string;
export declare const getSectionVisibleValue: (section: FieldSection, target: "input-rtl" | "input-ltr" | "non-input", localizedDigits: string[]) => string;
export declare const changeSectionValueFormat: (adapter: MuiPickersAdapter, valueStr: string, currentFormat: string, newFormat: string) => string;
export declare const doesSectionFormatHaveLeadingZeros: (adapter: MuiPickersAdapter, contentType: FieldSectionContentType, sectionType: FieldSectionType, format: string) => boolean;
/**
* Some date libraries like `dayjs` don't support parsing from date with escaped characters.
* To make sure that the parsing works, we are building a format and a date without any separator.
*/
export declare const getDateFromDateSections: (adapter: MuiPickersAdapter, sections: FieldSection[], localizedDigits: string[]) => PickerValidDate;
export declare const createDateStrForV7HiddenInputFromSections: (sections: FieldSection[]) => string;
export declare const createDateStrForV6InputFromSections: (sections: FieldSection[], localizedDigits: string[], isRtl: boolean) => string;
export declare const getSectionsBoundaries: (adapter: MuiPickersAdapter, localizedDigits: string[], timezone: PickersTimezone) => FieldSectionsValueBoundaries;
export declare const validateSections: <TValue extends PickerValidValue>(sections: InferFieldSection<TValue>[], valueType: PickerValueType) => void;
export declare const mergeDateIntoReferenceDate: (adapter: MuiPickersAdapter, dateToTransferFrom: PickerValidDate, sections: FieldSection[], referenceDate: PickerValidDate, shouldLimitToEditedSections: boolean) => PickerValidDate;
export declare const isAndroid: () => boolean;
export declare const getSectionOrder: (sections: FieldSection[], shouldApplyRTL: boolean) => SectionOrdering;
export declare const parseSelectedSections: (selectedSections: FieldSelectedSections, sections: FieldSection[]) => FieldParsedSelectedSections;

View file

@ -0,0 +1,523 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.validateSections = exports.removeLocalizedDigits = exports.parseSelectedSections = exports.mergeDateIntoReferenceDate = exports.isStringNumber = exports.isAndroid = exports.getSectionsBoundaries = exports.getSectionVisibleValue = exports.getSectionOrder = exports.getLocalizedDigits = exports.getLetterEditingOptions = exports.getDaysInWeekStr = exports.getDateSectionConfigFromFormatToken = exports.getDateFromDateSections = exports.doesSectionFormatHaveLeadingZeros = exports.createDateStrForV7HiddenInputFromSections = exports.createDateStrForV6InputFromSections = exports.cleanLeadingZeros = exports.cleanDigitSectionValue = exports.changeSectionValueFormat = exports.applyLocalizedDigits = exports.FORMAT_SECONDS_NO_LEADING_ZEROS = void 0;
var _dateUtils = require("../../utils/date-utils");
const getDateSectionConfigFromFormatToken = (adapter, formatToken) => {
const config = adapter.formatTokenMap[formatToken];
if (config == null) {
throw new Error([`MUI X: The token "${formatToken}" is not supported by the Date and Time Pickers.`, 'Please try using another token or open an issue on https://github.com/mui/mui-x/issues/new/choose if you think it should be supported.'].join('\n'));
}
if (typeof config === 'string') {
return {
type: config,
contentType: config === 'meridiem' ? 'letter' : 'digit',
maxLength: undefined
};
}
return {
type: config.sectionType,
contentType: config.contentType,
maxLength: config.maxLength
};
};
exports.getDateSectionConfigFromFormatToken = getDateSectionConfigFromFormatToken;
const getDaysInWeekStr = (adapter, format) => {
const elements = [];
const now = adapter.date(undefined, 'default');
const startDate = adapter.startOfWeek(now);
const endDate = adapter.endOfWeek(now);
let current = startDate;
while (adapter.isBefore(current, endDate)) {
elements.push(current);
current = adapter.addDays(current, 1);
}
return elements.map(weekDay => adapter.formatByString(weekDay, format));
};
exports.getDaysInWeekStr = getDaysInWeekStr;
const getLetterEditingOptions = (adapter, timezone, sectionType, format) => {
switch (sectionType) {
case 'month':
{
return (0, _dateUtils.getMonthsInYear)(adapter, adapter.date(undefined, timezone)).map(month => adapter.formatByString(month, format));
}
case 'weekDay':
{
return getDaysInWeekStr(adapter, format);
}
case 'meridiem':
{
const now = adapter.date(undefined, timezone);
return [adapter.startOfDay(now), adapter.endOfDay(now)].map(date => adapter.formatByString(date, format));
}
default:
{
return [];
}
}
};
// This format should be the same on all the adapters
// If some adapter does not respect this convention, then we will need to hardcode the format on each adapter.
exports.getLetterEditingOptions = getLetterEditingOptions;
const FORMAT_SECONDS_NO_LEADING_ZEROS = exports.FORMAT_SECONDS_NO_LEADING_ZEROS = 's';
const NON_LOCALIZED_DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const getLocalizedDigits = adapter => {
const today = adapter.date(undefined);
const formattedZero = adapter.formatByString(adapter.setSeconds(today, 0), FORMAT_SECONDS_NO_LEADING_ZEROS);
if (formattedZero === '0') {
return NON_LOCALIZED_DIGITS;
}
return Array.from({
length: 10
}).map((_, index) => adapter.formatByString(adapter.setSeconds(today, index), FORMAT_SECONDS_NO_LEADING_ZEROS));
};
exports.getLocalizedDigits = getLocalizedDigits;
const removeLocalizedDigits = (valueStr, localizedDigits) => {
if (localizedDigits[0] === '0') {
return valueStr;
}
const digits = [];
let currentFormattedDigit = '';
for (let i = 0; i < valueStr.length; i += 1) {
currentFormattedDigit += valueStr[i];
const matchingDigitIndex = localizedDigits.indexOf(currentFormattedDigit);
if (matchingDigitIndex > -1) {
digits.push(matchingDigitIndex.toString());
currentFormattedDigit = '';
}
}
return digits.join('');
};
exports.removeLocalizedDigits = removeLocalizedDigits;
const applyLocalizedDigits = (valueStr, localizedDigits) => {
if (localizedDigits[0] === '0') {
return valueStr;
}
return valueStr.split('').map(char => localizedDigits[Number(char)]).join('');
};
exports.applyLocalizedDigits = applyLocalizedDigits;
const isStringNumber = (valueStr, localizedDigits) => {
const nonLocalizedValueStr = removeLocalizedDigits(valueStr, localizedDigits);
// `Number(' ')` returns `0` even if ' ' is not a valid number.
return nonLocalizedValueStr !== ' ' && !Number.isNaN(Number(nonLocalizedValueStr));
};
/**
* Make sure the value of a digit section have the right amount of leading zeros.
* E.g.: `03` => `3`
* Warning: Should only be called with non-localized digits. Call `removeLocalizedDigits` with your value if needed.
*/
exports.isStringNumber = isStringNumber;
const cleanLeadingZeros = (valueStr, size) => {
// Remove the leading zeros and then add back as many as needed.
return Number(valueStr).toString().padStart(size, '0');
};
exports.cleanLeadingZeros = cleanLeadingZeros;
const cleanDigitSectionValue = (adapter, value, sectionBoundaries, localizedDigits, section) => {
if (process.env.NODE_ENV !== 'production') {
if (section.type !== 'day' && section.contentType === 'digit-with-letter') {
throw new Error([`MUI X: The token "${section.format}" is a digit format with letter in it.'
This type of format is only supported for 'day' sections`].join('\n'));
}
}
if (section.type === 'day' && section.contentType === 'digit-with-letter') {
const date = adapter.setDate(sectionBoundaries.longestMonth, value);
return adapter.formatByString(date, section.format);
}
// queryValue without leading `0` (`01` => `1`)
let valueStr = value.toString();
if (section.hasLeadingZerosInInput) {
valueStr = cleanLeadingZeros(valueStr, section.maxLength);
}
return applyLocalizedDigits(valueStr, localizedDigits);
};
exports.cleanDigitSectionValue = cleanDigitSectionValue;
const getSectionVisibleValue = (section, target, localizedDigits) => {
let value = section.value || section.placeholder;
const hasLeadingZeros = target === 'non-input' ? section.hasLeadingZerosInFormat : section.hasLeadingZerosInInput;
if (target === 'non-input' && section.hasLeadingZerosInInput && !section.hasLeadingZerosInFormat) {
value = Number(removeLocalizedDigits(value, localizedDigits)).toString();
}
// In the input, we add an empty character at the end of each section without leading zeros.
// This makes sure that `onChange` will always be fired.
// Otherwise, when your input value equals `1/dd/yyyy` (format `M/DD/YYYY` on DayJs),
// If you press `1`, on the first section, the new value is also `1/dd/yyyy`,
// So the browser will not fire the input `onChange`.
const shouldAddInvisibleSpace = ['input-rtl', 'input-ltr'].includes(target) && section.contentType === 'digit' && !hasLeadingZeros && value.length === 1;
if (shouldAddInvisibleSpace) {
value = `${value}\u200e`;
}
if (target === 'input-rtl') {
value = `\u2068${value}\u2069`;
}
return value;
};
exports.getSectionVisibleValue = getSectionVisibleValue;
const changeSectionValueFormat = (adapter, valueStr, currentFormat, newFormat) => {
if (process.env.NODE_ENV !== 'production') {
if (getDateSectionConfigFromFormatToken(adapter, currentFormat).type === 'weekDay') {
throw new Error("changeSectionValueFormat doesn't support week day formats");
}
}
return adapter.formatByString(adapter.parse(valueStr, currentFormat), newFormat);
};
exports.changeSectionValueFormat = changeSectionValueFormat;
const isFourDigitYearFormat = (adapter, format) => adapter.formatByString(adapter.date(undefined, 'system'), format).length === 4;
const doesSectionFormatHaveLeadingZeros = (adapter, contentType, sectionType, format) => {
if (contentType !== 'digit') {
return false;
}
const now = adapter.date(undefined, 'default');
switch (sectionType) {
// We can't use `changeSectionValueFormat`, because `adapter.parse('1', 'YYYY')` returns `1971` instead of `1`.
case 'year':
{
// Remove once https://github.com/iamkun/dayjs/pull/2847 is merged and bump dayjs version
if (adapter.lib === 'dayjs' && format === 'YY') {
return true;
}
return adapter.formatByString(adapter.setYear(now, 1), format).startsWith('0');
}
case 'month':
{
return adapter.formatByString(adapter.startOfYear(now), format).length > 1;
}
case 'day':
{
return adapter.formatByString(adapter.startOfMonth(now), format).length > 1;
}
case 'weekDay':
{
return adapter.formatByString(adapter.startOfWeek(now), format).length > 1;
}
case 'hours':
{
return adapter.formatByString(adapter.setHours(now, 1), format).length > 1;
}
case 'minutes':
{
return adapter.formatByString(adapter.setMinutes(now, 1), format).length > 1;
}
case 'seconds':
{
return adapter.formatByString(adapter.setSeconds(now, 1), format).length > 1;
}
default:
{
throw new Error('Invalid section type');
}
}
};
/**
* Some date libraries like `dayjs` don't support parsing from date with escaped characters.
* To make sure that the parsing works, we are building a format and a date without any separator.
*/
exports.doesSectionFormatHaveLeadingZeros = doesSectionFormatHaveLeadingZeros;
const getDateFromDateSections = (adapter, sections, localizedDigits) => {
// If we have both a day and a weekDay section,
// Then we skip the weekDay in the parsing because libraries like dayjs can't parse complicated formats containing a weekDay.
// dayjs(dayjs().format('dddd MMMM D YYYY'), 'dddd MMMM D YYYY')) // returns `Invalid Date` even if the format is valid.
const shouldSkipWeekDays = sections.some(section => section.type === 'day');
const sectionFormats = [];
const sectionValues = [];
for (let i = 0; i < sections.length; i += 1) {
const section = sections[i];
const shouldSkip = shouldSkipWeekDays && section.type === 'weekDay';
if (!shouldSkip) {
sectionFormats.push(section.format);
sectionValues.push(getSectionVisibleValue(section, 'non-input', localizedDigits));
}
}
const formatWithoutSeparator = sectionFormats.join(' ');
const dateWithoutSeparatorStr = sectionValues.join(' ');
return adapter.parse(dateWithoutSeparatorStr, formatWithoutSeparator);
};
exports.getDateFromDateSections = getDateFromDateSections;
const createDateStrForV7HiddenInputFromSections = sections => sections.map(section => {
return `${section.startSeparator}${section.value || section.placeholder}${section.endSeparator}`;
}).join('');
exports.createDateStrForV7HiddenInputFromSections = createDateStrForV7HiddenInputFromSections;
const createDateStrForV6InputFromSections = (sections, localizedDigits, isRtl) => {
const formattedSections = sections.map(section => {
const dateValue = getSectionVisibleValue(section, isRtl ? 'input-rtl' : 'input-ltr', localizedDigits);
return `${section.startSeparator}${dateValue}${section.endSeparator}`;
});
const dateStr = formattedSections.join('');
if (!isRtl) {
return dateStr;
}
// \u2066: start left-to-right isolation
// \u2067: start right-to-left isolation
// \u2068: start first strong character isolation
// \u2069: pop isolation
// wrap into an isolated group such that separators can split the string in smaller ones by adding \u2069\u2068
return `\u2066${dateStr}\u2069`;
};
exports.createDateStrForV6InputFromSections = createDateStrForV6InputFromSections;
const getSectionsBoundaries = (adapter, localizedDigits, timezone) => {
const today = adapter.date(undefined, timezone);
const endOfYear = adapter.endOfYear(today);
const endOfDay = adapter.endOfDay(today);
const {
maxDaysInMonth,
longestMonth
} = (0, _dateUtils.getMonthsInYear)(adapter, today).reduce((acc, month) => {
const daysInMonth = adapter.getDaysInMonth(month);
if (daysInMonth > acc.maxDaysInMonth) {
return {
maxDaysInMonth: daysInMonth,
longestMonth: month
};
}
return acc;
}, {
maxDaysInMonth: 0,
longestMonth: null
});
return {
year: ({
format
}) => ({
minimum: 0,
maximum: isFourDigitYearFormat(adapter, format) ? 9999 : 99
}),
month: () => ({
minimum: 1,
// Assumption: All years have the same amount of months
maximum: adapter.getMonth(endOfYear) + 1
}),
day: ({
currentDate
}) => ({
minimum: 1,
maximum: adapter.isValid(currentDate) ? adapter.getDaysInMonth(currentDate) : maxDaysInMonth,
longestMonth: longestMonth
}),
weekDay: ({
format,
contentType
}) => {
if (contentType === 'digit') {
const daysInWeek = getDaysInWeekStr(adapter, format).map(Number);
return {
minimum: Math.min(...daysInWeek),
maximum: Math.max(...daysInWeek)
};
}
return {
minimum: 1,
maximum: 7
};
},
hours: ({
format
}) => {
const lastHourInDay = adapter.getHours(endOfDay);
const hasMeridiem = removeLocalizedDigits(adapter.formatByString(adapter.endOfDay(today), format), localizedDigits) !== lastHourInDay.toString();
if (hasMeridiem) {
return {
minimum: 1,
maximum: Number(removeLocalizedDigits(adapter.formatByString(adapter.startOfDay(today), format), localizedDigits))
};
}
return {
minimum: 0,
maximum: lastHourInDay
};
},
minutes: () => ({
minimum: 0,
// Assumption: All years have the same amount of minutes
maximum: adapter.getMinutes(endOfDay)
}),
seconds: () => ({
minimum: 0,
// Assumption: All years have the same amount of seconds
maximum: adapter.getSeconds(endOfDay)
}),
meridiem: () => ({
minimum: 0,
maximum: 1
}),
empty: () => ({
minimum: 0,
maximum: 0
})
};
};
exports.getSectionsBoundaries = getSectionsBoundaries;
let warnedOnceInvalidSection = false;
const validateSections = (sections, valueType) => {
if (process.env.NODE_ENV !== 'production') {
if (!warnedOnceInvalidSection) {
const supportedSections = ['empty'];
if (['date', 'date-time'].includes(valueType)) {
supportedSections.push('weekDay', 'day', 'month', 'year');
}
if (['time', 'date-time'].includes(valueType)) {
supportedSections.push('hours', 'minutes', 'seconds', 'meridiem');
}
const invalidSection = sections.find(section => !supportedSections.includes(section.type));
if (invalidSection) {
console.warn(`MUI X: The field component you are using is not compatible with the "${invalidSection.type}" date section.`, `The supported date sections are ["${supportedSections.join('", "')}"]\`.`);
warnedOnceInvalidSection = true;
}
}
}
};
exports.validateSections = validateSections;
const transferDateSectionValue = (adapter, section, dateToTransferFrom, dateToTransferTo) => {
switch (section.type) {
case 'year':
{
return adapter.setYear(dateToTransferTo, adapter.getYear(dateToTransferFrom));
}
case 'month':
{
return adapter.setMonth(dateToTransferTo, adapter.getMonth(dateToTransferFrom));
}
case 'weekDay':
{
let dayInWeekStrOfActiveDate = adapter.formatByString(dateToTransferFrom, section.format);
if (section.hasLeadingZerosInInput) {
dayInWeekStrOfActiveDate = cleanLeadingZeros(dayInWeekStrOfActiveDate, section.maxLength);
}
const formattedDaysInWeek = getDaysInWeekStr(adapter, section.format);
const dayInWeekOfActiveDate = formattedDaysInWeek.indexOf(dayInWeekStrOfActiveDate);
const dayInWeekOfNewSectionValue = formattedDaysInWeek.indexOf(section.value);
const diff = dayInWeekOfNewSectionValue - dayInWeekOfActiveDate;
return adapter.addDays(dateToTransferFrom, diff);
}
case 'day':
{
return adapter.setDate(dateToTransferTo, adapter.getDate(dateToTransferFrom));
}
case 'meridiem':
{
const isAM = adapter.getHours(dateToTransferFrom) < 12;
const mergedDateHours = adapter.getHours(dateToTransferTo);
if (isAM && mergedDateHours >= 12) {
return adapter.addHours(dateToTransferTo, -12);
}
if (!isAM && mergedDateHours < 12) {
return adapter.addHours(dateToTransferTo, 12);
}
return dateToTransferTo;
}
case 'hours':
{
return adapter.setHours(dateToTransferTo, adapter.getHours(dateToTransferFrom));
}
case 'minutes':
{
return adapter.setMinutes(dateToTransferTo, adapter.getMinutes(dateToTransferFrom));
}
case 'seconds':
{
return adapter.setSeconds(dateToTransferTo, adapter.getSeconds(dateToTransferFrom));
}
default:
{
return dateToTransferTo;
}
}
};
const reliableSectionModificationOrder = {
year: 1,
month: 2,
day: 3,
weekDay: 4,
hours: 5,
minutes: 6,
seconds: 7,
meridiem: 8,
empty: 9
};
const mergeDateIntoReferenceDate = (adapter, dateToTransferFrom, sections, referenceDate, shouldLimitToEditedSections) =>
// cloning sections before sort to avoid mutating it
[...sections].sort((a, b) => reliableSectionModificationOrder[a.type] - reliableSectionModificationOrder[b.type]).reduce((mergedDate, section) => {
if (!shouldLimitToEditedSections || section.modified) {
return transferDateSectionValue(adapter, section, dateToTransferFrom, mergedDate);
}
return mergedDate;
}, referenceDate);
exports.mergeDateIntoReferenceDate = mergeDateIntoReferenceDate;
const isAndroid = () => navigator.userAgent.toLowerCase().includes('android');
// TODO v9: Remove
exports.isAndroid = isAndroid;
const getSectionOrder = (sections, shouldApplyRTL) => {
const neighbors = {};
if (!shouldApplyRTL) {
sections.forEach((_, index) => {
const leftIndex = index === 0 ? null : index - 1;
const rightIndex = index === sections.length - 1 ? null : index + 1;
neighbors[index] = {
leftIndex,
rightIndex
};
});
return {
neighbors,
startIndex: 0,
endIndex: sections.length - 1
};
}
const rtl2ltr = {};
const ltr2rtl = {};
let groupedSectionsStart = 0;
let groupedSectionsEnd = 0;
let RTLIndex = sections.length - 1;
while (RTLIndex >= 0) {
groupedSectionsEnd = sections.findIndex(
// eslint-disable-next-line @typescript-eslint/no-loop-func
(section, index) => index >= groupedSectionsStart && section.endSeparator?.includes(' ') &&
// Special case where the spaces were not there in the initial input
section.endSeparator !== ' / ');
if (groupedSectionsEnd === -1) {
groupedSectionsEnd = sections.length - 1;
}
for (let i = groupedSectionsEnd; i >= groupedSectionsStart; i -= 1) {
ltr2rtl[i] = RTLIndex;
rtl2ltr[RTLIndex] = i;
RTLIndex -= 1;
}
groupedSectionsStart = groupedSectionsEnd + 1;
}
sections.forEach((_, index) => {
const rtlIndex = ltr2rtl[index];
const leftIndex = rtlIndex === 0 ? null : rtl2ltr[rtlIndex - 1];
const rightIndex = rtlIndex === sections.length - 1 ? null : rtl2ltr[rtlIndex + 1];
neighbors[index] = {
leftIndex,
rightIndex
};
});
return {
neighbors,
startIndex: rtl2ltr[0],
endIndex: rtl2ltr[sections.length - 1]
};
};
exports.getSectionOrder = getSectionOrder;
const parseSelectedSections = (selectedSections, sections) => {
if (selectedSections == null) {
return null;
}
if (selectedSections === 'all') {
return 'all';
}
if (typeof selectedSections === 'string') {
const index = sections.findIndex(section => section.type === selectedSections);
return index === -1 ? null : index;
}
return selectedSections;
};
exports.parseSelectedSections = parseSelectedSections;

View file

@ -0,0 +1,29 @@
import { UseFieldStateReturnValue } from "./useFieldState.js";
import { PickerValidValue } from "../../models/index.js";
/**
* Update the active section value when the user pressed a key that is not a navigation key (arrow key for example).
* This hook has two main editing behaviors
*
* 1. The numeric editing when the user presses a digit
* 2. The letter editing when the user presses another key
*/
export declare const useFieldCharacterEditing: <TValue extends PickerValidValue>({
stateResponse: {
localizedDigits,
sectionsValueBoundaries,
state,
timezone,
setCharacterQuery,
setTempAndroidValueStr,
updateSectionValue
}
}: UseFieldCharacterEditingParameters<TValue>) => UseFieldCharacterEditingReturnValue;
export interface ApplyCharacterEditingParameters {
keyPressed: string;
sectionIndex: number;
}
interface UseFieldCharacterEditingParameters<TValue extends PickerValidValue> {
stateResponse: UseFieldStateReturnValue<TValue>;
}
export type UseFieldCharacterEditingReturnValue = (params: ApplyCharacterEditingParameters) => void;
export {};

View file

@ -0,0 +1,264 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldCharacterEditing = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _useField = require("./useField.utils");
var _usePickerAdapter = require("../../../hooks/usePickerAdapter");
const isQueryResponseWithoutValue = response => response.saveQuery != null;
/**
* Update the active section value when the user pressed a key that is not a navigation key (arrow key for example).
* This hook has two main editing behaviors
*
* 1. The numeric editing when the user presses a digit
* 2. The letter editing when the user presses another key
*/
const useFieldCharacterEditing = ({
stateResponse: {
// States and derived states
localizedDigits,
sectionsValueBoundaries,
state,
timezone,
// Methods to update the states
setCharacterQuery,
setTempAndroidValueStr,
updateSectionValue
}
}) => {
const adapter = (0, _usePickerAdapter.usePickerAdapter)();
const applyQuery = ({
keyPressed,
sectionIndex
}, getFirstSectionValueMatchingWithQuery, isValidQueryValue) => {
const cleanKeyPressed = keyPressed.toLowerCase();
const activeSection = state.sections[sectionIndex];
// The current query targets the section being editing
// We can try to concatenate the value
if (state.characterQuery != null && (!isValidQueryValue || isValidQueryValue(state.characterQuery.value)) && state.characterQuery.sectionIndex === sectionIndex) {
const concatenatedQueryValue = `${state.characterQuery.value}${cleanKeyPressed}`;
const queryResponse = getFirstSectionValueMatchingWithQuery(concatenatedQueryValue, activeSection);
if (!isQueryResponseWithoutValue(queryResponse)) {
setCharacterQuery({
sectionIndex,
value: concatenatedQueryValue,
sectionType: activeSection.type
});
return queryResponse;
}
}
const queryResponse = getFirstSectionValueMatchingWithQuery(cleanKeyPressed, activeSection);
if (isQueryResponseWithoutValue(queryResponse) && !queryResponse.saveQuery) {
setCharacterQuery(null);
return null;
}
setCharacterQuery({
sectionIndex,
value: cleanKeyPressed,
sectionType: activeSection.type
});
if (isQueryResponseWithoutValue(queryResponse)) {
return null;
}
return queryResponse;
};
const applyLetterEditing = params => {
const findMatchingOptions = (format, options, queryValue) => {
const matchingValues = options.filter(option => option.toLowerCase().startsWith(queryValue));
if (matchingValues.length === 0) {
return {
saveQuery: false
};
}
return {
sectionValue: matchingValues[0],
shouldGoToNextSection: matchingValues.length === 1
};
};
const testQueryOnFormatAndFallbackFormat = (queryValue, activeSection, fallbackFormat, formatFallbackValue) => {
const getOptions = format => (0, _useField.getLetterEditingOptions)(adapter, timezone, activeSection.type, format);
if (activeSection.contentType === 'letter') {
return findMatchingOptions(activeSection.format, getOptions(activeSection.format), queryValue);
}
// When editing a digit-format month / weekDay and the user presses a letter,
// We can support the letter editing by using the letter-format month / weekDay and re-formatting the result.
// We just have to make sure that the default month / weekDay format is a letter format,
if (fallbackFormat && formatFallbackValue != null && (0, _useField.getDateSectionConfigFromFormatToken)(adapter, fallbackFormat).contentType === 'letter') {
const fallbackOptions = getOptions(fallbackFormat);
const response = findMatchingOptions(fallbackFormat, fallbackOptions, queryValue);
if (isQueryResponseWithoutValue(response)) {
return {
saveQuery: false
};
}
return (0, _extends2.default)({}, response, {
sectionValue: formatFallbackValue(response.sectionValue, fallbackOptions)
});
}
return {
saveQuery: false
};
};
const getFirstSectionValueMatchingWithQuery = (queryValue, activeSection) => {
switch (activeSection.type) {
case 'month':
{
const formatFallbackValue = fallbackValue => (0, _useField.changeSectionValueFormat)(adapter, fallbackValue, adapter.formats.month, activeSection.format);
return testQueryOnFormatAndFallbackFormat(queryValue, activeSection, adapter.formats.month, formatFallbackValue);
}
case 'weekDay':
{
const formatFallbackValue = (fallbackValue, fallbackOptions) => fallbackOptions.indexOf(fallbackValue).toString();
return testQueryOnFormatAndFallbackFormat(queryValue, activeSection, adapter.formats.weekday, formatFallbackValue);
}
case 'meridiem':
{
return testQueryOnFormatAndFallbackFormat(queryValue, activeSection);
}
default:
{
return {
saveQuery: false
};
}
}
};
return applyQuery(params, getFirstSectionValueMatchingWithQuery);
};
const applyNumericEditing = params => {
const getNewSectionValue = ({
queryValue,
skipIfBelowMinimum,
section
}) => {
const cleanQueryValue = (0, _useField.removeLocalizedDigits)(queryValue, localizedDigits);
const queryValueNumber = Number(cleanQueryValue);
const sectionBoundaries = sectionsValueBoundaries[section.type]({
currentDate: null,
format: section.format,
contentType: section.contentType
});
if (queryValueNumber > sectionBoundaries.maximum) {
return {
saveQuery: false
};
}
// If the user types `0` on a month section,
// It is below the minimum, but we want to store the `0` in the query,
// So that when he pressed `1`, it will store `01` and move to the next section.
if (skipIfBelowMinimum && queryValueNumber < sectionBoundaries.minimum) {
return {
saveQuery: true
};
}
const shouldGoToNextSection = queryValueNumber * 10 > sectionBoundaries.maximum || cleanQueryValue.length === sectionBoundaries.maximum.toString().length;
const newSectionValue = (0, _useField.cleanDigitSectionValue)(adapter, queryValueNumber, sectionBoundaries, localizedDigits, section);
return {
sectionValue: newSectionValue,
shouldGoToNextSection
};
};
const getFirstSectionValueMatchingWithQuery = (queryValue, activeSection) => {
if (activeSection.contentType === 'digit' || activeSection.contentType === 'digit-with-letter') {
return getNewSectionValue({
queryValue,
skipIfBelowMinimum: false,
section: activeSection
});
}
// When editing a letter-format month and the user presses a digit,
// We can support the numeric editing by using the digit-format month and re-formatting the result.
if (activeSection.type === 'month') {
const hasLeadingZerosInFormat = (0, _useField.doesSectionFormatHaveLeadingZeros)(adapter, 'digit', 'month', 'MM');
const response = getNewSectionValue({
queryValue,
skipIfBelowMinimum: true,
section: {
type: activeSection.type,
format: 'MM',
hasLeadingZerosInFormat,
hasLeadingZerosInInput: true,
contentType: 'digit',
maxLength: 2
}
});
if (isQueryResponseWithoutValue(response)) {
return response;
}
const formattedValue = (0, _useField.changeSectionValueFormat)(adapter, response.sectionValue, 'MM', activeSection.format);
return (0, _extends2.default)({}, response, {
sectionValue: formattedValue
});
}
// When editing a letter-format weekDay and the user presses a digit,
// We can support the numeric editing by returning the nth day in the week day array.
if (activeSection.type === 'weekDay') {
const response = getNewSectionValue({
queryValue,
skipIfBelowMinimum: true,
section: activeSection
});
if (isQueryResponseWithoutValue(response)) {
return response;
}
const formattedValue = (0, _useField.getDaysInWeekStr)(adapter, activeSection.format)[Number(response.sectionValue) - 1];
return (0, _extends2.default)({}, response, {
sectionValue: formattedValue
});
}
return {
saveQuery: false
};
};
return applyQuery(params, getFirstSectionValueMatchingWithQuery, queryValue => (0, _useField.isStringNumber)(queryValue, localizedDigits));
};
return (0, _useEventCallback.default)(params => {
const section = state.sections[params.sectionIndex];
const isNumericEditing = (0, _useField.isStringNumber)(params.keyPressed, localizedDigits);
const response = isNumericEditing ? applyNumericEditing((0, _extends2.default)({}, params, {
keyPressed: (0, _useField.applyLocalizedDigits)(params.keyPressed, localizedDigits)
})) : applyLetterEditing(params);
if (response == null) {
setTempAndroidValueStr(null);
return;
}
updateSectionValue({
section,
newSectionValue: response.sectionValue,
shouldGoToNextSection: response.shouldGoToNextSection
});
});
};
/**
* The letter editing and the numeric editing each define a `CharacterEditingApplier`.
* This function decides what the new section value should be and if the focus should switch to the next section.
*
* If it returns `null`, then the section value is not updated and the focus does not move.
*/
/**
* Function called by `applyQuery` which decides:
* - what is the new section value ?
* - should the query used to get this value be stored for the next key press ?
*
* If it returns `{ sectionValue: string; shouldGoToNextSection: boolean }`,
* Then we store the query and update the section with the new value.
*
* If it returns `{ saveQuery: true` },
* Then we store the query and don't update the section.
*
* If it returns `{ saveQuery: false },
* Then we do nothing.
*/
exports.useFieldCharacterEditing = useFieldCharacterEditing;

View file

@ -0,0 +1,20 @@
import * as React from 'react';
import { PickerManager } from "../../../models/index.js";
import { UseFieldStateReturnValue } from "./useFieldState.js";
/**
* Generate the props to pass to the hidden input element of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldHiddenInputPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldHiddenInputPropsReturnValue} The props to forward to the hidden input element of the field.
*/
export declare function useFieldHiddenInputProps(parameters: UseFieldHiddenInputPropsParameters): UseFieldHiddenInputPropsReturnValue;
interface UseFieldHiddenInputPropsParameters {
manager: PickerManager<any, any, any, any, any>;
stateResponse: UseFieldStateReturnValue<any>;
}
interface UseFieldHiddenInputPropsReturnValue {
value: string;
onChange: React.ChangeEventHandler<HTMLInputElement>;
}
export {};

View file

@ -0,0 +1,39 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldHiddenInputProps = useFieldHiddenInputProps;
var React = _interopRequireWildcard(require("react"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
/**
* Generate the props to pass to the hidden input element of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldHiddenInputPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldHiddenInputPropsReturnValue} The props to forward to the hidden input element of the field.
*/
function useFieldHiddenInputProps(parameters) {
const {
manager: {
internal_fieldValueManager: fieldValueManager
},
stateResponse: {
// States and derived states
areAllSectionsEmpty,
state,
// Methods to update the states
updateValueFromValueStr
}
} = parameters;
const handleChange = (0, _useEventCallback.default)(event => {
updateValueFromValueStr(event.target.value);
});
const valueStr = React.useMemo(() => areAllSectionsEmpty ? '' : fieldValueManager.getV7HiddenInputValueFromSections(state.sections), [areAllSectionsEmpty, state.sections, fieldValueManager]);
return {
value: valueStr,
onChange: handleChange
};
}

View file

@ -0,0 +1,17 @@
import { PickerAnyManager, PickerManagerFieldInternalProps, PickerManagerFieldInternalPropsWithDefaults } from "../../models/index.js";
/**
* Applies the default values to the field internal props.
* This is a temporary hook that will be removed during a follow up when `useField` will receive the internal props without the defaults.
* It is only here to allow the migration to be done in smaller steps.
*/
export declare function useFieldInternalPropsWithDefaults<TManager extends PickerAnyManager>(parameters: UseFieldInternalPropsWithDefaultsParameters<TManager>): PickerManagerFieldInternalPropsWithDefaults<TManager>;
interface UseFieldInternalPropsWithDefaultsParameters<TManager extends PickerAnyManager> {
manager: TManager;
internalProps: PickerManagerFieldInternalProps<TManager>;
/**
* Hack to make sure that on multi input range field, the `useNullableFieldPrivateContext().fieldRef` is only bound to the field matching the range position.
* @default false
*/
skipContextFieldRefAssignment?: boolean;
}
export {};

View file

@ -0,0 +1,60 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldInternalPropsWithDefaults = useFieldInternalPropsWithDefaults;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _useForkRef = _interopRequireDefault(require("@mui/utils/useForkRef"));
var _useNullablePickerContext = require("../useNullablePickerContext");
var _useNullableFieldPrivateContext = require("../useNullableFieldPrivateContext");
/**
* Applies the default values to the field internal props.
* This is a temporary hook that will be removed during a follow up when `useField` will receive the internal props without the defaults.
* It is only here to allow the migration to be done in smaller steps.
*/
function useFieldInternalPropsWithDefaults(parameters) {
const {
manager: {
internal_useApplyDefaultValuesToFieldInternalProps: useApplyDefaultValuesToFieldInternalProps
},
internalProps,
skipContextFieldRefAssignment
} = parameters;
const pickerContext = (0, _useNullablePickerContext.useNullablePickerContext)();
const fieldPrivateContext = (0, _useNullableFieldPrivateContext.useNullableFieldPrivateContext)();
const handleFieldRef = (0, _useForkRef.default)(internalProps.unstableFieldRef, skipContextFieldRefAssignment ? null : fieldPrivateContext?.fieldRef);
const setValue = pickerContext?.setValue;
const handleChangeFromPicker = React.useCallback((newValue, ctx) => {
return setValue?.(newValue, {
validationError: ctx.validationError,
shouldClose: false
});
}, [setValue]);
const internalPropsWithDefaultsFromContext = React.useMemo(() => {
// If one of the context is null,
// Then the field is used as a standalone component and the other context will be null as well.
if (fieldPrivateContext != null && pickerContext != null) {
return (0, _extends2.default)({
value: pickerContext.value,
onChange: handleChangeFromPicker,
timezone: pickerContext.timezone,
disabled: pickerContext.disabled,
readOnly: pickerContext.readOnly,
autoFocus: pickerContext.autoFocus && !pickerContext.open,
focused: pickerContext.open ? true : undefined,
format: pickerContext.fieldFormat,
formatDensity: fieldPrivateContext.formatDensity,
enableAccessibleFieldDOMStructure: fieldPrivateContext.enableAccessibleFieldDOMStructure,
selectedSections: fieldPrivateContext.selectedSections,
onSelectedSectionsChange: fieldPrivateContext.onSelectedSectionsChange,
unstableFieldRef: handleFieldRef
}, internalProps);
}
return internalProps;
}, [pickerContext, fieldPrivateContext, internalProps, handleChangeFromPicker, handleFieldRef]);
return useApplyDefaultValuesToFieldInternalProps(internalPropsWithDefaultsFromContext);
}

View file

@ -0,0 +1,16 @@
import { PickerManager } from "../../../models/index.js";
import { PickerValidValue } from "../../models/index.js";
import { UseFieldStateReturnValue } from "./useFieldState.js";
import { UseFieldInternalProps } from "./useField.types.js";
/**
* Returns the `onKeyDown` handler to pass to the root element of the field.
*/
export declare function useFieldRootHandleKeyDown<TValue extends PickerValidValue>(parameters: UseFieldRootHandleKeyDownParameters<TValue>): (event: React.KeyboardEvent<HTMLSpanElement>) => void;
interface UseFieldRootHandleKeyDownParameters<TValue extends PickerValidValue> {
manager: PickerManager<TValue, any, any, any, any>;
stateResponse: UseFieldStateReturnValue<TValue>;
internalPropsWithDefaults: UseFieldInternalProps<TValue, any, any> & {
minutesStep?: number;
};
}
export {};

View file

@ -0,0 +1,211 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldRootHandleKeyDown = useFieldRootHandleKeyDown;
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _useField = require("./useField.utils");
var _usePickerAdapter = require("../../../hooks/usePickerAdapter");
/**
* Returns the `onKeyDown` handler to pass to the root element of the field.
*/
function useFieldRootHandleKeyDown(parameters) {
const adapter = (0, _usePickerAdapter.usePickerAdapter)();
const {
manager: {
internal_fieldValueManager: fieldValueManager
},
internalPropsWithDefaults: {
minutesStep,
disabled,
readOnly
},
stateResponse: {
// States and derived states
state,
value,
activeSectionIndex,
parsedSelectedSections,
sectionsValueBoundaries,
localizedDigits,
timezone,
sectionOrder,
// Methods to update the states
clearValue,
clearActiveSection,
setSelectedSections,
updateSectionValue
}
} = parameters;
return (0, _useEventCallback.default)(event => {
if (disabled) {
return;
}
// eslint-disable-next-line default-case
switch (true) {
// Select all
case (event.ctrlKey || event.metaKey) && String.fromCharCode(event.keyCode) === 'A' && !event.shiftKey && !event.altKey:
{
// prevent default to make sure that the next line "select all" while updating
// the internal state at the same time.
event.preventDefault();
setSelectedSections('all');
break;
}
// Move selection to next section
case event.key === 'ArrowRight':
{
event.preventDefault();
if (parsedSelectedSections == null) {
setSelectedSections(sectionOrder.startIndex);
} else if (parsedSelectedSections === 'all') {
setSelectedSections(sectionOrder.endIndex);
} else {
const nextSectionIndex = sectionOrder.neighbors[parsedSelectedSections].rightIndex;
if (nextSectionIndex !== null) {
setSelectedSections(nextSectionIndex);
}
}
break;
}
// Move selection to previous section
case event.key === 'ArrowLeft':
{
event.preventDefault();
if (parsedSelectedSections == null) {
setSelectedSections(sectionOrder.endIndex);
} else if (parsedSelectedSections === 'all') {
setSelectedSections(sectionOrder.startIndex);
} else {
const nextSectionIndex = sectionOrder.neighbors[parsedSelectedSections].leftIndex;
if (nextSectionIndex !== null) {
setSelectedSections(nextSectionIndex);
}
}
break;
}
// Reset the value of the selected section
case event.key === 'Delete':
{
event.preventDefault();
if (readOnly) {
break;
}
if (parsedSelectedSections == null || parsedSelectedSections === 'all') {
clearValue();
} else {
clearActiveSection();
}
break;
}
// Increment / decrement the selected section value
case ['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key):
{
event.preventDefault();
if (readOnly || activeSectionIndex == null) {
break;
}
// if all sections are selected, mark the currently editing one as selected
if (parsedSelectedSections === 'all') {
setSelectedSections(activeSectionIndex);
}
const activeSection = state.sections[activeSectionIndex];
const newSectionValue = adjustSectionValue(adapter, timezone, activeSection, event.key, sectionsValueBoundaries, localizedDigits, fieldValueManager.getDateFromSection(value, activeSection), {
minutesStep
});
updateSectionValue({
section: activeSection,
newSectionValue,
shouldGoToNextSection: false
});
break;
}
}
});
}
function getDeltaFromKeyCode(keyCode) {
switch (keyCode) {
case 'ArrowUp':
return 1;
case 'ArrowDown':
return -1;
case 'PageUp':
return 5;
case 'PageDown':
return -5;
default:
return 0;
}
}
function adjustSectionValue(adapter, timezone, section, keyCode, sectionsValueBoundaries, localizedDigits, activeDate, stepsAttributes) {
const delta = getDeltaFromKeyCode(keyCode);
const isStart = keyCode === 'Home';
const isEnd = keyCode === 'End';
const shouldSetAbsolute = section.value === '' || isStart || isEnd;
const adjustDigitSection = () => {
const sectionBoundaries = sectionsValueBoundaries[section.type]({
currentDate: activeDate,
format: section.format,
contentType: section.contentType
});
const getCleanValue = value => (0, _useField.cleanDigitSectionValue)(adapter, value, sectionBoundaries, localizedDigits, section);
const step = section.type === 'minutes' && stepsAttributes?.minutesStep ? stepsAttributes.minutesStep : 1;
let newSectionValueNumber;
if (shouldSetAbsolute) {
if (section.type === 'year' && !isEnd && !isStart) {
return adapter.formatByString(adapter.date(undefined, timezone), section.format);
}
if (delta > 0 || isStart) {
newSectionValueNumber = sectionBoundaries.minimum;
} else {
newSectionValueNumber = sectionBoundaries.maximum;
}
} else {
const currentSectionValue = parseInt((0, _useField.removeLocalizedDigits)(section.value, localizedDigits), 10);
newSectionValueNumber = currentSectionValue + delta * step;
}
if (newSectionValueNumber % step !== 0) {
if (delta < 0 || isStart) {
newSectionValueNumber += step - (step + newSectionValueNumber) % step; // for JS -3 % 5 = -3 (should be 2)
}
if (delta > 0 || isEnd) {
newSectionValueNumber -= newSectionValueNumber % step;
}
}
if (newSectionValueNumber > sectionBoundaries.maximum) {
return getCleanValue(sectionBoundaries.minimum + (newSectionValueNumber - sectionBoundaries.maximum - 1) % (sectionBoundaries.maximum - sectionBoundaries.minimum + 1));
}
if (newSectionValueNumber < sectionBoundaries.minimum) {
return getCleanValue(sectionBoundaries.maximum - (sectionBoundaries.minimum - newSectionValueNumber - 1) % (sectionBoundaries.maximum - sectionBoundaries.minimum + 1));
}
return getCleanValue(newSectionValueNumber);
};
const adjustLetterSection = () => {
const options = (0, _useField.getLetterEditingOptions)(adapter, timezone, section.type, section.format);
if (options.length === 0) {
return section.value;
}
if (shouldSetAbsolute) {
if (delta > 0 || isStart) {
return options[0];
}
return options[options.length - 1];
}
const currentOptionIndex = options.indexOf(section.value);
const newOptionIndex = (currentOptionIndex + delta) % options.length;
const clampedIndex = (newOptionIndex + options.length) % options.length;
return options[clampedIndex];
};
if (section.contentType === 'digit' || section.contentType === 'digit-with-letter') {
return adjustDigitSection();
}
return adjustLetterSection();
}

View file

@ -0,0 +1,32 @@
import { PickerManager } from "../../../models/index.js";
import { UseFieldDOMGetters, UseFieldInternalProps } from "./useField.types.js";
import { UseFieldStateReturnValue } from "./useFieldState.js";
import { UseFieldCharacterEditingReturnValue } from "./useFieldCharacterEditing.js";
/**
* Generate the props to pass to the root element of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldRootPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldRootPropsReturnValue} The props to forward to the root element of the field.
*/
export declare function useFieldRootProps(parameters: UseFieldRootPropsParameters): UseFieldRootPropsReturnValue;
interface UseFieldRootPropsParameters {
manager: PickerManager<any, any, any, any, any>;
stateResponse: UseFieldStateReturnValue<any>;
applyCharacterEditing: UseFieldCharacterEditingReturnValue;
internalPropsWithDefaults: UseFieldInternalProps<any, any, any>;
domGetters: UseFieldDOMGetters;
focused: boolean;
setFocused: (focused: boolean) => void;
}
interface UseFieldRootPropsReturnValue {
onKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
onBlur: React.FocusEventHandler<HTMLDivElement>;
onFocus: React.FocusEventHandler<HTMLDivElement>;
onClick: React.MouseEventHandler<HTMLDivElement>;
onPaste: React.ClipboardEventHandler<HTMLDivElement>;
onInput: React.FormEventHandler<HTMLDivElement>;
contentEditable: boolean;
tabIndex: number;
}
export {};

View file

@ -0,0 +1,157 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldRootProps = useFieldRootProps;
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _useTimeout = _interopRequireDefault(require("@mui/utils/useTimeout"));
var _useFieldRootHandleKeyDown = require("./useFieldRootHandleKeyDown");
var _utils = require("../../utils/utils");
var _syncSelectionToDOM = require("./syncSelectionToDOM");
/**
* Generate the props to pass to the root element of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldRootPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldRootPropsReturnValue} The props to forward to the root element of the field.
*/
function useFieldRootProps(parameters) {
const {
manager,
focused,
setFocused,
domGetters,
stateResponse,
applyCharacterEditing,
internalPropsWithDefaults,
stateResponse: {
// States and derived states
parsedSelectedSections,
sectionOrder,
state,
// Methods to update the states
clearValue,
setCharacterQuery,
setSelectedSections,
updateValueFromValueStr
},
internalPropsWithDefaults: {
disabled = false,
readOnly = false
}
} = parameters;
// TODO: Inline onContainerKeyDown once the old DOM structure is removed
const handleKeyDown = (0, _useFieldRootHandleKeyDown.useFieldRootHandleKeyDown)({
manager,
internalPropsWithDefaults,
stateResponse
});
const containerClickTimeout = (0, _useTimeout.default)();
const handleClick = (0, _useEventCallback.default)(event => {
if (disabled || !domGetters.isReady()) {
return;
}
setFocused(true);
if (parsedSelectedSections === 'all') {
containerClickTimeout.start(0, () => {
const cursorPosition = document.getSelection().getRangeAt(0).startOffset;
if (cursorPosition === 0) {
setSelectedSections(sectionOrder.startIndex);
return;
}
let sectionIndex = 0;
let cursorOnStartOfSection = 0;
while (cursorOnStartOfSection < cursorPosition && sectionIndex < state.sections.length) {
const section = state.sections[sectionIndex];
sectionIndex += 1;
cursorOnStartOfSection += `${section.startSeparator}${section.value || section.placeholder}${section.endSeparator}`.length;
}
setSelectedSections(sectionIndex - 1);
});
} else if (!focused) {
setFocused(true);
setSelectedSections(sectionOrder.startIndex);
} else {
const hasClickedOnASection = domGetters.getRoot().contains(event.target);
if (!hasClickedOnASection) {
setSelectedSections(sectionOrder.startIndex);
}
}
});
const handleInput = (0, _useEventCallback.default)(event => {
if (!domGetters.isReady() || parsedSelectedSections !== 'all') {
return;
}
const target = event.target;
const keyPressed = target.textContent ?? '';
domGetters.getRoot().innerHTML = state.sections.map(section => `${section.startSeparator}${section.value || section.placeholder}${section.endSeparator}`).join('');
(0, _syncSelectionToDOM.syncSelectionToDOM)({
focused,
domGetters,
stateResponse
});
if (keyPressed.length === 0 || keyPressed.charCodeAt(0) === 10) {
clearValue();
setSelectedSections('all');
} else if (keyPressed.length > 1) {
updateValueFromValueStr(keyPressed);
} else {
if (parsedSelectedSections === 'all') {
setSelectedSections(0);
}
applyCharacterEditing({
keyPressed,
sectionIndex: 0
});
}
});
const handlePaste = (0, _useEventCallback.default)(event => {
if (readOnly || parsedSelectedSections !== 'all') {
event.preventDefault();
return;
}
const pastedValue = event.clipboardData.getData('text');
event.preventDefault();
setCharacterQuery(null);
updateValueFromValueStr(pastedValue);
});
const handleFocus = (0, _useEventCallback.default)(() => {
if (focused || disabled || !domGetters.isReady()) {
return;
}
const activeElement = (0, _utils.getActiveElement)(domGetters.getRoot());
setFocused(true);
const isFocusInsideASection = domGetters.getSectionIndexFromDOMElement(activeElement) != null;
if (!isFocusInsideASection) {
setSelectedSections(sectionOrder.startIndex);
}
});
const handleBlur = (0, _useEventCallback.default)(() => {
setTimeout(() => {
if (!domGetters.isReady()) {
return;
}
const activeElement = (0, _utils.getActiveElement)(domGetters.getRoot());
const shouldBlur = !domGetters.getRoot().contains(activeElement);
if (shouldBlur) {
setFocused(false);
setSelectedSections(null);
}
});
});
return {
// Event handlers
onKeyDown: handleKeyDown,
onBlur: handleBlur,
onFocus: handleFocus,
onClick: handleClick,
onPaste: handlePaste,
onInput: handleInput,
// Other
contentEditable: parsedSelectedSections === 'all',
tabIndex: parsedSelectedSections === 0 ? -1 : 0 // TODO: Try to set to undefined when there is a section selected.
};
}

View file

@ -0,0 +1,17 @@
import * as React from 'react';
import { UseFieldStateReturnValue } from "./useFieldState.js";
import { UseFieldInternalProps } from "./useField.types.js";
/**
* Generate the props to pass to the container element of each section of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldRootPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldRootPropsReturnValue} The props to forward to the container element of each section of the field.
*/
export declare function useFieldSectionContainerProps(parameters: UseFieldSectionContainerPropsParameters): UseFieldSectionContainerPropsReturnValue;
interface UseFieldSectionContainerPropsParameters {
stateResponse: UseFieldStateReturnValue<any>;
internalPropsWithDefaults: UseFieldInternalProps<any, any, any>;
}
type UseFieldSectionContainerPropsReturnValue = (sectionIndex: number) => React.HTMLAttributes<HTMLSpanElement>;
export {};

View file

@ -0,0 +1,38 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldSectionContainerProps = useFieldSectionContainerProps;
var React = _interopRequireWildcard(require("react"));
/**
* Generate the props to pass to the container element of each section of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldRootPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldRootPropsReturnValue} The props to forward to the container element of each section of the field.
*/
function useFieldSectionContainerProps(parameters) {
const {
stateResponse: {
// Methods to update the states
setSelectedSections
},
internalPropsWithDefaults: {
disabled = false
}
} = parameters;
const createHandleClick = React.useCallback(sectionIndex => event => {
// The click event on the clear button would propagate to the input, trigger this handler and result in a wrong section selection.
// We avoid this by checking if the call to this function is actually intended, or a side effect.
if (disabled || event.isDefaultPrevented()) {
return;
}
setSelectedSections(sectionIndex);
}, [disabled, setSelectedSections]);
return React.useCallback(sectionIndex => ({
'data-sectionindex': sectionIndex,
onClick: createHandleClick(sectionIndex)
}), [createHandleClick]);
}

View file

@ -0,0 +1,23 @@
import { UseFieldStateReturnValue } from "./useFieldState.js";
import { FieldSection, PickerManager } from "../../../models/index.js";
import { UseFieldDOMGetters, UseFieldInternalProps } from "./useField.types.js";
import { UseFieldCharacterEditingReturnValue } from "./useFieldCharacterEditing.js";
import { PickersSectionElement } from "../../../PickersSectionList/index.js";
/**
* Generate the props to pass to the content element of each section of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldRootPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldRootPropsReturnValue} The props to forward to the content element of each section of the field.
*/
export declare function useFieldSectionContentProps(parameters: UseFieldSectionContentPropsParameters): UseFieldSectionContentPropsReturnValue;
interface UseFieldSectionContentPropsParameters {
manager: PickerManager<any, any, any, any, any>;
stateResponse: UseFieldStateReturnValue<any>;
applyCharacterEditing: UseFieldCharacterEditingReturnValue;
internalPropsWithDefaults: UseFieldInternalProps<any, any, any>;
domGetters: UseFieldDOMGetters;
focused: boolean;
}
type UseFieldSectionContentPropsReturnValue = (section: FieldSection, sectionIndex: number) => PickersSectionElement['content'];
export {};

View file

@ -0,0 +1,236 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldSectionContentProps = useFieldSectionContentProps;
var React = _interopRequireWildcard(require("react"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _useId = _interopRequireDefault(require("@mui/utils/useId"));
var _hooks = require("../../../hooks");
var _syncSelectionToDOM = require("./syncSelectionToDOM");
/**
* Generate the props to pass to the content element of each section of the field.
* It is not used by the non-accessible DOM structure (with an <input /> element for editing).
* It should be used in the MUI accessible DOM structure and the Base UI implementation.
* @param {UseFieldRootPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldRootPropsReturnValue} The props to forward to the content element of each section of the field.
*/
function useFieldSectionContentProps(parameters) {
const adapter = (0, _hooks.usePickerAdapter)();
const translations = (0, _hooks.usePickerTranslations)();
const id = (0, _useId.default)();
const {
focused,
domGetters,
stateResponse,
applyCharacterEditing,
manager: {
internal_fieldValueManager: fieldValueManager
},
stateResponse: {
// States and derived states
parsedSelectedSections,
sectionsValueBoundaries,
state,
value,
// Methods to update the states
clearActiveSection,
setCharacterQuery,
setSelectedSections,
updateSectionValue,
updateValueFromValueStr
},
internalPropsWithDefaults: {
disabled = false,
readOnly = false
}
} = parameters;
const isContainerEditable = parsedSelectedSections === 'all';
const isEditable = !isContainerEditable && !disabled && !readOnly;
/**
* If a section content has been updated with a value we don't want to keep,
* Then we need to imperatively revert it (we can't let React do it because the value did not change in his internal representation).
*/
const revertDOMSectionChange = (0, _useEventCallback.default)(sectionIndex => {
if (!domGetters.isReady()) {
return;
}
const section = state.sections[sectionIndex];
domGetters.getSectionContent(sectionIndex).innerHTML = section.value || section.placeholder;
(0, _syncSelectionToDOM.syncSelectionToDOM)({
focused,
domGetters,
stateResponse
});
});
const handleInput = (0, _useEventCallback.default)(event => {
if (!domGetters.isReady()) {
return;
}
const target = event.target;
const keyPressed = target.textContent ?? '';
const sectionIndex = domGetters.getSectionIndexFromDOMElement(target);
const section = state.sections[sectionIndex];
if (readOnly) {
revertDOMSectionChange(sectionIndex);
return;
}
if (keyPressed.length === 0) {
if (section.value === '') {
revertDOMSectionChange(sectionIndex);
return;
}
const inputType = event.nativeEvent.inputType;
if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
revertDOMSectionChange(sectionIndex);
return;
}
revertDOMSectionChange(sectionIndex);
clearActiveSection();
return;
}
applyCharacterEditing({
keyPressed,
sectionIndex
});
// The DOM value needs to remain the one React is expecting.
revertDOMSectionChange(sectionIndex);
});
const handleMouseUp = (0, _useEventCallback.default)(event => {
// Without this, the browser will remove the selected when clicking inside an already-selected section.
event.preventDefault();
});
const handlePaste = (0, _useEventCallback.default)(event => {
// prevent default to avoid the input `onInput` handler being called
event.preventDefault();
if (readOnly || disabled || typeof parsedSelectedSections !== 'number') {
return;
}
const activeSection = state.sections[parsedSelectedSections];
const pastedValue = event.clipboardData.getData('text');
const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue);
const digitsOnly = /^[0-9]+$/.test(pastedValue);
const digitsAndLetterOnly = /^(([a-zA-Z]+)|)([0-9]+)(([a-zA-Z]+)|)$/.test(pastedValue);
const isValidPastedValue = activeSection.contentType === 'letter' && lettersOnly || activeSection.contentType === 'digit' && digitsOnly || activeSection.contentType === 'digit-with-letter' && digitsAndLetterOnly;
if (isValidPastedValue) {
setCharacterQuery(null);
updateSectionValue({
section: activeSection,
newSectionValue: pastedValue,
shouldGoToNextSection: true
});
}
// If the pasted value corresponds to a single section, but not the expected type, we skip the modification
else if (!lettersOnly && !digitsOnly) {
setCharacterQuery(null);
updateValueFromValueStr(pastedValue);
}
});
const handleDragOver = (0, _useEventCallback.default)(event => {
event.preventDefault();
event.dataTransfer.dropEffect = 'none';
});
const createFocusHandler = React.useCallback(sectionIndex => () => {
if (disabled) {
return;
}
setSelectedSections(sectionIndex);
}, [disabled, setSelectedSections]);
return React.useCallback((section, sectionIndex) => {
const sectionBoundaries = sectionsValueBoundaries[section.type]({
currentDate: fieldValueManager.getDateFromSection(value, section),
contentType: section.contentType,
format: section.format
});
return {
// Event handlers
onInput: handleInput,
onPaste: handlePaste,
onMouseUp: handleMouseUp,
onDragOver: handleDragOver,
onFocus: createFocusHandler(sectionIndex),
// Aria attributes
'aria-labelledby': `${id}-${section.type}`,
'aria-readonly': readOnly,
'aria-valuenow': getSectionValueNow(section, adapter),
'aria-valuemin': sectionBoundaries.minimum,
'aria-valuemax': sectionBoundaries.maximum,
'aria-valuetext': section.value ? getSectionValueText(section, adapter) : translations.empty,
'aria-label': translations[section.type],
'aria-disabled': disabled,
// Other
tabIndex: isContainerEditable || sectionIndex > 0 ? -1 : 0,
contentEditable: !isContainerEditable && !disabled && !readOnly,
role: 'spinbutton',
id: `${id}-${section.type}`,
'data-range-position': section.dateName || undefined,
spellCheck: isEditable ? false : undefined,
autoCapitalize: isEditable ? 'off' : undefined,
autoCorrect: isEditable ? 'off' : undefined,
children: section.value || section.placeholder,
inputMode: section.contentType === 'letter' ? 'text' : 'numeric'
};
}, [sectionsValueBoundaries, id, isContainerEditable, disabled, readOnly, isEditable, translations, adapter, handleInput, handlePaste, handleMouseUp, handleDragOver, createFocusHandler, fieldValueManager, value]);
}
function getSectionValueText(section, adapter) {
if (!section.value) {
return undefined;
}
switch (section.type) {
case 'month':
{
if (section.contentType === 'digit') {
return adapter.format(adapter.setMonth(adapter.date(), Number(section.value) - 1), 'month');
}
const parsedDate = adapter.parse(section.value, section.format);
return parsedDate ? adapter.format(parsedDate, 'month') : undefined;
}
case 'day':
return section.contentType === 'digit' ? adapter.format(adapter.setDate(adapter.startOfYear(adapter.date()), Number(section.value)), 'dayOfMonthFull') : section.value;
case 'weekDay':
// TODO: improve by providing the label of the week day
return undefined;
default:
return undefined;
}
}
function getSectionValueNow(section, adapter) {
if (!section.value) {
return undefined;
}
switch (section.type) {
case 'weekDay':
{
if (section.contentType === 'letter') {
// TODO: improve by resolving the week day number from a letter week day
return undefined;
}
return Number(section.value);
}
case 'meridiem':
{
const parsedDate = adapter.parse(`01:00 ${section.value}`, `${adapter.formats.hours12h}:${adapter.formats.minutes} ${section.format}`);
if (parsedDate) {
return adapter.getHours(parsedDate) >= 12 ? 1 : 0;
}
return undefined;
}
case 'day':
return section.contentType === 'digit-with-letter' ? parseInt(section.value, 10) : Number(section.value);
case 'month':
{
if (section.contentType === 'digit') {
return Number(section.value);
}
const parsedDate = adapter.parse(section.value, section.format);
return parsedDate ? adapter.getMonth(parsedDate) + 1 : undefined;
}
default:
return section.contentType !== 'letter' ? Number(section.value) : undefined;
}
}

View file

@ -0,0 +1,44 @@
import { UseFieldInternalProps, UseFieldState, FieldParsedSelectedSections, FieldSectionsValueBoundaries, SectionOrdering, UseFieldForwardedProps, CharacterEditingQuery } from "./useField.types.js";
import { FieldSelectedSections, PickersTimezone, InferFieldSection, PickerManager } from "../../../models/index.js";
import { PickerValidValue } from "../../models/index.js";
export declare const useFieldState: <TValue extends PickerValidValue, TEnableAccessibleFieldDOMStructure extends boolean, TError, TValidationProps extends {}, TForwardedProps extends UseFieldForwardedProps<TEnableAccessibleFieldDOMStructure>>(parameters: UseFieldStateParameters<TValue, TEnableAccessibleFieldDOMStructure, TError, TValidationProps, TForwardedProps>) => UseFieldStateReturnValue<TValue>;
interface UseFieldStateParameters<TValue extends PickerValidValue, TEnableAccessibleFieldDOMStructure extends boolean, TError, TValidationProps extends {}, TForwardedProps extends UseFieldForwardedProps<TEnableAccessibleFieldDOMStructure>> {
manager: PickerManager<TValue, TEnableAccessibleFieldDOMStructure, TError, TValidationProps, any>;
internalPropsWithDefaults: UseFieldInternalProps<TValue, TEnableAccessibleFieldDOMStructure, TError> & TValidationProps;
forwardedProps: TForwardedProps;
}
export interface UpdateSectionValueParameters<TValue extends PickerValidValue> {
/**
* The section on which we want to apply the new value.
*/
section: InferFieldSection<TValue>;
/**
* Value to apply to the active section.
*/
newSectionValue: string;
/**
* If `true`, the focus will move to the next section.
*/
shouldGoToNextSection: boolean;
}
export interface UseFieldStateReturnValue<TValue extends PickerValidValue> {
activeSectionIndex: number | null;
areAllSectionsEmpty: boolean;
error: boolean;
localizedDigits: string[];
parsedSelectedSections: FieldParsedSelectedSections;
sectionOrder: SectionOrdering;
sectionsValueBoundaries: FieldSectionsValueBoundaries;
state: UseFieldState<TValue>;
timezone: PickersTimezone;
value: TValue;
clearValue: () => void;
clearActiveSection: () => void;
setCharacterQuery: (characterQuery: CharacterEditingQuery | null) => void;
setSelectedSections: (sections: FieldSelectedSections) => void;
setTempAndroidValueStr: (tempAndroidValueStr: string | null) => void;
updateSectionValue: (parameters: UpdateSectionValueParameters<TValue>) => void;
updateValueFromValueStr: (valueStr: string) => void;
getSectionsFromValue: (value: TValue, fallbackSections?: InferFieldSection<TValue>[] | null) => InferFieldSection<TValue>[];
}
export {};

View file

@ -0,0 +1,400 @@
"use strict";
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldState = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _useControlled = _interopRequireDefault(require("@mui/utils/useControlled"));
var _useTimeout = _interopRequireDefault(require("@mui/utils/useTimeout"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _RtlProvider = require("@mui/system/RtlProvider");
var _hooks = require("../../../hooks");
var _useField = require("./useField.utils");
var _buildSectionsFromFormat = require("./buildSectionsFromFormat");
var _validation = require("../../../validation");
var _useControlledValue = require("../useControlledValue");
var _getDefaultReferenceDate = require("../../utils/getDefaultReferenceDate");
const QUERY_LIFE_DURATION_MS = 5000;
const useFieldState = parameters => {
const adapter = (0, _hooks.usePickerAdapter)();
const translations = (0, _hooks.usePickerTranslations)();
const isRtl = (0, _RtlProvider.useRtl)();
const {
manager: {
validator,
valueType,
internal_valueManager: valueManager,
internal_fieldValueManager: fieldValueManager
},
internalPropsWithDefaults,
internalPropsWithDefaults: {
value: valueProp,
defaultValue,
referenceDate: referenceDateProp,
onChange,
format,
formatDensity = 'dense',
selectedSections: selectedSectionsProp,
onSelectedSectionsChange,
shouldRespectLeadingZeros = false,
timezone: timezoneProp,
enableAccessibleFieldDOMStructure = true
},
forwardedProps: {
error: errorProp
}
} = parameters;
const {
value,
handleValueChange,
timezone
} = (0, _useControlledValue.useControlledValue)({
name: 'a field component',
timezone: timezoneProp,
value: valueProp,
defaultValue,
referenceDate: referenceDateProp,
onChange,
valueManager
});
const valueRef = React.useRef(value);
React.useEffect(() => {
valueRef.current = value;
}, [value]);
const {
hasValidationError
} = (0, _validation.useValidation)({
props: internalPropsWithDefaults,
validator,
timezone,
value,
onError: internalPropsWithDefaults.onError
});
const error = React.useMemo(() => {
// only override when `error` is undefined.
// in case of multi input fields, the `error` value is provided externally and will always be defined.
if (errorProp !== undefined) {
return errorProp;
}
return hasValidationError;
}, [hasValidationError, errorProp]);
const localizedDigits = React.useMemo(() => (0, _useField.getLocalizedDigits)(adapter), [adapter]);
const sectionsValueBoundaries = React.useMemo(() => (0, _useField.getSectionsBoundaries)(adapter, localizedDigits, timezone), [adapter, localizedDigits, timezone]);
const getSectionsFromValue = React.useCallback(valueToAnalyze => fieldValueManager.getSectionsFromValue(valueToAnalyze, date => (0, _buildSectionsFromFormat.buildSectionsFromFormat)({
adapter,
localeText: translations,
localizedDigits,
format,
date,
formatDensity,
shouldRespectLeadingZeros,
enableAccessibleFieldDOMStructure,
isRtl
})), [fieldValueManager, format, translations, localizedDigits, isRtl, shouldRespectLeadingZeros, adapter, formatDensity, enableAccessibleFieldDOMStructure]);
const [state, setState] = React.useState(() => {
const sections = getSectionsFromValue(value);
(0, _useField.validateSections)(sections, valueType);
const stateWithoutReferenceDate = {
sections,
lastExternalValue: value,
lastSectionsDependencies: {
format,
isRtl,
locale: adapter.locale
},
tempValueStrAndroid: null,
characterQuery: null
};
const granularity = (0, _getDefaultReferenceDate.getSectionTypeGranularity)(sections);
const referenceValue = valueManager.getInitialReferenceValue({
referenceDate: referenceDateProp,
value,
adapter,
props: internalPropsWithDefaults,
granularity,
timezone
});
return (0, _extends2.default)({}, stateWithoutReferenceDate, {
referenceValue
});
});
const [selectedSections, innerSetSelectedSections] = (0, _useControlled.default)({
controlled: selectedSectionsProp,
default: null,
name: 'useField',
state: 'selectedSections'
});
const setSelectedSections = newSelectedSections => {
innerSetSelectedSections(newSelectedSections);
onSelectedSectionsChange?.(newSelectedSections);
};
const parsedSelectedSections = React.useMemo(() => (0, _useField.parseSelectedSections)(selectedSections, state.sections), [selectedSections, state.sections]);
const activeSectionIndex = parsedSelectedSections === 'all' ? 0 : parsedSelectedSections;
const sectionOrder = React.useMemo(() => (0, _useField.getSectionOrder)(state.sections, isRtl && !enableAccessibleFieldDOMStructure), [state.sections, isRtl, enableAccessibleFieldDOMStructure]);
const areAllSectionsEmpty = React.useMemo(() => state.sections.every(section => section.value === ''), [state.sections]);
const publishValue = newValue => {
const context = {
validationError: validator({
adapter,
value: newValue,
timezone,
props: internalPropsWithDefaults
})
};
handleValueChange(newValue, context);
};
const setSectionValue = (sectionIndex, newSectionValue) => {
const newSections = [...state.sections];
newSections[sectionIndex] = (0, _extends2.default)({}, newSections[sectionIndex], {
value: newSectionValue,
modified: true
});
return newSections;
};
const sectionToUpdateOnNextInvalidDateRef = React.useRef(null);
const updateSectionValueOnNextInvalidDateTimeout = (0, _useTimeout.default)();
const setSectionUpdateToApplyOnNextInvalidDate = newSectionValue => {
if (activeSectionIndex == null) {
return;
}
sectionToUpdateOnNextInvalidDateRef.current = {
sectionIndex: activeSectionIndex,
value: newSectionValue
};
updateSectionValueOnNextInvalidDateTimeout.start(0, () => {
sectionToUpdateOnNextInvalidDateRef.current = null;
});
};
const clearValue = () => {
if (valueManager.areValuesEqual(adapter, value, valueManager.emptyValue)) {
setState(prevState => (0, _extends2.default)({}, prevState, {
sections: prevState.sections.map(section => (0, _extends2.default)({}, section, {
value: ''
})),
tempValueStrAndroid: null,
characterQuery: null
}));
} else {
setState(prevState => (0, _extends2.default)({}, prevState, {
characterQuery: null
}));
publishValue(valueManager.emptyValue);
}
};
const clearActiveSection = () => {
if (activeSectionIndex == null) {
return;
}
const activeSection = state.sections[activeSectionIndex];
if (activeSection.value === '') {
return;
}
setSectionUpdateToApplyOnNextInvalidDate('');
if (fieldValueManager.getDateFromSection(value, activeSection) === null) {
setState(prevState => (0, _extends2.default)({}, prevState, {
sections: setSectionValue(activeSectionIndex, ''),
tempValueStrAndroid: null,
characterQuery: null
}));
} else {
setState(prevState => (0, _extends2.default)({}, prevState, {
characterQuery: null
}));
publishValue(fieldValueManager.updateDateInValue(value, activeSection, null));
}
};
const updateValueFromValueStr = valueStr => {
const parseDateStr = (dateStr, referenceDate) => {
const date = adapter.parse(dateStr, format);
if (!adapter.isValid(date)) {
return null;
}
const sections = (0, _buildSectionsFromFormat.buildSectionsFromFormat)({
adapter,
localeText: translations,
localizedDigits,
format,
date,
formatDensity,
shouldRespectLeadingZeros,
enableAccessibleFieldDOMStructure,
isRtl
});
return (0, _useField.mergeDateIntoReferenceDate)(adapter, date, sections, referenceDate, false);
};
const newValue = fieldValueManager.parseValueStr(valueStr, state.referenceValue, parseDateStr);
publishValue(newValue);
};
const cleanActiveDateSectionsIfValueNullTimeout = (0, _useTimeout.default)();
const updateSectionValue = ({
section,
newSectionValue,
shouldGoToNextSection
}) => {
updateSectionValueOnNextInvalidDateTimeout.clear();
cleanActiveDateSectionsIfValueNullTimeout.clear();
const activeDate = fieldValueManager.getDateFromSection(value, section);
/**
* Decide which section should be focused
*/
if (shouldGoToNextSection && activeSectionIndex < state.sections.length - 1) {
setSelectedSections(activeSectionIndex + 1);
}
/**
* Try to build a valid date from the new section value
*/
const newSections = setSectionValue(activeSectionIndex, newSectionValue);
const newActiveDateSections = fieldValueManager.getDateSectionsFromValue(newSections, section);
const newActiveDate = (0, _useField.getDateFromDateSections)(adapter, newActiveDateSections, localizedDigits);
/**
* If the new date is valid,
* Then we merge the value of the modified sections into the reference date.
* This makes sure that we don't lose some information of the initial date (like the time on a date field).
*/
if (adapter.isValid(newActiveDate)) {
const mergedDate = (0, _useField.mergeDateIntoReferenceDate)(adapter, newActiveDate, newActiveDateSections, fieldValueManager.getDateFromSection(state.referenceValue, section), true);
if (activeDate == null) {
cleanActiveDateSectionsIfValueNullTimeout.start(0, () => {
if (valueRef.current === value) {
setState(prevState => (0, _extends2.default)({}, prevState, {
sections: fieldValueManager.clearDateSections(state.sections, section),
tempValueStrAndroid: null
}));
}
});
}
return publishValue(fieldValueManager.updateDateInValue(value, section, mergedDate));
}
/**
* If all the sections are filled but the date is invalid and the previous date is valid or null,
* Then we publish an invalid date.
*/
if (newActiveDateSections.every(sectionBis => sectionBis.value !== '') && (activeDate == null || adapter.isValid(activeDate))) {
setSectionUpdateToApplyOnNextInvalidDate(newSectionValue);
return publishValue(fieldValueManager.updateDateInValue(value, section, newActiveDate));
}
/**
* If the previous date is not null,
* Then we publish the date as `null`.
*/
if (activeDate != null) {
setSectionUpdateToApplyOnNextInvalidDate(newSectionValue);
return publishValue(fieldValueManager.updateDateInValue(value, section, null));
}
/**
* If the previous date is already null,
* Then we don't publish the date and we update the sections.
*/
return setState(prevState => (0, _extends2.default)({}, prevState, {
sections: newSections,
tempValueStrAndroid: null
}));
};
const setTempAndroidValueStr = tempValueStrAndroid => setState(prevState => (0, _extends2.default)({}, prevState, {
tempValueStrAndroid
}));
const setCharacterQuery = (0, _useEventCallback.default)(newCharacterQuery => {
setState(prevState => (0, _extends2.default)({}, prevState, {
characterQuery: newCharacterQuery
}));
});
// If `prop.value` changes, we update the state to reflect the new value
if (value !== state.lastExternalValue) {
let sections;
if (sectionToUpdateOnNextInvalidDateRef.current != null && !adapter.isValid(fieldValueManager.getDateFromSection(value, state.sections[sectionToUpdateOnNextInvalidDateRef.current.sectionIndex]))) {
sections = setSectionValue(sectionToUpdateOnNextInvalidDateRef.current.sectionIndex, sectionToUpdateOnNextInvalidDateRef.current.value);
} else {
sections = getSectionsFromValue(value);
}
setState(prevState => (0, _extends2.default)({}, prevState, {
lastExternalValue: value,
sections,
sectionsDependencies: {
format,
isRtl,
locale: adapter.locale
},
referenceValue: fieldValueManager.updateReferenceValue(adapter, value, prevState.referenceValue),
tempValueStrAndroid: null
}));
}
if (isRtl !== state.lastSectionsDependencies.isRtl || format !== state.lastSectionsDependencies.format || adapter.locale !== state.lastSectionsDependencies.locale) {
const sections = getSectionsFromValue(value);
(0, _useField.validateSections)(sections, valueType);
setState(prevState => (0, _extends2.default)({}, prevState, {
lastSectionsDependencies: {
format,
isRtl,
locale: adapter.locale
},
sections,
tempValueStrAndroid: null,
characterQuery: null
}));
}
if (state.characterQuery != null && !error && activeSectionIndex == null) {
setCharacterQuery(null);
}
if (state.characterQuery != null && state.sections[state.characterQuery.sectionIndex]?.type !== state.characterQuery.sectionType) {
setCharacterQuery(null);
}
React.useEffect(() => {
if (sectionToUpdateOnNextInvalidDateRef.current != null) {
sectionToUpdateOnNextInvalidDateRef.current = null;
}
});
const cleanCharacterQueryTimeout = (0, _useTimeout.default)();
React.useEffect(() => {
if (state.characterQuery != null) {
cleanCharacterQueryTimeout.start(QUERY_LIFE_DURATION_MS, () => setCharacterQuery(null));
}
return () => {};
}, [state.characterQuery, setCharacterQuery, cleanCharacterQueryTimeout]);
// If `tempValueStrAndroid` is still defined for some section when running `useEffect`,
// Then `onChange` has only been called once, which means the user pressed `Backspace` to reset the section.
// This causes a small flickering on Android,
// But we can't use `useEnhancedEffect` which is always called before the second `onChange` call and then would cause false positives.
React.useEffect(() => {
if (state.tempValueStrAndroid != null && activeSectionIndex != null) {
clearActiveSection();
}
}, [state.sections]); // eslint-disable-line react-hooks/exhaustive-deps
return {
// States and derived states
activeSectionIndex,
areAllSectionsEmpty,
error,
localizedDigits,
parsedSelectedSections,
sectionOrder,
sectionsValueBoundaries,
state,
timezone,
value,
// Methods to update the states
clearValue,
clearActiveSection,
setCharacterQuery,
setSelectedSections,
setTempAndroidValueStr,
updateSectionValue,
updateValueFromValueStr,
// Utilities methods
getSectionsFromValue
};
};
exports.useFieldState = useFieldState;

View file

@ -0,0 +1,26 @@
import { UseFieldParameters, UseFieldProps, UseFieldReturnValue } from "./useField.types.js";
import { InferFieldSection } from "../../../models/index.js";
import { PickerValidValue } from "../../models/index.js";
export declare const addPositionPropertiesToSections: <TValue extends PickerValidValue>(sections: InferFieldSection<TValue>[], localizedDigits: string[], isRtl: boolean) => FieldSectionWithPositions<TValue>[];
export declare const useFieldV6TextField: <TValue extends PickerValidValue, TError, TValidationProps extends {}, TProps extends UseFieldProps<false>>(parameters: UseFieldParameters<TValue, false, TError, TValidationProps, TProps>) => UseFieldReturnValue<false, TProps>;
type FieldSectionWithPositions<TValue extends PickerValidValue> = InferFieldSection<TValue> & {
/**
* Start index of the section in the format
*/
start: number;
/**
* End index of the section in the format
*/
end: number;
/**
* Start index of the section value in the input.
* Takes into account invisible unicode characters such as \u2069 but does not include them
*/
startInInput: number;
/**
* End index of the section value in the input.
* Takes into account invisible unicode characters such as \u2069 but does not include them
*/
endInInput: number;
};
export {};

View file

@ -0,0 +1,420 @@
"use strict";
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldV6TextField = exports.addPositionPropertiesToSections = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _RtlProvider = require("@mui/system/RtlProvider");
var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _useTimeout = _interopRequireDefault(require("@mui/utils/useTimeout"));
var _useForkRef = _interopRequireDefault(require("@mui/utils/useForkRef"));
var _hooks = require("../../../hooks");
var _utils = require("../../utils/utils");
var _useField = require("./useField.utils");
var _useFieldCharacterEditing = require("./useFieldCharacterEditing");
var _useFieldRootHandleKeyDown = require("./useFieldRootHandleKeyDown");
var _useFieldState = require("./useFieldState");
var _useFieldInternalPropsWithDefaults = require("./useFieldInternalPropsWithDefaults");
const cleanString = dirtyString => dirtyString.replace(/[\u2066\u2067\u2068\u2069]/g, '');
const addPositionPropertiesToSections = (sections, localizedDigits, isRtl) => {
let position = 0;
let positionInInput = isRtl ? 1 : 0;
const newSections = [];
for (let i = 0; i < sections.length; i += 1) {
const section = sections[i];
const renderedValue = (0, _useField.getSectionVisibleValue)(section, isRtl ? 'input-rtl' : 'input-ltr', localizedDigits);
const sectionStr = `${section.startSeparator}${renderedValue}${section.endSeparator}`;
const sectionLength = cleanString(sectionStr).length;
const sectionLengthInInput = sectionStr.length;
// The ...InInput values consider the unicode characters but do include them in their indexes
const cleanedValue = cleanString(renderedValue);
const startInInput = positionInInput + (cleanedValue === '' ? 0 : renderedValue.indexOf(cleanedValue[0])) + section.startSeparator.length;
const endInInput = startInInput + cleanedValue.length;
newSections.push((0, _extends2.default)({}, section, {
start: position,
end: position + sectionLength,
startInInput,
endInInput
}));
position += sectionLength;
// Move position to the end of string associated to the current section
positionInInput += sectionLengthInInput;
}
return newSections;
};
exports.addPositionPropertiesToSections = addPositionPropertiesToSections;
const useFieldV6TextField = parameters => {
const isRtl = (0, _RtlProvider.useRtl)();
const focusTimeout = (0, _useTimeout.default)();
const selectionSyncTimeout = (0, _useTimeout.default)();
const {
props,
manager,
skipContextFieldRefAssignment,
manager: {
valueType,
internal_valueManager: valueManager,
internal_fieldValueManager: fieldValueManager,
internal_useOpenPickerButtonAriaLabel: useOpenPickerButtonAriaLabel
}
} = parameters;
const {
internalProps,
forwardedProps
} = (0, _hooks.useSplitFieldProps)(props, valueType);
const internalPropsWithDefaults = (0, _useFieldInternalPropsWithDefaults.useFieldInternalPropsWithDefaults)({
manager,
internalProps,
skipContextFieldRefAssignment
});
const {
onFocus,
onClick,
onPaste,
onBlur,
onKeyDown,
onClear,
clearable,
inputRef: inputRefProp,
placeholder: inPlaceholder
} = forwardedProps;
const {
readOnly = false,
disabled = false,
autoFocus = false,
focused,
unstableFieldRef
} = internalPropsWithDefaults;
const inputRef = React.useRef(null);
const handleRef = (0, _useForkRef.default)(inputRefProp, inputRef);
const stateResponse = (0, _useFieldState.useFieldState)({
manager,
internalPropsWithDefaults,
forwardedProps
});
const {
// States and derived states
activeSectionIndex,
areAllSectionsEmpty,
error,
localizedDigits,
parsedSelectedSections,
sectionOrder,
state,
value,
// Methods to update the states
clearValue,
clearActiveSection,
setCharacterQuery,
setSelectedSections,
setTempAndroidValueStr,
updateSectionValue,
updateValueFromValueStr,
// Utilities methods
getSectionsFromValue
} = stateResponse;
const applyCharacterEditing = (0, _useFieldCharacterEditing.useFieldCharacterEditing)({
stateResponse
});
const openPickerAriaLabel = useOpenPickerButtonAriaLabel(value);
const sections = React.useMemo(() => addPositionPropertiesToSections(state.sections, localizedDigits, isRtl), [state.sections, localizedDigits, isRtl]);
function syncSelectionFromDOM() {
const browserStartIndex = inputRef.current.selectionStart ?? 0;
let nextSectionIndex;
if (browserStartIndex <= sections[0].startInInput) {
// Special case if browser index is in invisible characters at the beginning
nextSectionIndex = 1;
} else if (browserStartIndex >= sections[sections.length - 1].endInInput) {
// If the click is after the last character of the input, then we want to select the 1st section.
nextSectionIndex = 1;
} else {
nextSectionIndex = sections.findIndex(section => section.startInInput - section.startSeparator.length > browserStartIndex);
}
const sectionIndex = nextSectionIndex === -1 ? sections.length - 1 : nextSectionIndex - 1;
setSelectedSections(sectionIndex);
}
function focusField(newSelectedSection = 0) {
if ((0, _utils.getActiveElement)(inputRef.current) === inputRef.current) {
return;
}
inputRef.current?.focus();
setSelectedSections(newSelectedSection);
}
const handleInputFocus = (0, _useEventCallback.default)(event => {
onFocus?.(event);
// The ref is guaranteed to be resolved at this point.
const input = inputRef.current;
focusTimeout.start(0, () => {
// The ref changed, the component got remounted, the focus event is no longer relevant.
if (!input || input !== inputRef.current) {
return;
}
if (activeSectionIndex != null) {
return;
}
if (
// avoid selecting all sections when focusing empty field without value
input.value.length && Number(input.selectionEnd) - Number(input.selectionStart) === input.value.length) {
setSelectedSections('all');
} else {
syncSelectionFromDOM();
}
});
});
const handleInputClick = (0, _useEventCallback.default)((event, ...args) => {
// The click event on the clear button would propagate to the input, trigger this handler and result in a wrong section selection.
// We avoid this by checking if the call of `handleInputClick` is actually intended, or a side effect.
if (event.isDefaultPrevented()) {
return;
}
onClick?.(event, ...args);
syncSelectionFromDOM();
});
const handleInputPaste = (0, _useEventCallback.default)(event => {
onPaste?.(event);
// prevent default to avoid the input `onChange` handler being called
event.preventDefault();
if (readOnly || disabled) {
return;
}
const pastedValue = event.clipboardData.getData('text');
if (typeof parsedSelectedSections === 'number') {
const activeSection = state.sections[parsedSelectedSections];
const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue);
const digitsOnly = /^[0-9]+$/.test(pastedValue);
const digitsAndLetterOnly = /^(([a-zA-Z]+)|)([0-9]+)(([a-zA-Z]+)|)$/.test(pastedValue);
const isValidPastedValue = activeSection.contentType === 'letter' && lettersOnly || activeSection.contentType === 'digit' && digitsOnly || activeSection.contentType === 'digit-with-letter' && digitsAndLetterOnly;
if (isValidPastedValue) {
setCharacterQuery(null);
updateSectionValue({
section: activeSection,
newSectionValue: pastedValue,
shouldGoToNextSection: true
});
return;
}
if (lettersOnly || digitsOnly) {
// The pasted value corresponds to a single section, but not the expected type,
// skip the modification
return;
}
}
setCharacterQuery(null);
updateValueFromValueStr(pastedValue);
});
const handleContainerBlur = (0, _useEventCallback.default)(event => {
onBlur?.(event);
setSelectedSections(null);
});
const handleInputChange = (0, _useEventCallback.default)(event => {
if (readOnly) {
return;
}
const targetValue = event.target.value;
if (targetValue === '') {
clearValue();
return;
}
const eventData = event.nativeEvent.data;
// Calling `.fill(04/11/2022)` in playwright will trigger a change event with the requested content to insert in `event.nativeEvent.data`
// usual changes have only the currently typed character in the `event.nativeEvent.data`
const shouldUseEventData = eventData && eventData.length > 1;
const valueStr = shouldUseEventData ? eventData : targetValue;
const cleanValueStr = cleanString(valueStr);
if (parsedSelectedSections === 'all') {
setSelectedSections(activeSectionIndex);
}
// If no section is selected or eventData should be used, we just try to parse the new value
// This line is mostly triggered by imperative code / application tests.
if (activeSectionIndex == null || shouldUseEventData) {
updateValueFromValueStr(shouldUseEventData ? eventData : cleanValueStr);
return;
}
let keyPressed;
if (parsedSelectedSections === 'all' && cleanValueStr.length === 1) {
keyPressed = cleanValueStr;
} else {
const prevValueStr = cleanString(fieldValueManager.getV6InputValueFromSections(sections, localizedDigits, isRtl));
let startOfDiffIndex = -1;
let endOfDiffIndex = -1;
for (let i = 0; i < prevValueStr.length; i += 1) {
if (startOfDiffIndex === -1 && prevValueStr[i] !== cleanValueStr[i]) {
startOfDiffIndex = i;
}
if (endOfDiffIndex === -1 && prevValueStr[prevValueStr.length - i - 1] !== cleanValueStr[cleanValueStr.length - i - 1]) {
endOfDiffIndex = i;
}
}
const activeSection = sections[activeSectionIndex];
const hasDiffOutsideOfActiveSection = startOfDiffIndex < activeSection.start || prevValueStr.length - endOfDiffIndex - 1 > activeSection.end;
if (hasDiffOutsideOfActiveSection) {
// TODO: Support if the new date is valid
return;
}
// The active section being selected, the browser has replaced its value with the key pressed by the user.
const activeSectionEndRelativeToNewValue = cleanValueStr.length - prevValueStr.length + activeSection.end - cleanString(activeSection.endSeparator || '').length;
keyPressed = cleanValueStr.slice(activeSection.start + cleanString(activeSection.startSeparator || '').length, activeSectionEndRelativeToNewValue);
}
if (keyPressed.length === 0) {
if ((0, _useField.isAndroid)()) {
setTempAndroidValueStr(valueStr);
}
clearActiveSection();
return;
}
applyCharacterEditing({
keyPressed,
sectionIndex: activeSectionIndex
});
});
const handleClear = (0, _useEventCallback.default)((event, ...args) => {
event.preventDefault();
onClear?.(event, ...args);
clearValue();
if (!isFieldFocused(inputRef)) {
// setSelectedSections is called internally
focusField(0);
} else {
setSelectedSections(sectionOrder.startIndex);
}
});
const handleContainerKeyDown = (0, _useFieldRootHandleKeyDown.useFieldRootHandleKeyDown)({
manager,
internalPropsWithDefaults,
stateResponse
});
const wrappedHandleContainerKeyDown = (0, _useEventCallback.default)(event => {
onKeyDown?.(event);
handleContainerKeyDown(event);
});
const placeholder = React.useMemo(() => {
if (inPlaceholder !== undefined) {
return inPlaceholder;
}
return fieldValueManager.getV6InputValueFromSections(getSectionsFromValue(valueManager.emptyValue), localizedDigits, isRtl);
}, [inPlaceholder, fieldValueManager, getSectionsFromValue, valueManager.emptyValue, localizedDigits, isRtl]);
const valueStr = React.useMemo(() => state.tempValueStrAndroid ?? fieldValueManager.getV6InputValueFromSections(state.sections, localizedDigits, isRtl), [state.sections, fieldValueManager, state.tempValueStrAndroid, localizedDigits, isRtl]);
React.useEffect(() => {
// Select all the sections when focused on mount (`autoFocus = true` on the input)
if (inputRef.current && inputRef.current === (0, _utils.getActiveElement)(inputRef.current)) {
setSelectedSections('all');
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
(0, _useEnhancedEffect.default)(() => {
function syncSelectionToDOM() {
if (!inputRef.current) {
return;
}
if (parsedSelectedSections == null) {
if (inputRef.current.scrollLeft) {
// Ensure that input content is not marked as selected.
// setting selection range to 0 causes issues in Safari.
// https://bugs.webkit.org/show_bug.cgi?id=224425
inputRef.current.scrollLeft = 0;
}
return;
}
// On multi input range pickers we want to update selection range only for the active input
// This helps to avoid the focus jumping on Safari https://github.com/mui/mui-x/issues/9003
// because WebKit implements the `setSelectionRange` based on the spec: https://bugs.webkit.org/show_bug.cgi?id=224425
if (inputRef.current !== (0, _utils.getActiveElement)(inputRef.current)) {
return;
}
// Fix scroll jumping on iOS browser: https://github.com/mui/mui-x/issues/8321
const currentScrollTop = inputRef.current.scrollTop;
if (parsedSelectedSections === 'all') {
inputRef.current.select();
} else {
const selectedSection = sections[parsedSelectedSections];
const selectionStart = selectedSection.type === 'empty' ? selectedSection.startInInput - selectedSection.startSeparator.length : selectedSection.startInInput;
const selectionEnd = selectedSection.type === 'empty' ? selectedSection.endInInput + selectedSection.endSeparator.length : selectedSection.endInInput;
if (selectionStart !== inputRef.current.selectionStart || selectionEnd !== inputRef.current.selectionEnd) {
if (inputRef.current === (0, _utils.getActiveElement)(inputRef.current)) {
inputRef.current.setSelectionRange(selectionStart, selectionEnd);
}
}
selectionSyncTimeout.start(0, () => {
// handle case when the selection is not updated correctly
// could happen on Android
if (inputRef.current && inputRef.current === (0, _utils.getActiveElement)(inputRef.current) &&
// The section might loose all selection, where `selectionStart === selectionEnd`
// https://github.com/mui/mui-x/pull/13652
inputRef.current.selectionStart === inputRef.current.selectionEnd && (inputRef.current.selectionStart !== selectionStart || inputRef.current.selectionEnd !== selectionEnd)) {
syncSelectionToDOM();
}
});
}
// Even reading this variable seems to do the trick, but also setting it just to make use of it
inputRef.current.scrollTop = currentScrollTop;
}
syncSelectionToDOM();
});
const inputMode = React.useMemo(() => {
if (activeSectionIndex == null) {
return 'text';
}
if (state.sections[activeSectionIndex].contentType === 'letter') {
return 'text';
}
return 'numeric';
}, [activeSectionIndex, state.sections]);
const inputHasFocus = inputRef.current && inputRef.current === (0, _utils.getActiveElement)(inputRef.current);
const shouldShowPlaceholder = !inputHasFocus && areAllSectionsEmpty;
React.useImperativeHandle(unstableFieldRef, () => ({
getSections: () => state.sections,
getActiveSectionIndex: () => {
const browserStartIndex = inputRef.current.selectionStart ?? 0;
const browserEndIndex = inputRef.current.selectionEnd ?? 0;
if (browserStartIndex === 0 && browserEndIndex === 0) {
return null;
}
const nextSectionIndex = browserStartIndex <= sections[0].startInInput ? 1 // Special case if browser index is in invisible characters at the beginning.
: sections.findIndex(section => section.startInInput - section.startSeparator.length > browserStartIndex);
return nextSectionIndex === -1 ? sections.length - 1 : nextSectionIndex - 1;
},
setSelectedSections: newSelectedSections => setSelectedSections(newSelectedSections),
focusField,
isFieldFocused: () => isFieldFocused(inputRef)
}));
return (0, _extends2.default)({}, forwardedProps, {
error,
clearable: Boolean(clearable && !areAllSectionsEmpty && !readOnly && !disabled),
onBlur: handleContainerBlur,
onClick: handleInputClick,
onFocus: handleInputFocus,
onPaste: handleInputPaste,
onKeyDown: wrappedHandleContainerKeyDown,
onClear: handleClear,
inputRef: handleRef,
// Additional
enableAccessibleFieldDOMStructure: false,
placeholder,
inputMode,
autoComplete: 'off',
value: shouldShowPlaceholder ? '' : valueStr,
onChange: handleInputChange,
focused,
disabled,
readOnly,
autoFocus,
openPickerAriaLabel
});
};
exports.useFieldV6TextField = useFieldV6TextField;
function isFieldFocused(inputRef) {
return inputRef.current === (0, _utils.getActiveElement)(inputRef.current);
}

View file

@ -0,0 +1,3 @@
import { UseFieldParameters, UseFieldProps, UseFieldReturnValue } from "./useField.types.js";
import { PickerValidValue } from "../../models/index.js";
export declare const useFieldV7TextField: <TValue extends PickerValidValue, TError, TValidationProps extends {}, TProps extends UseFieldProps<true>>(parameters: UseFieldParameters<TValue, true, TError, TValidationProps, TProps>) => UseFieldReturnValue<true, TProps>;

View file

@ -0,0 +1,263 @@
"use strict";
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useFieldV7TextField = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _useForkRef = _interopRequireDefault(require("@mui/utils/useForkRef"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect"));
var _useField = require("./useField.utils");
var _utils = require("../../utils/utils");
var _hooks = require("../../../hooks");
var _useFieldCharacterEditing = require("./useFieldCharacterEditing");
var _useFieldState = require("./useFieldState");
var _useFieldInternalPropsWithDefaults = require("./useFieldInternalPropsWithDefaults");
var _syncSelectionToDOM = require("./syncSelectionToDOM");
var _useFieldRootProps = require("./useFieldRootProps");
var _useFieldHiddenInputProps = require("./useFieldHiddenInputProps");
var _useFieldSectionContainerProps = require("./useFieldSectionContainerProps");
var _useFieldSectionContentProps = require("./useFieldSectionContentProps");
const useFieldV7TextField = parameters => {
const {
props,
manager,
skipContextFieldRefAssignment,
manager: {
valueType,
internal_useOpenPickerButtonAriaLabel: useOpenPickerButtonAriaLabel
}
} = parameters;
const {
internalProps,
forwardedProps
} = (0, _hooks.useSplitFieldProps)(props, valueType);
const internalPropsWithDefaults = (0, _useFieldInternalPropsWithDefaults.useFieldInternalPropsWithDefaults)({
manager,
internalProps,
skipContextFieldRefAssignment
});
const {
sectionListRef: sectionListRefProp,
onBlur,
onClick,
onFocus,
onInput,
onPaste,
onKeyDown,
onClear,
clearable
} = forwardedProps;
const {
disabled = false,
readOnly = false,
autoFocus = false,
focused: focusedProp,
unstableFieldRef
} = internalPropsWithDefaults;
const sectionListRef = React.useRef(null);
const handleSectionListRef = (0, _useForkRef.default)(sectionListRefProp, sectionListRef);
const domGetters = React.useMemo(() => ({
isReady: () => sectionListRef.current != null,
getRoot: () => sectionListRef.current.getRoot(),
getSectionContainer: sectionIndex => sectionListRef.current.getSectionContainer(sectionIndex),
getSectionContent: sectionIndex => sectionListRef.current.getSectionContent(sectionIndex),
getSectionIndexFromDOMElement: element => sectionListRef.current.getSectionIndexFromDOMElement(element)
}), [sectionListRef]);
const stateResponse = (0, _useFieldState.useFieldState)({
manager,
internalPropsWithDefaults,
forwardedProps
});
const {
// States and derived states
areAllSectionsEmpty,
error,
parsedSelectedSections,
sectionOrder,
state,
value,
// Methods to update the states
clearValue,
setSelectedSections
} = stateResponse;
const applyCharacterEditing = (0, _useFieldCharacterEditing.useFieldCharacterEditing)({
stateResponse
});
const openPickerAriaLabel = useOpenPickerButtonAriaLabel(value);
const [focused, setFocused] = React.useState(false);
function focusField(newSelectedSections = 0) {
if (disabled || !sectionListRef.current ||
// if the field is already focused, we don't need to focus it again
getActiveSectionIndex(sectionListRef) != null) {
return;
}
const newParsedSelectedSections = (0, _useField.parseSelectedSections)(newSelectedSections, state.sections);
setFocused(true);
sectionListRef.current.getSectionContent(newParsedSelectedSections).focus();
}
const rootProps = (0, _useFieldRootProps.useFieldRootProps)({
manager,
internalPropsWithDefaults,
stateResponse,
applyCharacterEditing,
focused,
setFocused,
domGetters
});
const hiddenInputProps = (0, _useFieldHiddenInputProps.useFieldHiddenInputProps)({
manager,
stateResponse
});
const createSectionContainerProps = (0, _useFieldSectionContainerProps.useFieldSectionContainerProps)({
stateResponse,
internalPropsWithDefaults
});
const createSectionContentProps = (0, _useFieldSectionContentProps.useFieldSectionContentProps)({
manager,
stateResponse,
applyCharacterEditing,
internalPropsWithDefaults,
domGetters,
focused
});
const handleRootKeyDown = (0, _useEventCallback.default)(event => {
onKeyDown?.(event);
rootProps.onKeyDown(event);
});
const handleRootBlur = (0, _useEventCallback.default)(event => {
onBlur?.(event);
rootProps.onBlur(event);
});
const handleRootFocus = (0, _useEventCallback.default)(event => {
onFocus?.(event);
rootProps.onFocus(event);
});
const handleRootClick = (0, _useEventCallback.default)(event => {
// The click event on the clear or open button would propagate to the input, trigger this handler and result in an inadvertent section selection.
// We avoid this by checking if the call of `handleInputClick` is actually intended, or a propagated call, which should be skipped.
if (event.isDefaultPrevented()) {
return;
}
onClick?.(event);
rootProps.onClick(event);
});
const handleRootPaste = (0, _useEventCallback.default)(event => {
onPaste?.(event);
rootProps.onPaste(event);
});
const handleRootInput = (0, _useEventCallback.default)(event => {
onInput?.(event);
rootProps.onInput(event);
});
const handleClear = (0, _useEventCallback.default)((event, ...args) => {
event.preventDefault();
onClear?.(event, ...args);
clearValue();
if (!isFieldFocused(sectionListRef)) {
// setSelectedSections is called internally
focusField(0);
} else {
setSelectedSections(sectionOrder.startIndex);
}
});
const elements = React.useMemo(() => {
return state.sections.map((section, sectionIndex) => {
const content = createSectionContentProps(section, sectionIndex);
return {
container: createSectionContainerProps(sectionIndex),
content: createSectionContentProps(section, sectionIndex),
before: {
children: section.startSeparator
},
after: {
children: section.endSeparator,
'data-range-position': section.isEndFormatSeparator ? content['data-range-position'] : undefined
}
};
});
}, [state.sections, createSectionContainerProps, createSectionContentProps]);
React.useEffect(() => {
if (sectionListRef.current == null) {
throw new Error(['MUI X: The `sectionListRef` prop has not been initialized by `PickersSectionList`', 'You probably tried to pass a component to the `textField` slot that contains an `<input />` element instead of a `PickersSectionList`.', '', 'If you want to keep using an `<input />` HTML element for the editing, please add the `enableAccessibleFieldDOMStructure={false}` prop to your Picker or Field component:', '', '<DatePicker enableAccessibleFieldDOMStructure={false} slots={{ textField: MyCustomTextField }} />', '', 'Learn more about the field accessible DOM structure on the MUI documentation: https://mui.com/x/react-date-pickers/fields/#fields-to-edit-a-single-element'].join('\n'));
}
if (autoFocus && !disabled && sectionListRef.current) {
sectionListRef.current.getSectionContent(sectionOrder.startIndex).focus();
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
(0, _useEnhancedEffect.default)(() => {
if (!focused || !sectionListRef.current) {
return;
}
if (parsedSelectedSections === 'all') {
sectionListRef.current.getRoot().focus();
} else if (typeof parsedSelectedSections === 'number') {
const domElement = sectionListRef.current.getSectionContent(parsedSelectedSections);
if (domElement) {
domElement.focus();
}
}
}, [parsedSelectedSections, focused]);
(0, _useEnhancedEffect.default)(() => {
(0, _syncSelectionToDOM.syncSelectionToDOM)({
focused,
domGetters,
stateResponse
});
});
React.useImperativeHandle(unstableFieldRef, () => ({
getSections: () => state.sections,
getActiveSectionIndex: () => getActiveSectionIndex(sectionListRef),
setSelectedSections: newSelectedSections => {
if (disabled || !sectionListRef.current) {
return;
}
const newParsedSelectedSections = (0, _useField.parseSelectedSections)(newSelectedSections, state.sections);
const newActiveSectionIndex = newParsedSelectedSections === 'all' ? 0 : newParsedSelectedSections;
setFocused(newActiveSectionIndex !== null);
setSelectedSections(newSelectedSections);
},
focusField,
isFieldFocused: () => isFieldFocused(sectionListRef)
}));
return (0, _extends2.default)({}, forwardedProps, rootProps, {
onBlur: handleRootBlur,
onClick: handleRootClick,
onFocus: handleRootFocus,
onInput: handleRootInput,
onPaste: handleRootPaste,
onKeyDown: handleRootKeyDown,
onClear: handleClear
}, hiddenInputProps, {
error,
clearable: Boolean(clearable && !areAllSectionsEmpty && !readOnly && !disabled),
focused: focusedProp ?? focused,
sectionListRef: handleSectionListRef,
// Additional
enableAccessibleFieldDOMStructure: true,
elements,
areAllSectionsEmpty,
disabled,
readOnly,
autoFocus,
openPickerAriaLabel
});
};
exports.useFieldV7TextField = useFieldV7TextField;
function getActiveSectionIndex(sectionListRef) {
const activeElement = (0, _utils.getActiveElement)(sectionListRef.current?.getRoot());
if (!activeElement || !sectionListRef.current || !sectionListRef.current.getRoot().contains(activeElement)) {
return null;
}
return sectionListRef.current.getSectionIndexFromDOMElement(activeElement);
}
function isFieldFocused(sectionListRef) {
const activeElement = (0, _utils.getActiveElement)(sectionListRef.current?.getRoot());
return !!sectionListRef.current && sectionListRef.current.getRoot().contains(activeElement);
}