@jsenv/navi 0.27.42 → 0.27.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +436 -28
- package/dist/jsenv_navi.js.map +31 -22
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isValidElement, createContext, h, toChildArray, render, Fragment, clone
|
|
|
3
3
|
import { useErrorBoundary, useLayoutEffect, useEffect, useContext, useMemo, useRef, useState, useCallback, useId } from "preact/hooks";
|
|
4
4
|
import { jsxs, jsx, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
5
5
|
import { signal, effect, computed, batch, useSignal } from "@preact/signals";
|
|
6
|
-
import { createIterableWeakSet, createEventGroupLogger, normalizeStyle, mergeOneStyle, createPubSub, findEvent, dispatchInternalCustomEvent, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, dispatchCustomEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, measureLongestVisualLineWidth,
|
|
6
|
+
import { createIterableWeakSet, createEventGroupLogger, normalizeStyle, mergeOneStyle, createPubSub, findEvent, dispatchInternalCustomEvent, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, dispatchCustomEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, measureLongestVisualLineWidth, getKeyboardEventDefaultAction, chainEvent, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, activeElementSignal, initFocusGroup, elementIsFocusable, snapToPixel, trapScrollInside, trapFocusInside, scrollIntoViewScoped, measureWidestChildRow, performTabNavigation, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
7
7
|
export { contrastColor, startDragToReorder } from "@jsenv/dom";
|
|
8
8
|
import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
|
|
9
9
|
import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
|
|
@@ -18260,6 +18260,50 @@ naviI18n.addAll({
|
|
|
18260
18260
|
},
|
|
18261
18261
|
});
|
|
18262
18262
|
|
|
18263
|
+
// charGuard / maxLengthGuard callout messages
|
|
18264
|
+
naviI18n.addAll({
|
|
18265
|
+
// Preset-specific char messages — more informative than the generic fallback
|
|
18266
|
+
"constraint.guard.number": {
|
|
18267
|
+
fr: "Ce champ ne peut contenir que des chiffres.",
|
|
18268
|
+
en: "This field can only contain digits.",
|
|
18269
|
+
},
|
|
18270
|
+
"constraint.guard.alpha": {
|
|
18271
|
+
fr: "Ce champ ne peut contenir que des lettres.",
|
|
18272
|
+
en: "This field can only contain letters.",
|
|
18273
|
+
},
|
|
18274
|
+
"constraint.guard.alphanumeric": {
|
|
18275
|
+
fr: "Ce champ ne peut contenir que des lettres et des chiffres.",
|
|
18276
|
+
en: "This field can only contain letters and digits.",
|
|
18277
|
+
},
|
|
18278
|
+
"constraint.guard.uppercase": {
|
|
18279
|
+
fr: "Ce champ ne peut contenir que des lettres majuscules.",
|
|
18280
|
+
en: "This field can only contain uppercase letters.",
|
|
18281
|
+
},
|
|
18282
|
+
"constraint.guard.hex": {
|
|
18283
|
+
fr: "Ce champ ne peut contenir que des chiffres hexadécimaux (0-9, A-F).",
|
|
18284
|
+
en: "This field can only contain hexadecimal digits (0-9, A-F).",
|
|
18285
|
+
},
|
|
18286
|
+
"constraint.guard.slug": {
|
|
18287
|
+
fr: "Ce champ ne peut contenir que des lettres minuscules, des chiffres et des tirets.",
|
|
18288
|
+
en: "This field can only contain lowercase letters, digits, and hyphens.",
|
|
18289
|
+
},
|
|
18290
|
+
// Generic fallback for custom char classes and other presets (tel, card, postal, iban…)
|
|
18291
|
+
"constraint.guard.chars": {
|
|
18292
|
+
fr: "Ce champ ne peut contenir que les caractères autorisés.",
|
|
18293
|
+
en: "This field can only contain allowed characters.",
|
|
18294
|
+
},
|
|
18295
|
+
// maxLength: keydown blocked (one character would exceed the limit)
|
|
18296
|
+
"constraint.guard.max_length.typing": {
|
|
18297
|
+
fr: "Longueur maximale de [max] caractère[s] atteinte.",
|
|
18298
|
+
en: "Maximum length of [max] character[s] reached.",
|
|
18299
|
+
},
|
|
18300
|
+
// maxLength: paste/set truncated to maxLength (autofix always applied)
|
|
18301
|
+
"constraint.guard.max_length.value": {
|
|
18302
|
+
fr: "Ce champ ne peut pas contenir plus de [max] caractère[s], une partie a été tronquée.",
|
|
18303
|
+
en: "This field cannot contain more than [max] character[s]; the value was truncated.",
|
|
18304
|
+
},
|
|
18305
|
+
});
|
|
18306
|
+
|
|
18263
18307
|
// Date/time placeholder tokens — shown when no value is selected
|
|
18264
18308
|
// Override any key to adapt to your language conventions
|
|
18265
18309
|
naviI18n.addAll({
|
|
@@ -19543,7 +19587,7 @@ const MAX_LENGTH_CONSTRAINT = {
|
|
|
19543
19587
|
} else if (!isTextarea) {
|
|
19544
19588
|
return null;
|
|
19545
19589
|
}
|
|
19546
|
-
const maxLength = field.controlHostProps.maxLength;
|
|
19590
|
+
const maxLength = field.controlHostProps.maxLength ?? field.props?.maxLengthGuard;
|
|
19547
19591
|
if (maxLength === undefined) {
|
|
19548
19592
|
return null;
|
|
19549
19593
|
}
|
|
@@ -21888,6 +21932,9 @@ const CONTROL_PROP_SET = new Set([
|
|
|
21888
21932
|
"resetOnCancel",
|
|
21889
21933
|
"resetOnAbort",
|
|
21890
21934
|
"resetOnError",
|
|
21935
|
+
|
|
21936
|
+
"charGuard",
|
|
21937
|
+
"maxLengthGuard",
|
|
21891
21938
|
]);
|
|
21892
21939
|
|
|
21893
21940
|
const MessagePropsRefContext = createContext();
|
|
@@ -21905,6 +21952,221 @@ const ActionRequesterContext = createContext();
|
|
|
21905
21952
|
|
|
21906
21953
|
const FormContext = createContext();
|
|
21907
21954
|
|
|
21955
|
+
/**
|
|
21956
|
+
* Named presets for the `charGuard` prop.
|
|
21957
|
+
* Each value is a regex character class (including the [ ] delimiters).
|
|
21958
|
+
*/
|
|
21959
|
+
const CHAR_CLASS_PRESETS = {
|
|
21960
|
+
numeric: "[0-9]", // digits only
|
|
21961
|
+
alpha: "[A-Za-z]", // letters only
|
|
21962
|
+
alphanumeric: "[0-9A-Za-z]", // letters and digits
|
|
21963
|
+
uppercase: "[A-Z]", // uppercase letters only
|
|
21964
|
+
tel: "[-0-9+() ]", // phone: digits, +, -, parens, space
|
|
21965
|
+
card: "[0-9 ]", // credit card: digits and spaces
|
|
21966
|
+
hex: "[0-9A-Fa-f]", // hexadecimal digits
|
|
21967
|
+
pin: "[0-9]", // numeric PIN
|
|
21968
|
+
postal: "[0-9A-Za-z -]", // postal code (FR, UK, US)
|
|
21969
|
+
iban: "[0-9A-Z]", // IBAN: uppercase and digits
|
|
21970
|
+
slug: "[a-z0-9-]", // URL slug
|
|
21971
|
+
};
|
|
21972
|
+
|
|
21973
|
+
// Presets that imply a specific mobile keyboard layout
|
|
21974
|
+
const INPUT_MODE_FROM_PRESET = {
|
|
21975
|
+
numeric: "numeric",
|
|
21976
|
+
pin: "numeric",
|
|
21977
|
+
card: "numeric",
|
|
21978
|
+
tel: "tel",
|
|
21979
|
+
};
|
|
21980
|
+
|
|
21981
|
+
// Specific i18n keys per preset — more informative than the generic fallback
|
|
21982
|
+
const MESSAGE_KEY_FROM_PRESET = {
|
|
21983
|
+
numeric: "constraint.guard.number",
|
|
21984
|
+
pin: "constraint.guard.number",
|
|
21985
|
+
alpha: "constraint.guard.alpha",
|
|
21986
|
+
alphanumeric: "constraint.guard.alphanumeric",
|
|
21987
|
+
uppercase: "constraint.guard.uppercase",
|
|
21988
|
+
hex: "constraint.guard.hex",
|
|
21989
|
+
slug: "constraint.guard.slug",
|
|
21990
|
+
// tel, card, postal, iban, custom → generic fallback
|
|
21991
|
+
};
|
|
21992
|
+
|
|
21993
|
+
/**
|
|
21994
|
+
* Returns the regex character class for a preset name, or the raw value as-is
|
|
21995
|
+
* if it's not a known preset (allows custom classes like "[A-Z0-9_]").
|
|
21996
|
+
*/
|
|
21997
|
+
const resolveCharClass = (value) => {
|
|
21998
|
+
if (!value) return null;
|
|
21999
|
+
return CHAR_CLASS_PRESETS[value] ?? value;
|
|
22000
|
+
};
|
|
22001
|
+
|
|
22002
|
+
/**
|
|
22003
|
+
* Returns the inputMode to auto-apply for the given preset, or null.
|
|
22004
|
+
*/
|
|
22005
|
+
const resolveInputModeFromAllowedChars = (value) => {
|
|
22006
|
+
return INPUT_MODE_FROM_PRESET[value] ?? null;
|
|
22007
|
+
};
|
|
22008
|
+
|
|
22009
|
+
/**
|
|
22010
|
+
* Returns the i18n key for the char guard rejection message.
|
|
22011
|
+
* Falls back to the generic "constraint.guard.chars" for custom classes and
|
|
22012
|
+
* presets without a specific message.
|
|
22013
|
+
*/
|
|
22014
|
+
const getCharGuardMessageKey = (value) => {
|
|
22015
|
+
return MESSAGE_KEY_FROM_PRESET[value] ?? "constraint.guard.chars";
|
|
22016
|
+
};
|
|
22017
|
+
|
|
22018
|
+
/**
|
|
22019
|
+
* Input guard — enforces character and length constraints during typing, paste,
|
|
22020
|
+
* and external value sets.
|
|
22021
|
+
*
|
|
22022
|
+
* The guard owns a single callout token (shared across all rejection reasons) so
|
|
22023
|
+
* successive rejections update the same callout rather than stacking.
|
|
22024
|
+
*
|
|
22025
|
+
* Exposed as `controller.rules.guard` (created inside `createControlRules`).
|
|
22026
|
+
*
|
|
22027
|
+
* Props read from `controller.props` on each call (always up to date):
|
|
22028
|
+
* - `charGuard` — preset name (e.g. "numeric", "tel") or raw char class "[A-Z]"
|
|
22029
|
+
* - `maxLengthGuard` — maximum character count (enables both guard and constraint)
|
|
22030
|
+
*/
|
|
22031
|
+
|
|
22032
|
+
|
|
22033
|
+
const isTypingIntent = (e) =>
|
|
22034
|
+
getKeyboardEventDefaultAction(e) === "type";
|
|
22035
|
+
|
|
22036
|
+
const s = (n) => (n > 1 ? "s" : "");
|
|
22037
|
+
|
|
22038
|
+
// Keydown: block only single printable characters that don't match the class.
|
|
22039
|
+
// Multi-character key names (Delete, ArrowLeft…) are always allowed.
|
|
22040
|
+
const getInvalidCharMessage = (char, { charClass, messageKey }) => {
|
|
22041
|
+
if (char.length !== 1) return null;
|
|
22042
|
+
if (new RegExp(charClass).test(char)) return null;
|
|
22043
|
+
return naviI18n(messageKey);
|
|
22044
|
+
};
|
|
22045
|
+
|
|
22046
|
+
// Keydown: block when inserting one char would exceed maxLength.
|
|
22047
|
+
const getMaxLengthInsertionMessage = (el, { maxLength }) => {
|
|
22048
|
+
if (maxLength === undefined) return null;
|
|
22049
|
+
const selStart = el.selectionStart ?? el.value.length;
|
|
22050
|
+
const selEnd = el.selectionEnd ?? el.value.length;
|
|
22051
|
+
const newLen = el.value.length - (selEnd - selStart) + 1;
|
|
22052
|
+
if (newLen <= maxLength) return null;
|
|
22053
|
+
return naviI18n("constraint.guard.max_length.typing", {
|
|
22054
|
+
max: maxLength,
|
|
22055
|
+
s: s(maxLength),
|
|
22056
|
+
});
|
|
22057
|
+
};
|
|
22058
|
+
|
|
22059
|
+
// Paste / set: block when value contains disallowed chars.
|
|
22060
|
+
const getInvalidCharsMessage = (uiState, { charClass, messageKey }) => {
|
|
22061
|
+
const str = uiState === undefined ? "" : String(uiState);
|
|
22062
|
+
if (new RegExp(`^(?:${charClass})*$`).test(str)) return null;
|
|
22063
|
+
return naviI18n(messageKey);
|
|
22064
|
+
};
|
|
22065
|
+
|
|
22066
|
+
// Paste / set: truncate when value exceeds maxLength.
|
|
22067
|
+
const getLengthOverflowResult = (uiState, { maxLength }) => {
|
|
22068
|
+
if (maxLength === undefined) return null;
|
|
22069
|
+
const str = uiState === undefined ? "" : String(uiState);
|
|
22070
|
+
if (str.length <= maxLength) return null;
|
|
22071
|
+
return {
|
|
22072
|
+
fixedValue: str.slice(0, maxLength),
|
|
22073
|
+
message: naviI18n("constraint.guard.max_length.value", {
|
|
22074
|
+
max: maxLength,
|
|
22075
|
+
s: s(maxLength),
|
|
22076
|
+
}),
|
|
22077
|
+
};
|
|
22078
|
+
};
|
|
22079
|
+
|
|
22080
|
+
const createControlGuard = (controller) => {
|
|
22081
|
+
const token = createOpenToken();
|
|
22082
|
+
|
|
22083
|
+
const show = (message, e) => {
|
|
22084
|
+
controller.rules.callout.addOpenToken(token, {
|
|
22085
|
+
message,
|
|
22086
|
+
status: "info",
|
|
22087
|
+
event: e,
|
|
22088
|
+
skipFocus: true,
|
|
22089
|
+
});
|
|
22090
|
+
};
|
|
22091
|
+
|
|
22092
|
+
const clear = (e) => {
|
|
22093
|
+
controller.rules.callout.removeOpenToken(token, e);
|
|
22094
|
+
};
|
|
22095
|
+
|
|
22096
|
+
/**
|
|
22097
|
+
* Called on every keydown. Returns true when the key should be blocked
|
|
22098
|
+
* (caller must call e.preventDefault()).
|
|
22099
|
+
* Non-typing keys (Delete, Arrow…) are always allowed.
|
|
22100
|
+
*/
|
|
22101
|
+
const checkKeydown = (e, el) => {
|
|
22102
|
+
if (!isTypingIntent(e)) {
|
|
22103
|
+
return false;
|
|
22104
|
+
}
|
|
22105
|
+
const { charGuard, maxLengthGuard } = controller.props;
|
|
22106
|
+
|
|
22107
|
+
if (charGuard) {
|
|
22108
|
+
const charClass = resolveCharClass(charGuard);
|
|
22109
|
+
const messageKey = getCharGuardMessageKey(charGuard);
|
|
22110
|
+
const charMsg = getInvalidCharMessage(e.key, { charClass, messageKey });
|
|
22111
|
+
if (charMsg) {
|
|
22112
|
+
show(charMsg, e);
|
|
22113
|
+
return true;
|
|
22114
|
+
}
|
|
22115
|
+
}
|
|
22116
|
+
if (maxLengthGuard !== undefined) {
|
|
22117
|
+
const lenMsg = getMaxLengthInsertionMessage(el, {
|
|
22118
|
+
maxLength: maxLengthGuard,
|
|
22119
|
+
});
|
|
22120
|
+
if (lenMsg) {
|
|
22121
|
+
show(lenMsg, e);
|
|
22122
|
+
return true;
|
|
22123
|
+
}
|
|
22124
|
+
}
|
|
22125
|
+
clear(e);
|
|
22126
|
+
return false;
|
|
22127
|
+
};
|
|
22128
|
+
|
|
22129
|
+
/**
|
|
22130
|
+
* Called when a full value is about to be applied (paste or external set).
|
|
22131
|
+
*
|
|
22132
|
+
* Returns:
|
|
22133
|
+
* null — value is valid, proceed normally
|
|
22134
|
+
* { blocked } — value rejected, caller must not apply it (callout shown)
|
|
22135
|
+
* { fixedValue } — value was truncated to maxLengthGuard (callout shown as info)
|
|
22136
|
+
*/
|
|
22137
|
+
const checkUIState = (uiState, e) => {
|
|
22138
|
+
const { charGuard, maxLengthGuard } = controller.props;
|
|
22139
|
+
|
|
22140
|
+
if (charGuard) {
|
|
22141
|
+
const charClass = resolveCharClass(charGuard);
|
|
22142
|
+
const messageKey = getCharGuardMessageKey(charGuard);
|
|
22143
|
+
const charsMsg = getInvalidCharsMessage(uiState, {
|
|
22144
|
+
charClass,
|
|
22145
|
+
messageKey,
|
|
22146
|
+
});
|
|
22147
|
+
if (charsMsg) {
|
|
22148
|
+
show(charsMsg, e);
|
|
22149
|
+
return { blocked: true };
|
|
22150
|
+
}
|
|
22151
|
+
}
|
|
22152
|
+
|
|
22153
|
+
if (maxLengthGuard !== undefined) {
|
|
22154
|
+
const lengthResult = getLengthOverflowResult(uiState, {
|
|
22155
|
+
maxLength: maxLengthGuard,
|
|
22156
|
+
});
|
|
22157
|
+
if (lengthResult) {
|
|
22158
|
+
show(lengthResult.message, e);
|
|
22159
|
+
return { fixedValue: lengthResult.fixedValue };
|
|
22160
|
+
}
|
|
22161
|
+
}
|
|
22162
|
+
|
|
22163
|
+
clear(e);
|
|
22164
|
+
return null;
|
|
22165
|
+
};
|
|
22166
|
+
|
|
22167
|
+
return { checkKeydown, checkUIState, show, clear };
|
|
22168
|
+
};
|
|
22169
|
+
|
|
21908
22170
|
/**
|
|
21909
22171
|
* Orchestrates the three rule managers for a UI state controller.
|
|
21910
22172
|
*
|
|
@@ -21921,13 +22183,13 @@ const FormContext = createContext();
|
|
|
21921
22183
|
|
|
21922
22184
|
|
|
21923
22185
|
/**
|
|
21924
|
-
* Creates all
|
|
22186
|
+
* Creates all rule managers for a controller and wires them together.
|
|
21925
22187
|
*
|
|
21926
22188
|
* @param {object} controller - The UI state controller.
|
|
21927
22189
|
* @param {object} [options]
|
|
21928
22190
|
* @param {Function} [options.debugUIState] - Debug logger for state/validity events.
|
|
21929
22191
|
* @param {Function} [options.debugFocus] - Debug logger for focus events.
|
|
21930
|
-
* @returns {{ callout, interaction, validation, uninstall }}
|
|
22192
|
+
* @returns {{ callout, interaction, validation, guard, uninstall }}
|
|
21931
22193
|
*/
|
|
21932
22194
|
const createControlRules = (
|
|
21933
22195
|
controller,
|
|
@@ -21951,10 +22213,13 @@ const createControlRules = (
|
|
|
21951
22213
|
debugUIState,
|
|
21952
22214
|
});
|
|
21953
22215
|
|
|
22216
|
+
const guard = createControlGuard(controller);
|
|
22217
|
+
|
|
21954
22218
|
return {
|
|
21955
22219
|
callout,
|
|
21956
22220
|
interaction,
|
|
21957
22221
|
validation,
|
|
22222
|
+
guard,
|
|
21958
22223
|
uninstall: teardown,
|
|
21959
22224
|
};
|
|
21960
22225
|
};
|
|
@@ -22183,6 +22448,18 @@ const useUIStateController = (
|
|
|
22183
22448
|
}
|
|
22184
22449
|
},
|
|
22185
22450
|
setUIState: (newUIState, e) => {
|
|
22451
|
+
const guardResult = uiStateController.rules.guard.checkUIState(
|
|
22452
|
+
newUIState,
|
|
22453
|
+
e,
|
|
22454
|
+
);
|
|
22455
|
+
if (guardResult) {
|
|
22456
|
+
if (Object.hasOwn(guardResult, "fixedValue")) {
|
|
22457
|
+
newUIState = guardResult.fixedValue;
|
|
22458
|
+
// fall through — continue with truncated value (callout already shown by guard)
|
|
22459
|
+
} else {
|
|
22460
|
+
return false;
|
|
22461
|
+
}
|
|
22462
|
+
}
|
|
22186
22463
|
const controllerSig = getElementSignature(e.currentTarget || ref.current);
|
|
22187
22464
|
// if (persists) {
|
|
22188
22465
|
// setNavState(prop);
|
|
@@ -23455,7 +23732,7 @@ const useControlProps = (props, {
|
|
|
23455
23732
|
}
|
|
23456
23733
|
};
|
|
23457
23734
|
}
|
|
23458
|
-
return
|
|
23735
|
+
return keyDownDefault(e);
|
|
23459
23736
|
},
|
|
23460
23737
|
click: e => {
|
|
23461
23738
|
return {
|
|
@@ -23501,6 +23778,20 @@ const useControlProps = (props, {
|
|
|
23501
23778
|
}
|
|
23502
23779
|
if (controlType === "picker") {
|
|
23503
23780
|
return {
|
|
23781
|
+
keyDown: e => {
|
|
23782
|
+
if (e.key === " ") {
|
|
23783
|
+
return {
|
|
23784
|
+
name: "space to click",
|
|
23785
|
+
allowed: () => {
|
|
23786
|
+
ref.current.click();
|
|
23787
|
+
},
|
|
23788
|
+
always: () => {
|
|
23789
|
+
e.preventDefault(); // prevent page scroll
|
|
23790
|
+
}
|
|
23791
|
+
};
|
|
23792
|
+
}
|
|
23793
|
+
return keyDownDefault(e);
|
|
23794
|
+
},
|
|
23504
23795
|
input: e => {
|
|
23505
23796
|
return {
|
|
23506
23797
|
name: "input",
|
|
@@ -23676,7 +23967,14 @@ const useControlProps = (props, {
|
|
|
23676
23967
|
const isInputTextual = controlType === "input";
|
|
23677
23968
|
if (isInputTextual) {
|
|
23678
23969
|
return {
|
|
23679
|
-
keyDown:
|
|
23970
|
+
keyDown: e => {
|
|
23971
|
+
const blocked = uiStateController.rules.guard.checkKeydown(e, ref.current);
|
|
23972
|
+
if (blocked) {
|
|
23973
|
+
e.preventDefault();
|
|
23974
|
+
return null;
|
|
23975
|
+
}
|
|
23976
|
+
return keyDownDefaultOnInput(e);
|
|
23977
|
+
},
|
|
23680
23978
|
input: e => {
|
|
23681
23979
|
return {
|
|
23682
23980
|
name: "input",
|
|
@@ -23800,7 +24098,27 @@ const useControlProps = (props, {
|
|
|
23800
24098
|
dispatchRequestInteraction(ref.current, {
|
|
23801
24099
|
event: e,
|
|
23802
24100
|
name: "paste",
|
|
23803
|
-
prevented: () => e.preventDefault()
|
|
24101
|
+
prevented: () => e.preventDefault(),
|
|
24102
|
+
allowed: () => {
|
|
24103
|
+
const pastedText = e.clipboardData?.getData("text") ?? "";
|
|
24104
|
+
const el = ref.current;
|
|
24105
|
+
const selStart = el.selectionStart ?? el.value.length;
|
|
24106
|
+
const selEnd = el.selectionEnd ?? el.value.length;
|
|
24107
|
+
const newValue = el.value.slice(0, selStart) + pastedText + el.value.slice(selEnd);
|
|
24108
|
+
const guardResult = uiStateController.rules.guard.checkUIState(newValue, e);
|
|
24109
|
+
if (guardResult?.blocked) {
|
|
24110
|
+
e.preventDefault();
|
|
24111
|
+
return;
|
|
24112
|
+
}
|
|
24113
|
+
if (guardResult?.fixedValue !== undefined) {
|
|
24114
|
+
// Pass newValue (not fixedValue) so setUIState's guard shows the
|
|
24115
|
+
// truncation callout and applies the truncated value itself.
|
|
24116
|
+
e.preventDefault();
|
|
24117
|
+
uiStateController.setUIState(newValue, e);
|
|
24118
|
+
return;
|
|
24119
|
+
}
|
|
24120
|
+
// valid — let the browser paste; the input event will sync UI state
|
|
24121
|
+
}
|
|
23804
24122
|
});
|
|
23805
24123
|
};
|
|
23806
24124
|
Object.assign(controlHostProps, {
|
|
@@ -23829,8 +24147,9 @@ const createControlInfo = (props, {
|
|
|
23829
24147
|
let disabledSupported = false;
|
|
23830
24148
|
let hasStateProp;
|
|
23831
24149
|
let value;
|
|
24150
|
+
const typeProp = props.type || "text";
|
|
23832
24151
|
if (controlType === "input") {
|
|
23833
|
-
if (
|
|
24152
|
+
if (typeProp === "checkbox" || typeProp === "radio") {
|
|
23834
24153
|
statePropName = "checked";
|
|
23835
24154
|
defaultStatePropName = "defaultChecked";
|
|
23836
24155
|
hasStateProp = Object.hasOwn(props, "checked");
|
|
@@ -23865,7 +24184,7 @@ const createControlInfo = (props, {
|
|
|
23865
24184
|
} else {
|
|
23866
24185
|
stateInitial = undefined;
|
|
23867
24186
|
}
|
|
23868
|
-
readOnlySupported =
|
|
24187
|
+
readOnlySupported = INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
|
|
23869
24188
|
}
|
|
23870
24189
|
disabledSupported = true;
|
|
23871
24190
|
} else if (controlType === "button") {
|
|
@@ -23893,7 +24212,7 @@ const createControlInfo = (props, {
|
|
|
23893
24212
|
stateInitial = undefined;
|
|
23894
24213
|
}
|
|
23895
24214
|
disabledSupported = true;
|
|
23896
|
-
readOnlySupported =
|
|
24215
|
+
readOnlySupported = INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
|
|
23897
24216
|
}
|
|
23898
24217
|
return {
|
|
23899
24218
|
controlType,
|
|
@@ -23907,6 +24226,8 @@ const createControlInfo = (props, {
|
|
|
23907
24226
|
disabledSupported
|
|
23908
24227
|
};
|
|
23909
24228
|
};
|
|
24229
|
+
// color, radio, image, file etc do not support readonly
|
|
24230
|
+
const INPUT_TYPE_SUPPORTING_READONLY_SET = new Set(["text", "date", "datetime-local", "email", "month", "number", "password", "search", "tel", "time", "url", "week"]);
|
|
23910
24231
|
const useReadOnlyUncontrolled = (props, controlInfo) => {
|
|
23911
24232
|
if (!controlInfo.hasStateProp) {
|
|
23912
24233
|
return false;
|
|
@@ -32776,6 +33097,29 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
32776
33097
|
* For non-textual inputs, specialized components will be used:
|
|
32777
33098
|
* - <InputCheckbox /> for type="checkbox"
|
|
32778
33099
|
* - <InputRadio /> for type="radio"
|
|
33100
|
+
*
|
|
33101
|
+
* Guard props (immediate feedback instead of wait-for-submit):
|
|
33102
|
+
*
|
|
33103
|
+
* - charGuard — restricts which characters can be typed, pasted, or set externally.
|
|
33104
|
+
* Accepts a preset name or a raw regex character class:
|
|
33105
|
+
* "numeric" → digits only, sets inputMode="numeric" + pattern auto
|
|
33106
|
+
* "alpha" → letters only
|
|
33107
|
+
* "alphanumeric" → letters and digits
|
|
33108
|
+
* "uppercase" → uppercase letters only
|
|
33109
|
+
* "tel" → phone chars (digits, +, -, parens, space), sets inputMode="tel"
|
|
33110
|
+
* "card" → credit card (digits and spaces), sets inputMode="numeric"
|
|
33111
|
+
* "hex" → hexadecimal digits
|
|
33112
|
+
* "pin" → numeric PIN, sets inputMode="numeric"
|
|
33113
|
+
* "postal" → postal code (digits, letters, space, hyphen)
|
|
33114
|
+
* "iban" → IBAN (uppercase and digits)
|
|
33115
|
+
* "slug" → URL slug (lowercase, digits, hyphens)
|
|
33116
|
+
* "[A-Z0-9]" → any custom regex character class
|
|
33117
|
+
* inputMode and pattern are auto-derived from the preset when not explicitly set.
|
|
33118
|
+
*
|
|
33119
|
+
* - maxLengthGuard — combines maxLength + overflow guard in one prop.
|
|
33120
|
+
* Blocks keydown when the limit is reached; truncates on paste/set with an info callout.
|
|
33121
|
+
* The maxLength constraint remains active for form validation at submit.
|
|
33122
|
+
* Use plain maxLength (without maxLengthGuard) for submit-only validation.
|
|
32779
33123
|
*/
|
|
32780
33124
|
const css$x = /* css */`
|
|
32781
33125
|
@layer navi {
|
|
@@ -33151,6 +33495,18 @@ const InputTextualFirstResolver = props => {
|
|
|
33151
33495
|
const defaultRef = useRef(null);
|
|
33152
33496
|
props.ref = props.ref || defaultRef;
|
|
33153
33497
|
resolveInputProps(props);
|
|
33498
|
+
|
|
33499
|
+
// charGuard → auto inputMode + auto pattern for mobile keyboard hints
|
|
33500
|
+
if (props.charGuard) {
|
|
33501
|
+
if (props.inputMode === undefined) {
|
|
33502
|
+
const autoMode = resolveInputModeFromAllowedChars(props.charGuard);
|
|
33503
|
+
if (autoMode) props.inputMode = autoMode;
|
|
33504
|
+
}
|
|
33505
|
+
if (props.pattern === undefined) {
|
|
33506
|
+
const charClass = resolveCharClass(props.charGuard);
|
|
33507
|
+
props.pattern = `${charClass}*`;
|
|
33508
|
+
}
|
|
33509
|
+
}
|
|
33154
33510
|
return jsx(Next, {
|
|
33155
33511
|
...props
|
|
33156
33512
|
});
|
|
@@ -33182,6 +33538,11 @@ const RealInput = props => {
|
|
|
33182
33538
|
e.target.select();
|
|
33183
33539
|
}
|
|
33184
33540
|
}
|
|
33541
|
+
// Never set native maxLength — our guard handles it. maxLength stays in
|
|
33542
|
+
// inputControlHostProps so form validation constraints still read it.
|
|
33543
|
+
,
|
|
33544
|
+
|
|
33545
|
+
maxLength: undefined
|
|
33185
33546
|
});
|
|
33186
33547
|
};
|
|
33187
33548
|
const InputStyleCSSVars = {
|
|
@@ -35377,6 +35738,7 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
|
|
|
35377
35738
|
}
|
|
35378
35739
|
`;
|
|
35379
35740
|
const PickerCustomResolver = props => {
|
|
35741
|
+
import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
35380
35742
|
if (props.children === undefined) {
|
|
35381
35743
|
return jsx(PickerNative, {
|
|
35382
35744
|
...props
|
|
@@ -35408,6 +35770,9 @@ const PickerNative = props => {
|
|
|
35408
35770
|
dispatchRequestInteraction(pickerInput, {
|
|
35409
35771
|
event: e,
|
|
35410
35772
|
name: "navi_request_open to show native picker",
|
|
35773
|
+
prevented: () => {
|
|
35774
|
+
e.preventDefault();
|
|
35775
|
+
},
|
|
35411
35776
|
allowed: () => {
|
|
35412
35777
|
try {
|
|
35413
35778
|
pickerInput.showPicker();
|
|
@@ -35427,10 +35792,13 @@ const PickerNative = props => {
|
|
|
35427
35792
|
allowed: () => {
|
|
35428
35793
|
const pickerEl = props.ref.current;
|
|
35429
35794
|
const pickerInput = getPickerInput(pickerEl);
|
|
35430
|
-
// requestCloseValidityCallout(pickerEl, e);
|
|
35431
35795
|
if (pickerInput.type === "color") ; else {
|
|
35432
35796
|
// other picker might not open the picker when clicking the input surface (only the calendar picker for instance would open)
|
|
35433
|
-
|
|
35797
|
+
try {
|
|
35798
|
+
pickerInput.showPicker();
|
|
35799
|
+
} catch {
|
|
35800
|
+
pickerInput.click();
|
|
35801
|
+
}
|
|
35434
35802
|
}
|
|
35435
35803
|
}
|
|
35436
35804
|
};
|
|
@@ -35704,6 +36072,15 @@ const PickerCustom = props => {
|
|
|
35704
36072
|
}
|
|
35705
36073
|
};
|
|
35706
36074
|
},
|
|
36075
|
+
"enter": e => {
|
|
36076
|
+
return {
|
|
36077
|
+
name: "enter_to_open",
|
|
36078
|
+
allowed: () => {
|
|
36079
|
+
requestOpen(e);
|
|
36080
|
+
e.preventDefault(); // prevent form submission
|
|
36081
|
+
}
|
|
36082
|
+
};
|
|
36083
|
+
},
|
|
35707
36084
|
"escape": e => {
|
|
35708
36085
|
if (!expandedRef.current) {
|
|
35709
36086
|
return null;
|
|
@@ -35742,6 +36119,9 @@ const PickerCustom = props => {
|
|
|
35742
36119
|
// in that case we want to open it (otherwise we have already opened on mousedown interaction)
|
|
35743
36120
|
return {
|
|
35744
36121
|
name: e.detail === 0 ? "click (keyboard or progammatic) to open picker" : "click to open picker",
|
|
36122
|
+
prevented: () => {
|
|
36123
|
+
e.preventDefault();
|
|
36124
|
+
},
|
|
35745
36125
|
allowed: () => {
|
|
35746
36126
|
requestOpen(e);
|
|
35747
36127
|
e.preventDefault();
|
|
@@ -35810,7 +36190,6 @@ const getPickerInputUIState = pickerEl => {
|
|
|
35810
36190
|
};
|
|
35811
36191
|
const PickerContentInsidePopover = props => {
|
|
35812
36192
|
const Next = useNextResolver();
|
|
35813
|
-
import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
35814
36193
|
const {
|
|
35815
36194
|
popupProps,
|
|
35816
36195
|
children,
|
|
@@ -35882,7 +36261,6 @@ const PickerContentInsidePopover = props => {
|
|
|
35882
36261
|
};
|
|
35883
36262
|
const PickerContentInsideDialog = props => {
|
|
35884
36263
|
const Next = useNextResolver();
|
|
35885
|
-
import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
35886
36264
|
const {
|
|
35887
36265
|
popupProps,
|
|
35888
36266
|
children,
|
|
@@ -37037,6 +37415,9 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
|
|
|
37037
37415
|
}
|
|
37038
37416
|
|
|
37039
37417
|
.navi_list_item[navi-selectable] {
|
|
37418
|
+
--list-item-padding-x-default: inherit;
|
|
37419
|
+
--list-item-padding-y-default: inherit;
|
|
37420
|
+
|
|
37040
37421
|
outline-width: var(--list-item-outline-width);
|
|
37041
37422
|
outline-color: var(--list-item-outline-color);
|
|
37042
37423
|
outline-offset: var(--list-item-outline-offset);
|
|
@@ -39899,13 +40280,12 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
|
|
|
39899
40280
|
}
|
|
39900
40281
|
}
|
|
39901
40282
|
.navi_picker_right_slot {
|
|
39902
|
-
--slot-spacing: calc(0.
|
|
40283
|
+
--slot-spacing: calc(var(--x-picker-padding-right) * 0.5);
|
|
39903
40284
|
|
|
39904
40285
|
display: inline-flex;
|
|
39905
40286
|
height: 1em;
|
|
39906
40287
|
height: 1lh;
|
|
39907
40288
|
margin-right: var(--slot-spacing);
|
|
39908
|
-
margin-left: var(--slot-spacing);
|
|
39909
40289
|
flex-shrink: 0;
|
|
39910
40290
|
align-items: center;
|
|
39911
40291
|
align-self: flex-start;
|
|
@@ -39924,17 +40304,13 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
|
|
|
39924
40304
|
}
|
|
39925
40305
|
}
|
|
39926
40306
|
.navi_picker_input {
|
|
39927
|
-
position: absolute;
|
|
39928
|
-
inset: 0;
|
|
39929
40307
|
box-sizing: border-box;
|
|
39930
|
-
width: 100%;
|
|
39931
|
-
height: 100%;
|
|
39932
40308
|
margin: 0;
|
|
39933
40309
|
padding: 0;
|
|
39934
40310
|
background: none;
|
|
39935
40311
|
border: none;
|
|
39936
|
-
|
|
39937
|
-
|
|
40312
|
+
border-radius: inherit;
|
|
40313
|
+
outline: none;
|
|
39938
40314
|
cursor: inherit;
|
|
39939
40315
|
pointer-events: auto;
|
|
39940
40316
|
|
|
@@ -39943,6 +40319,22 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
|
|
|
39943
40319
|
}
|
|
39944
40320
|
}
|
|
39945
40321
|
|
|
40322
|
+
&[navi-ui-custom] {
|
|
40323
|
+
.navi_picker_input {
|
|
40324
|
+
position: absolute;
|
|
40325
|
+
top: calc(-1 * var(--picker-border-width));
|
|
40326
|
+
right: calc(-1 * var(--picker-border-width));
|
|
40327
|
+
bottom: calc(-1 * var(--picker-border-width));
|
|
40328
|
+
left: calc(-1 * var(--picker-border-width));
|
|
40329
|
+
/* Reset width/height for input color */
|
|
40330
|
+
width: auto;
|
|
40331
|
+
height: auto;
|
|
40332
|
+
|
|
40333
|
+
opacity: 0;
|
|
40334
|
+
appearance: none;
|
|
40335
|
+
}
|
|
40336
|
+
}
|
|
40337
|
+
|
|
39946
40338
|
.navi_picker_content {
|
|
39947
40339
|
display: contents;
|
|
39948
40340
|
text-align: initial; /* Don't inherit picker text align */
|
|
@@ -40073,6 +40465,7 @@ const PickerButton = props => {
|
|
|
40073
40465
|
"data-variant": variant,
|
|
40074
40466
|
"navi-picker": "",
|
|
40075
40467
|
"navi-single-line": isSingleLine ? "" : undefined,
|
|
40468
|
+
"navi-ui-custom": ui === "default" ? undefined : "",
|
|
40076
40469
|
...pickerRemainingProps,
|
|
40077
40470
|
basePseudoState: basePseudoState,
|
|
40078
40471
|
styleCSSVars: PickerStyleCSSVars,
|
|
@@ -40104,6 +40497,7 @@ const PickerButton = props => {
|
|
|
40104
40497
|
children: undefined // we will render children into the div
|
|
40105
40498
|
,
|
|
40106
40499
|
|
|
40500
|
+
ui: ui,
|
|
40107
40501
|
onFocus: e => {
|
|
40108
40502
|
inputProps.onFocus?.(e);
|
|
40109
40503
|
e.target.select();
|
|
@@ -40173,7 +40567,7 @@ const PickerButton = props => {
|
|
|
40173
40567
|
});
|
|
40174
40568
|
e.preventDefault();
|
|
40175
40569
|
}
|
|
40176
|
-
}), variant === "icon" || variant === "headless" ? null : jsx(Text, {
|
|
40570
|
+
}), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
|
|
40177
40571
|
className: "navi_picker_value",
|
|
40178
40572
|
"navi-placeholder": value === undefined || value === "" ? "" : undefined,
|
|
40179
40573
|
maxLines: maxLines,
|
|
@@ -40185,7 +40579,7 @@ const PickerButton = props => {
|
|
|
40185
40579
|
},
|
|
40186
40580
|
children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
|
|
40187
40581
|
})
|
|
40188
|
-
}), variant === "headless" ? null : jsx("span", {
|
|
40582
|
+
}), variant === "headless" || ui === "default" ? null : jsx("span", {
|
|
40189
40583
|
className: "navi_picker_right_slot",
|
|
40190
40584
|
children: jsx(Icon, {
|
|
40191
40585
|
size: "m",
|
|
@@ -40204,12 +40598,21 @@ const isWithinPickerContent = (el, pickerEl) => {
|
|
|
40204
40598
|
return pickerEl.querySelector(".navi_picker_content")?.contains(el);
|
|
40205
40599
|
};
|
|
40206
40600
|
const PickerInput = props => {
|
|
40601
|
+
const {
|
|
40602
|
+
ui
|
|
40603
|
+
} = props;
|
|
40604
|
+
|
|
40605
|
+
// After type resolution: force readOnly when the input type would open the
|
|
40606
|
+
// mobile keyboard. We also suppress the visual ":read-only" state so the
|
|
40607
|
+
// picker still looks interactive (it is — just not keyboard-typeable).
|
|
40608
|
+
if (MOBILE_KEYBOARD_TYPES.has(props.type) && !props.readOnly) {
|
|
40609
|
+
props.readOnly = true;
|
|
40610
|
+
props["data-readonly-forced"] = "";
|
|
40611
|
+
}
|
|
40207
40612
|
return jsx(Box, {
|
|
40208
40613
|
as: "input",
|
|
40209
|
-
"data-readonly-forced": props.readOnly ? undefined : "",
|
|
40210
40614
|
...props,
|
|
40211
|
-
|
|
40212
|
-
readOnly: true,
|
|
40615
|
+
ui: undefined,
|
|
40213
40616
|
className: "navi_picker_input",
|
|
40214
40617
|
pseudoClasses: PickerInputPseudoClasses,
|
|
40215
40618
|
onKeyDown: e => {
|
|
@@ -40217,12 +40620,17 @@ const PickerInput = props => {
|
|
|
40217
40620
|
if (e.key === "Enter") {
|
|
40218
40621
|
// prevent form submission now that input can have focus
|
|
40219
40622
|
e.preventDefault();
|
|
40220
|
-
} else if (e.key === "Tab") {
|
|
40623
|
+
} else if (e.key === "Tab" && ui !== "default") {
|
|
40624
|
+
// Ensure tab does not tab through the browser picker elements (like in input date)
|
|
40221
40625
|
performTabNavigation(e);
|
|
40222
40626
|
}
|
|
40223
40627
|
}
|
|
40224
40628
|
});
|
|
40225
40629
|
};
|
|
40630
|
+
// Input types that open the software keyboard on mobile.
|
|
40631
|
+
// When the picker's underlying input has one of these types, we force readOnly
|
|
40632
|
+
// so tapping the picker doesn't open the keyboard (the picker manages its own UI).
|
|
40633
|
+
const MOBILE_KEYBOARD_TYPES = new Set(["text", "email", "url", "search", "password", "tel", "number"]);
|
|
40226
40634
|
const PICKER_BUTTON_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-loading", ":-navi-expanded", ":-navi-has-value"];
|
|
40227
40635
|
const PickerInputPseudoClasses = [":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value", ":-navi-expanded"];
|
|
40228
40636
|
const PickerStyleCSSVars = {
|