@jsenv/navi 0.27.41 → 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 +458 -34
- package/dist/jsenv_navi.js.map +39 -26
- 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 = {
|
|
@@ -34878,7 +35239,7 @@ const Dialog = props => {
|
|
|
34878
35239
|
addCleanup(trapScrollInside(dialogEl));
|
|
34879
35240
|
}
|
|
34880
35241
|
if (centerInVisualViewportProp && window.visualViewport) {
|
|
34881
|
-
const
|
|
35242
|
+
const updatePosition = () => {
|
|
34882
35243
|
const vv = window.visualViewport;
|
|
34883
35244
|
const dialogHeight = dialogEl.offsetHeight;
|
|
34884
35245
|
const availableHeight = vv.height;
|
|
@@ -34886,12 +35247,28 @@ const Dialog = props => {
|
|
|
34886
35247
|
const marginTop = availableHeight > dialogHeight ? topOffset + (availableHeight - dialogHeight) / 2 : topOffset;
|
|
34887
35248
|
dialogEl.style.setProperty("--dialog-top-inset", `${snapToPixel(marginTop)}px`);
|
|
34888
35249
|
};
|
|
34889
|
-
|
|
34890
|
-
|
|
34891
|
-
|
|
35250
|
+
const onScroll = () => {
|
|
35251
|
+
updatePosition();
|
|
35252
|
+
};
|
|
35253
|
+
let resizeTimeout;
|
|
35254
|
+
const cancelDelayedUpdatePosition = () => {
|
|
35255
|
+
clearTimeout(resizeTimeout);
|
|
35256
|
+
};
|
|
35257
|
+
const onResize = () => {
|
|
35258
|
+
// On mobile, tapping from one input to another triggers a resize because
|
|
35259
|
+
// the virtual keyboard briefly starts to close before the new input receives
|
|
35260
|
+
// focus and the keyboard reopens. Debouncing prevents repositioning the
|
|
35261
|
+
// dialog during that transient state, which would cause a visible flicker.
|
|
35262
|
+
cancelDelayedUpdatePosition();
|
|
35263
|
+
resizeTimeout = setTimeout(updatePosition, 100);
|
|
35264
|
+
};
|
|
35265
|
+
updatePosition();
|
|
35266
|
+
window.visualViewport.addEventListener("resize", onResize);
|
|
35267
|
+
window.visualViewport.addEventListener("scroll", onScroll);
|
|
34892
35268
|
addCleanup(() => {
|
|
34893
|
-
|
|
34894
|
-
window.visualViewport.removeEventListener("
|
|
35269
|
+
cancelDelayedUpdatePosition();
|
|
35270
|
+
window.visualViewport.removeEventListener("resize", onResize);
|
|
35271
|
+
window.visualViewport.removeEventListener("scroll", onScroll);
|
|
34895
35272
|
dialogEl.style.removeProperty("--dialog-top-inset");
|
|
34896
35273
|
});
|
|
34897
35274
|
}
|
|
@@ -35361,6 +35738,7 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
|
|
|
35361
35738
|
}
|
|
35362
35739
|
`;
|
|
35363
35740
|
const PickerCustomResolver = props => {
|
|
35741
|
+
import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
35364
35742
|
if (props.children === undefined) {
|
|
35365
35743
|
return jsx(PickerNative, {
|
|
35366
35744
|
...props
|
|
@@ -35392,6 +35770,9 @@ const PickerNative = props => {
|
|
|
35392
35770
|
dispatchRequestInteraction(pickerInput, {
|
|
35393
35771
|
event: e,
|
|
35394
35772
|
name: "navi_request_open to show native picker",
|
|
35773
|
+
prevented: () => {
|
|
35774
|
+
e.preventDefault();
|
|
35775
|
+
},
|
|
35395
35776
|
allowed: () => {
|
|
35396
35777
|
try {
|
|
35397
35778
|
pickerInput.showPicker();
|
|
@@ -35411,10 +35792,13 @@ const PickerNative = props => {
|
|
|
35411
35792
|
allowed: () => {
|
|
35412
35793
|
const pickerEl = props.ref.current;
|
|
35413
35794
|
const pickerInput = getPickerInput(pickerEl);
|
|
35414
|
-
// requestCloseValidityCallout(pickerEl, e);
|
|
35415
35795
|
if (pickerInput.type === "color") ; else {
|
|
35416
35796
|
// other picker might not open the picker when clicking the input surface (only the calendar picker for instance would open)
|
|
35417
|
-
|
|
35797
|
+
try {
|
|
35798
|
+
pickerInput.showPicker();
|
|
35799
|
+
} catch {
|
|
35800
|
+
pickerInput.click();
|
|
35801
|
+
}
|
|
35418
35802
|
}
|
|
35419
35803
|
}
|
|
35420
35804
|
};
|
|
@@ -35688,6 +36072,15 @@ const PickerCustom = props => {
|
|
|
35688
36072
|
}
|
|
35689
36073
|
};
|
|
35690
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
|
+
},
|
|
35691
36084
|
"escape": e => {
|
|
35692
36085
|
if (!expandedRef.current) {
|
|
35693
36086
|
return null;
|
|
@@ -35726,6 +36119,9 @@ const PickerCustom = props => {
|
|
|
35726
36119
|
// in that case we want to open it (otherwise we have already opened on mousedown interaction)
|
|
35727
36120
|
return {
|
|
35728
36121
|
name: e.detail === 0 ? "click (keyboard or progammatic) to open picker" : "click to open picker",
|
|
36122
|
+
prevented: () => {
|
|
36123
|
+
e.preventDefault();
|
|
36124
|
+
},
|
|
35729
36125
|
allowed: () => {
|
|
35730
36126
|
requestOpen(e);
|
|
35731
36127
|
e.preventDefault();
|
|
@@ -35794,7 +36190,6 @@ const getPickerInputUIState = pickerEl => {
|
|
|
35794
36190
|
};
|
|
35795
36191
|
const PickerContentInsidePopover = props => {
|
|
35796
36192
|
const Next = useNextResolver();
|
|
35797
|
-
import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
35798
36193
|
const {
|
|
35799
36194
|
popupProps,
|
|
35800
36195
|
children,
|
|
@@ -35866,7 +36261,6 @@ const PickerContentInsidePopover = props => {
|
|
|
35866
36261
|
};
|
|
35867
36262
|
const PickerContentInsideDialog = props => {
|
|
35868
36263
|
const Next = useNextResolver();
|
|
35869
|
-
import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
35870
36264
|
const {
|
|
35871
36265
|
popupProps,
|
|
35872
36266
|
children,
|
|
@@ -37021,6 +37415,9 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
|
|
|
37021
37415
|
}
|
|
37022
37416
|
|
|
37023
37417
|
.navi_list_item[navi-selectable] {
|
|
37418
|
+
--list-item-padding-x-default: inherit;
|
|
37419
|
+
--list-item-padding-y-default: inherit;
|
|
37420
|
+
|
|
37024
37421
|
outline-width: var(--list-item-outline-width);
|
|
37025
37422
|
outline-color: var(--list-item-outline-color);
|
|
37026
37423
|
outline-offset: var(--list-item-outline-offset);
|
|
@@ -39883,13 +40280,12 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
|
|
|
39883
40280
|
}
|
|
39884
40281
|
}
|
|
39885
40282
|
.navi_picker_right_slot {
|
|
39886
|
-
--slot-spacing: calc(0.
|
|
40283
|
+
--slot-spacing: calc(var(--x-picker-padding-right) * 0.5);
|
|
39887
40284
|
|
|
39888
40285
|
display: inline-flex;
|
|
39889
40286
|
height: 1em;
|
|
39890
40287
|
height: 1lh;
|
|
39891
40288
|
margin-right: var(--slot-spacing);
|
|
39892
|
-
margin-left: var(--slot-spacing);
|
|
39893
40289
|
flex-shrink: 0;
|
|
39894
40290
|
align-items: center;
|
|
39895
40291
|
align-self: flex-start;
|
|
@@ -39908,17 +40304,13 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
|
|
|
39908
40304
|
}
|
|
39909
40305
|
}
|
|
39910
40306
|
.navi_picker_input {
|
|
39911
|
-
position: absolute;
|
|
39912
|
-
inset: 0;
|
|
39913
40307
|
box-sizing: border-box;
|
|
39914
|
-
width: 100%;
|
|
39915
|
-
height: 100%;
|
|
39916
40308
|
margin: 0;
|
|
39917
40309
|
padding: 0;
|
|
39918
40310
|
background: none;
|
|
39919
40311
|
border: none;
|
|
39920
|
-
|
|
39921
|
-
|
|
40312
|
+
border-radius: inherit;
|
|
40313
|
+
outline: none;
|
|
39922
40314
|
cursor: inherit;
|
|
39923
40315
|
pointer-events: auto;
|
|
39924
40316
|
|
|
@@ -39927,6 +40319,22 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
|
|
|
39927
40319
|
}
|
|
39928
40320
|
}
|
|
39929
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
|
+
|
|
39930
40338
|
.navi_picker_content {
|
|
39931
40339
|
display: contents;
|
|
39932
40340
|
text-align: initial; /* Don't inherit picker text align */
|
|
@@ -40057,6 +40465,7 @@ const PickerButton = props => {
|
|
|
40057
40465
|
"data-variant": variant,
|
|
40058
40466
|
"navi-picker": "",
|
|
40059
40467
|
"navi-single-line": isSingleLine ? "" : undefined,
|
|
40468
|
+
"navi-ui-custom": ui === "default" ? undefined : "",
|
|
40060
40469
|
...pickerRemainingProps,
|
|
40061
40470
|
basePseudoState: basePseudoState,
|
|
40062
40471
|
styleCSSVars: PickerStyleCSSVars,
|
|
@@ -40088,6 +40497,7 @@ const PickerButton = props => {
|
|
|
40088
40497
|
children: undefined // we will render children into the div
|
|
40089
40498
|
,
|
|
40090
40499
|
|
|
40500
|
+
ui: ui,
|
|
40091
40501
|
onFocus: e => {
|
|
40092
40502
|
inputProps.onFocus?.(e);
|
|
40093
40503
|
e.target.select();
|
|
@@ -40157,7 +40567,7 @@ const PickerButton = props => {
|
|
|
40157
40567
|
});
|
|
40158
40568
|
e.preventDefault();
|
|
40159
40569
|
}
|
|
40160
|
-
}), variant === "icon" || variant === "headless" ? null : jsx(Text, {
|
|
40570
|
+
}), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
|
|
40161
40571
|
className: "navi_picker_value",
|
|
40162
40572
|
"navi-placeholder": value === undefined || value === "" ? "" : undefined,
|
|
40163
40573
|
maxLines: maxLines,
|
|
@@ -40169,7 +40579,7 @@ const PickerButton = props => {
|
|
|
40169
40579
|
},
|
|
40170
40580
|
children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
|
|
40171
40581
|
})
|
|
40172
|
-
}), variant === "headless" ? null : jsx("span", {
|
|
40582
|
+
}), variant === "headless" || ui === "default" ? null : jsx("span", {
|
|
40173
40583
|
className: "navi_picker_right_slot",
|
|
40174
40584
|
children: jsx(Icon, {
|
|
40175
40585
|
size: "m",
|
|
@@ -40188,12 +40598,21 @@ const isWithinPickerContent = (el, pickerEl) => {
|
|
|
40188
40598
|
return pickerEl.querySelector(".navi_picker_content")?.contains(el);
|
|
40189
40599
|
};
|
|
40190
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
|
+
}
|
|
40191
40612
|
return jsx(Box, {
|
|
40192
40613
|
as: "input",
|
|
40193
|
-
"data-readonly-forced": props.readOnly ? undefined : "",
|
|
40194
40614
|
...props,
|
|
40195
|
-
|
|
40196
|
-
readOnly: true,
|
|
40615
|
+
ui: undefined,
|
|
40197
40616
|
className: "navi_picker_input",
|
|
40198
40617
|
pseudoClasses: PickerInputPseudoClasses,
|
|
40199
40618
|
onKeyDown: e => {
|
|
@@ -40201,12 +40620,17 @@ const PickerInput = props => {
|
|
|
40201
40620
|
if (e.key === "Enter") {
|
|
40202
40621
|
// prevent form submission now that input can have focus
|
|
40203
40622
|
e.preventDefault();
|
|
40204
|
-
} 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)
|
|
40205
40625
|
performTabNavigation(e);
|
|
40206
40626
|
}
|
|
40207
40627
|
}
|
|
40208
40628
|
});
|
|
40209
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"]);
|
|
40210
40634
|
const PICKER_BUTTON_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-loading", ":-navi-expanded", ":-navi-has-value"];
|
|
40211
40635
|
const PickerInputPseudoClasses = [":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value", ":-navi-expanded"];
|
|
40212
40636
|
const PickerStyleCSSVars = {
|