@jsenv/navi 0.14.15 → 0.14.17

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.
@@ -4,7 +4,7 @@ import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
4
4
  import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, resolveCSSSize, findBefore, findAfter, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, hasCSSSizeUnit, activeElementSignal, canInterceptKeys, pickLightOrDark, resolveColorLuminance, initFocusGroup, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, elementIsFocusable } from "@jsenv/dom";
5
5
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
6
6
  import { effect, signal, computed, batch, useSignal } from "@preact/signals";
7
- import { createContext, toChildArray, createRef, cloneElement } from "preact";
7
+ import { createContext, render, isValidElement, toChildArray, createRef, cloneElement } from "preact";
8
8
  import { createPortal, forwardRef } from "preact/compat";
9
9
 
10
10
  const actionPrivatePropertiesWeakMap = new WeakMap();
@@ -4736,6 +4736,23 @@ const DIMENSION_PROPS = {
4736
4736
  height: PASS_THROUGH,
4737
4737
  minHeight: PASS_THROUGH,
4738
4738
  maxHeight: PASS_THROUGH,
4739
+ square: (v) => {
4740
+ if (!v) {
4741
+ return null;
4742
+ }
4743
+ return {
4744
+ aspectRatio: "1/1",
4745
+ };
4746
+ },
4747
+ circle: (v) => {
4748
+ if (!v) {
4749
+ return null;
4750
+ }
4751
+ return {
4752
+ aspectRatio: "1/1",
4753
+ borderRadius: "100%",
4754
+ };
4755
+ },
4739
4756
  expand: applyOnTwoProps("expandX", "expandY"),
4740
4757
  shrink: applyOnTwoProps("shrinkX", "shrinkY"),
4741
4758
  // apply after width/height to override if both are set
@@ -5088,9 +5105,15 @@ const getHowToHandleStyleProp = (name) => {
5088
5105
  }
5089
5106
  return getStyle;
5090
5107
  };
5091
- const prepareStyleValue = (existingValue, value, name, context) => {
5108
+ const prepareStyleValue = (
5109
+ existingValue,
5110
+ value,
5111
+ name,
5112
+ styleContext,
5113
+ context,
5114
+ ) => {
5092
5115
  const normalizer = getNormalizer(name);
5093
- const cssValue = normalizer(value, name);
5116
+ const cssValue = normalizer(value, name, styleContext, context);
5094
5117
  const mergedValue = mergeOneStyle(existingValue, cssValue, name, context);
5095
5118
  return mergedValue;
5096
5119
  };
@@ -5619,11 +5642,18 @@ const initPseudoStyles = (
5619
5642
  return teardown;
5620
5643
  };
5621
5644
 
5622
- const applyStyle = (element, style, pseudoState, pseudoNamedStyles) => {
5645
+ const applyStyle = (
5646
+ element,
5647
+ style,
5648
+ pseudoState,
5649
+ pseudoNamedStyles,
5650
+ preventInitialTransition,
5651
+ ) => {
5623
5652
  if (!element) {
5624
5653
  return;
5625
5654
  }
5626
- updateStyle(element, getStyleToApply(style, pseudoState, pseudoNamedStyles));
5655
+ const styleToApply = getStyleToApply(style, pseudoState, pseudoNamedStyles);
5656
+ updateStyle(element, styleToApply, preventInitialTransition);
5627
5657
  };
5628
5658
 
5629
5659
  const PSEUDO_STATE_DEFAULT = {};
@@ -5677,40 +5707,71 @@ const getStyleToApply = (styles, pseudoState, pseudoNamedStyles) => {
5677
5707
  };
5678
5708
 
5679
5709
  const styleKeySetWeakMap = new WeakMap();
5680
- const updateStyle = (element, style) => {
5681
- const oldStyleKeySet = styleKeySetWeakMap.get(element);
5682
- const styleKeySet = new Set(style ? Object.keys(style) : []);
5683
- if (!oldStyleKeySet) {
5684
- for (const key of styleKeySet) {
5685
- if (key.startsWith("--")) {
5686
- element.style.setProperty(key, style[key]);
5710
+ const elementTransitionWeakMap = new WeakMap();
5711
+ const elementRenderedWeakSet = new WeakSet();
5712
+ const NO_STYLE_KEY_SET = new Set();
5713
+ const updateStyle = (element, style, preventInitialTransition) => {
5714
+ const styleKeySet = style ? new Set(Object.keys(style)) : NO_STYLE_KEY_SET;
5715
+ const oldStyleKeySet = styleKeySetWeakMap.get(element) || NO_STYLE_KEY_SET;
5716
+ // TRANSITION ANTI-FLICKER STRATEGY:
5717
+ // Problem: When setting both transition and styled properties simultaneously
5718
+ // (e.g., el.style.transition = "border-radius 0.3s ease"; el.style.borderRadius = "20px"),
5719
+ // the browser will immediately perform a transition even if no transition existed before.
5720
+ //
5721
+ // Solution: Temporarily disable transitions during initial style application by setting
5722
+ // transition to "none", then restore the intended transition after the frame completes.
5723
+ // We handle multiple updateStyle calls in the same frame gracefully - only one
5724
+ // requestAnimationFrame is scheduled per element, and the final transition value wins.
5725
+ let styleKeySetToApply = styleKeySet;
5726
+ if (!elementRenderedWeakSet.has(element)) {
5727
+ const hasTransition = styleKeySet.has("transition");
5728
+ if (hasTransition || preventInitialTransition) {
5729
+ if (elementTransitionWeakMap.has(element)) {
5730
+ elementTransitionWeakMap.set(element, style?.transition);
5687
5731
  } else {
5688
- element.style[key] = style[key];
5732
+ element.style.transition = "none";
5733
+ elementTransitionWeakMap.set(element, style?.transition);
5734
+ }
5735
+ // Don't apply the transition property now - we've set it to "none" temporarily
5736
+ styleKeySetToApply = new Set(styleKeySet);
5737
+ styleKeySetToApply.delete("transition");
5738
+ }
5739
+ requestAnimationFrame(() => {
5740
+ if (elementTransitionWeakMap.has(element)) {
5741
+ const transitionToRestore = elementTransitionWeakMap.get(element);
5742
+ if (transitionToRestore === undefined) {
5743
+ element.style.transition = "";
5744
+ } else {
5745
+ element.style.transition = transitionToRestore;
5746
+ }
5747
+ elementTransitionWeakMap.delete(element);
5689
5748
  }
5690
- }
5691
- styleKeySetWeakMap.set(element, styleKeySet);
5692
- return;
5749
+ elementRenderedWeakSet.add(element);
5750
+ });
5693
5751
  }
5694
- const toDeleteKeySet = new Set(oldStyleKeySet);
5695
- for (const key of styleKeySet) {
5696
- toDeleteKeySet.delete(key);
5752
+
5753
+ // Apply all styles normally (excluding transition during anti-flicker)
5754
+ const keysToDelete = new Set(oldStyleKeySet);
5755
+ for (const key of styleKeySetToApply) {
5756
+ keysToDelete.delete(key);
5757
+ const value = style[key];
5697
5758
  if (key.startsWith("--")) {
5698
- element.style.setProperty(key, style[key]);
5759
+ element.style.setProperty(key, value);
5699
5760
  } else {
5700
- element.style[key] = style[key];
5761
+ element.style[key] = value;
5701
5762
  }
5702
5763
  }
5703
- for (const toDeleteKey of toDeleteKeySet) {
5704
- if (toDeleteKey.startsWith("--")) {
5705
- element.style.removeProperty(toDeleteKey);
5764
+
5765
+ // Remove obsolete styles
5766
+ for (const key of keysToDelete) {
5767
+ if (key.startsWith("--")) {
5768
+ element.style.removeProperty(key);
5706
5769
  } else {
5707
- // we can't use removeProperty because "toDeleteKey" is in camelCase
5708
- // e.g., backgroundColor (and it's safer to just let the browser do the conversion)
5709
- element.style[toDeleteKey] = "";
5770
+ element.style[key] = "";
5710
5771
  }
5711
5772
  }
5773
+
5712
5774
  styleKeySetWeakMap.set(element, styleKeySet);
5713
- return;
5714
5775
  };
5715
5776
 
5716
5777
  installImportMetaCss(import.meta);import.meta.css = /* css */`
@@ -5759,6 +5820,10 @@ const Box = props => {
5759
5820
  // -> introduced for <Input /> with a wrapped for loading, checkboxes, etc
5760
5821
  pseudoStateSelector,
5761
5822
  hasChildFunction,
5823
+ // preventInitialTransition can be used to prevent transition on mount
5824
+ // (when transition is set via props, this is done automatically)
5825
+ // so this prop is useful only when transition is enabled from "outside" (via CSS)
5826
+ preventInitialTransition,
5762
5827
  children,
5763
5828
  ...rest
5764
5829
  } = props;
@@ -5812,7 +5877,7 @@ const Box = props => {
5812
5877
  // Style context dependencies
5813
5878
  styleCSSVars, pseudoClasses, pseudoElements,
5814
5879
  // Selectors
5815
- visualSelector, pseudoStateSelector];
5880
+ visualSelector, pseudoStateSelector, preventInitialTransition];
5816
5881
  let innerPseudoState;
5817
5882
  if (basePseudoState && pseudoState) {
5818
5883
  innerPseudoState = {};
@@ -5866,7 +5931,7 @@ const Box = props => {
5866
5931
  const addStyle = (value, name, styleContext, stylesTarget, context) => {
5867
5932
  styleDeps.push(value); // impact box style -> add to deps
5868
5933
  const cssVar = styleContext.styleCSSVars[name];
5869
- const mergedValue = prepareStyleValue(stylesTarget[name], value, name, context);
5934
+ const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext, context);
5870
5935
  if (cssVar) {
5871
5936
  stylesTarget[cssVar] = mergedValue;
5872
5937
  return true;
@@ -6020,7 +6085,7 @@ const Box = props => {
6020
6085
  }
6021
6086
  const updateStyle = useCallback(state => {
6022
6087
  const boxEl = ref.current;
6023
- applyStyle(boxEl, boxStyles, state, boxPseudoNamedStyles);
6088
+ applyStyle(boxEl, boxStyles, state, boxPseudoNamedStyles, preventInitialTransition);
6024
6089
  }, styleDeps);
6025
6090
  const finalStyleDeps = [pseudoStateSelector, innerPseudoState, updateStyle];
6026
6091
  // By default ":hover", ":active" are not tracked.
@@ -11165,6 +11230,20 @@ const useAutoFocus = (
11165
11230
  }, []);
11166
11231
  };
11167
11232
 
11233
+ const CalloutCloseContext = createContext();
11234
+ const useCalloutClose = () => {
11235
+ return useContext(CalloutCloseContext);
11236
+ };
11237
+ const renderIntoCallout = (jsx$1, calloutMessageElement, {
11238
+ close
11239
+ }) => {
11240
+ const calloutJsx = jsx(CalloutCloseContext.Provider, {
11241
+ value: close,
11242
+ children: jsx$1
11243
+ });
11244
+ render(calloutJsx, calloutMessageElement);
11245
+ };
11246
+
11168
11247
  installImportMetaCss(import.meta);
11169
11248
  /**
11170
11249
  * A callout component that mimics native browser validation messages.
@@ -11271,6 +11350,10 @@ import.meta.css = /* css */ `
11271
11350
  }
11272
11351
 
11273
11352
  .navi_callout_message {
11353
+ position: relative;
11354
+ display: inline-flex;
11355
+ box-sizing: border-box;
11356
+ box-decoration-break: clone;
11274
11357
  min-width: 0;
11275
11358
  align-self: center;
11276
11359
  word-break: break-word;
@@ -11413,28 +11496,44 @@ const openCallout = (
11413
11496
  closeOnClickOutside = options.closeOnClickOutside;
11414
11497
  }
11415
11498
 
11416
- if (Error.isError(newMessage)) {
11417
- const error = newMessage;
11418
- newMessage = error.message;
11419
- if (showErrorStack && error.stack) {
11420
- newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(String(error.stack))}</pre>`;
11499
+ if (isValidElement(newMessage)) {
11500
+ calloutMessageElement.innerHTML = "";
11501
+ renderIntoCallout(newMessage, calloutMessageElement, { close });
11502
+ } else if (newMessage instanceof Node) {
11503
+ // Handle DOM node (cloned from CSS selector)
11504
+ calloutMessageElement.innerHTML = "";
11505
+ calloutMessageElement.appendChild(newMessage);
11506
+ } else if (typeof newMessage === "function") {
11507
+ calloutMessageElement.innerHTML = "";
11508
+ newMessage({
11509
+ renderIntoCallout: (jsx) =>
11510
+ renderIntoCallout(jsx, calloutMessageElement, { close }),
11511
+ close,
11512
+ });
11513
+ } else {
11514
+ if (Error.isError(newMessage)) {
11515
+ const error = newMessage;
11516
+ newMessage = error.message;
11517
+ if (showErrorStack && error.stack) {
11518
+ newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(String(error.stack))}</pre>`;
11519
+ }
11421
11520
  }
11422
- }
11423
11521
 
11424
- // Check if the message is a full HTML document (starts with DOCTYPE)
11425
- if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
11426
- // Create iframe to isolate the HTML document
11427
- const iframe = document.createElement("iframe");
11428
- iframe.style.border = "none";
11429
- iframe.style.width = "100%";
11430
- iframe.style.backgroundColor = "white";
11431
- iframe.srcdoc = newMessage;
11522
+ // Check if the message is a full HTML document (starts with DOCTYPE)
11523
+ if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
11524
+ // Create iframe to isolate the HTML document
11525
+ const iframe = document.createElement("iframe");
11526
+ iframe.style.border = "none";
11527
+ iframe.style.width = "100%";
11528
+ iframe.style.backgroundColor = "white";
11529
+ iframe.srcdoc = newMessage;
11432
11530
 
11433
- // Clear existing content and add iframe
11434
- calloutMessageElement.innerHTML = "";
11435
- calloutMessageElement.appendChild(iframe);
11436
- } else {
11437
- calloutMessageElement.innerHTML = newMessage;
11531
+ // Clear existing content and add iframe
11532
+ calloutMessageElement.innerHTML = "";
11533
+ calloutMessageElement.appendChild(iframe);
11534
+ } else {
11535
+ calloutMessageElement.innerHTML = newMessage;
11536
+ }
11438
11537
  }
11439
11538
  };
11440
11539
  {
@@ -11463,6 +11562,15 @@ const openCallout = (
11463
11562
  document.removeEventListener("click", handleClickOutside, true);
11464
11563
  });
11465
11564
  }
11565
+ {
11566
+ const handleCustomCloseEvent = () => {
11567
+ close("custom_event");
11568
+ };
11569
+ calloutElement.addEventListener(
11570
+ "navi_callout_close",
11571
+ handleCustomCloseEvent,
11572
+ );
11573
+ }
11466
11574
  Object.assign(callout, {
11467
11575
  element: calloutElement,
11468
11576
  update,
@@ -12195,6 +12303,189 @@ const generateSvgWithoutArrow = (width, height) => {
12195
12303
  </svg>`;
12196
12304
  };
12197
12305
 
12306
+ /**
12307
+ * Creates a live mirror of a source DOM element that automatically stays in sync.
12308
+ *
12309
+ * The mirror is implemented as a custom element (`<navi-mirror>`) that:
12310
+ * - Copies the source element's content (innerHTML) and attributes
12311
+ * - Automatically updates when the source element changes
12312
+ * - Efficiently manages observers based on DOM presence (starts observing when
12313
+ * added to DOM, stops when removed)
12314
+ * - Excludes the 'id' attribute to avoid conflicts
12315
+ *
12316
+ * @param {Element} sourceElement - The DOM element to mirror. Any changes to this
12317
+ * element's content, attributes, or structure will be automatically reflected
12318
+ * in the returned mirror element.
12319
+ *
12320
+ * @returns {NaviMirror} A custom element that mirrors the source element. Can be
12321
+ * inserted into the DOM like any other element. The mirror will automatically
12322
+ * start/stop observing the source based on its DOM presence.
12323
+ */
12324
+ const createNaviMirror = (sourceElement) => {
12325
+ const naviMirror = new NaviMirror(sourceElement);
12326
+ return naviMirror;
12327
+ };
12328
+
12329
+ // Custom element that mirrors another element's content
12330
+ class NaviMirror extends HTMLElement {
12331
+ constructor(sourceElement) {
12332
+ super();
12333
+ this.sourceElement = null;
12334
+ this.sourceObserver = null;
12335
+ this.setSourceElement(sourceElement);
12336
+ }
12337
+
12338
+ setSourceElement(sourceElement) {
12339
+ this.sourceElement = sourceElement;
12340
+ this.updateFromSource();
12341
+ }
12342
+
12343
+ updateFromSource() {
12344
+ if (!this.sourceElement) return;
12345
+
12346
+ this.innerHTML = this.sourceElement.innerHTML;
12347
+ // Copy attributes from source (except id to avoid conflicts)
12348
+ for (const attr of Array.from(this.sourceElement.attributes)) {
12349
+ if (attr.name !== "id") {
12350
+ this.setAttribute(attr.name, attr.value);
12351
+ }
12352
+ }
12353
+ }
12354
+
12355
+ startObserving() {
12356
+ if (this.sourceObserver || !this.sourceElement) return;
12357
+ this.sourceObserver = new MutationObserver(() => {
12358
+ this.updateFromSource();
12359
+ });
12360
+ this.sourceObserver.observe(this.sourceElement, {
12361
+ childList: true,
12362
+ subtree: true,
12363
+ attributes: true,
12364
+ characterData: true,
12365
+ });
12366
+ }
12367
+
12368
+ stopObserving() {
12369
+ if (this.sourceObserver) {
12370
+ this.sourceObserver.disconnect();
12371
+ this.sourceObserver = null;
12372
+ }
12373
+ }
12374
+
12375
+ // Called when element is added to DOM
12376
+ connectedCallback() {
12377
+ this.startObserving();
12378
+ }
12379
+
12380
+ // Called when element is removed from DOM
12381
+ disconnectedCallback() {
12382
+ this.stopObserving();
12383
+ }
12384
+ }
12385
+
12386
+ // Register the custom element if not already registered
12387
+ if (!customElements.get("navi-mirror")) {
12388
+ customElements.define("navi-mirror", NaviMirror);
12389
+ }
12390
+
12391
+ const getMessageFromAttribute = (
12392
+ originalElement,
12393
+ attributeName,
12394
+ generatedMessage,
12395
+ ) => {
12396
+ const selectorAttributeName = `${attributeName}-selector`;
12397
+ const eventAttributeName = `${attributeName}-event`;
12398
+ const resolutionSteps = [
12399
+ {
12400
+ description: "original element",
12401
+ element: originalElement,
12402
+ },
12403
+ {
12404
+ description: "closest fieldset",
12405
+ element: originalElement.closest("fieldset"),
12406
+ },
12407
+ {
12408
+ description: "closest form",
12409
+ element: originalElement.closest("form"),
12410
+ },
12411
+ ];
12412
+ // Sub-steps for each element (in order of priority)
12413
+ const subSteps = ["event", "selector", "message"];
12414
+ let currentStepIndex = 0;
12415
+ let currentSubStepIndex = 0;
12416
+ const resolve = () => {
12417
+ while (currentStepIndex < resolutionSteps.length) {
12418
+ const { element } = resolutionSteps[currentStepIndex];
12419
+ if (element) {
12420
+ while (currentSubStepIndex < subSteps.length) {
12421
+ const subStep = subSteps[currentSubStepIndex];
12422
+ currentSubStepIndex++;
12423
+ if (subStep === "event") {
12424
+ const eventAttribute = element.getAttribute(eventAttributeName);
12425
+ if (eventAttribute) {
12426
+ return createEventHandler(element, eventAttribute);
12427
+ }
12428
+ }
12429
+ if (subStep === "selector") {
12430
+ const selectorAttribute = element.getAttribute(
12431
+ selectorAttributeName,
12432
+ );
12433
+ if (selectorAttribute) {
12434
+ return fromSelectorAttribute(selectorAttribute);
12435
+ }
12436
+ }
12437
+ if (subStep === "message") {
12438
+ const messageAttribute = element.getAttribute(attributeName);
12439
+ if (messageAttribute) {
12440
+ return messageAttribute;
12441
+ }
12442
+ }
12443
+ }
12444
+ }
12445
+ currentStepIndex++;
12446
+ currentSubStepIndex = 0;
12447
+ }
12448
+ return generatedMessage;
12449
+ };
12450
+
12451
+ const createEventHandler = (element, eventName) => {
12452
+ return ({ renderIntoCallout }) => {
12453
+ element.dispatchEvent(
12454
+ new CustomEvent(eventName, {
12455
+ detail: {
12456
+ render: (message) => {
12457
+ if (message) {
12458
+ renderIntoCallout(message);
12459
+ } else {
12460
+ // Resume resolution from next step
12461
+ const nextResult = resolve();
12462
+ renderIntoCallout(nextResult);
12463
+ }
12464
+ },
12465
+ },
12466
+ }),
12467
+ );
12468
+ };
12469
+ };
12470
+
12471
+ return resolve();
12472
+ };
12473
+
12474
+ // Helper function to resolve messages that might be CSS selectors
12475
+ const fromSelectorAttribute = (messageAttributeValue) => {
12476
+ // It's a CSS selector, find the DOM element
12477
+ const messageSourceElement = document.querySelector(messageAttributeValue);
12478
+ if (!messageSourceElement) {
12479
+ console.warn(
12480
+ `Message selector "${messageAttributeValue}" not found in DOM`,
12481
+ );
12482
+ return null; // Fallback to the generic message
12483
+ }
12484
+ const mirror = createNaviMirror(messageSourceElement);
12485
+ mirror.setAttribute("data-source-selector", messageAttributeValue);
12486
+ return mirror;
12487
+ };
12488
+
12198
12489
  const generateFieldInvalidMessage = (template, { field }) => {
12199
12490
  return replaceStringVars(template, {
12200
12491
  "{field}": () => generateThisFieldText(field),
@@ -12232,6 +12523,7 @@ const replaceStringVars = (string, replacers) => {
12232
12523
 
12233
12524
  const MIN_LOWER_LETTER_CONSTRAINT = {
12234
12525
  name: "min_lower_letter",
12526
+ messageAttribute: "data-min-lower-letter-message",
12235
12527
  check: (field) => {
12236
12528
  const fieldValue = field.value;
12237
12529
  if (!fieldValue && !field.required) {
@@ -12249,12 +12541,6 @@ const MIN_LOWER_LETTER_CONSTRAINT = {
12249
12541
  }
12250
12542
  }
12251
12543
  if (numberOfLowercaseChars < min) {
12252
- const messageAttribute = field.getAttribute(
12253
- "data-min-lower-letter-message",
12254
- );
12255
- if (messageAttribute) {
12256
- return messageAttribute;
12257
- }
12258
12544
  if (min === 0) {
12259
12545
  return generateFieldInvalidMessage(
12260
12546
  `{field} doit contenir au moins une lettre minuscule.`,
@@ -12271,6 +12557,7 @@ const MIN_LOWER_LETTER_CONSTRAINT = {
12271
12557
  };
12272
12558
  const MIN_UPPER_LETTER_CONSTRAINT = {
12273
12559
  name: "min_upper_letter",
12560
+ messageAttribute: "data-min-upper-letter-message",
12274
12561
  check: (field) => {
12275
12562
  const fieldValue = field.value;
12276
12563
  if (!fieldValue && !field.required) {
@@ -12288,12 +12575,6 @@ const MIN_UPPER_LETTER_CONSTRAINT = {
12288
12575
  }
12289
12576
  }
12290
12577
  if (numberOfUppercaseChars < min) {
12291
- const messageAttribute = field.getAttribute(
12292
- "data-min-upper-letter-message",
12293
- );
12294
- if (messageAttribute) {
12295
- return messageAttribute;
12296
- }
12297
12578
  if (min === 0) {
12298
12579
  return generateFieldInvalidMessage(
12299
12580
  `{field} doit contenir au moins une lettre majuscule.`,
@@ -12310,6 +12591,7 @@ const MIN_UPPER_LETTER_CONSTRAINT = {
12310
12591
  };
12311
12592
  const MIN_DIGIT_CONSTRAINT = {
12312
12593
  name: "min_digit",
12594
+ messageAttribute: "data-min-digit-message",
12313
12595
  check: (field) => {
12314
12596
  const fieldValue = field.value;
12315
12597
  if (!fieldValue && !field.required) {
@@ -12327,10 +12609,6 @@ const MIN_DIGIT_CONSTRAINT = {
12327
12609
  }
12328
12610
  }
12329
12611
  if (numberOfDigitChars < min) {
12330
- const messageAttribute = field.getAttribute("data-min-digit-message");
12331
- if (messageAttribute) {
12332
- return messageAttribute;
12333
- }
12334
12612
  if (min === 0) {
12335
12613
  return generateFieldInvalidMessage(
12336
12614
  `{field} doit contenir au moins un chiffre.`,
@@ -12345,44 +12623,39 @@ const MIN_DIGIT_CONSTRAINT = {
12345
12623
  return "";
12346
12624
  },
12347
12625
  };
12348
- const MIN_SPECIAL_CHARS_CONSTRAINT = {
12349
- name: "min_special_chars",
12626
+ const MIN_SPECIAL_CHAR_CONSTRAINT = {
12627
+ name: "min_special_char",
12628
+ messageAttribute: "data-min-special-char-message",
12350
12629
  check: (field) => {
12351
12630
  const fieldValue = field.value;
12352
12631
  if (!fieldValue && !field.required) {
12353
12632
  return "";
12354
12633
  }
12355
- const minSpecialChars = field.getAttribute("data-min-special-chars");
12634
+ const minSpecialChars = field.getAttribute("data-min-special-char");
12356
12635
  if (!minSpecialChars) {
12357
12636
  return "";
12358
12637
  }
12359
12638
  const min = parseInt(minSpecialChars, 10);
12360
- const specialChars = field.getAttribute("data-special-chars");
12361
- if (!specialChars) {
12362
- return "L'attribut data-special-chars doit être défini pour utiliser data-min-special-chars.";
12639
+ const specialCharset = field.getAttribute("data-special-charset");
12640
+ if (!specialCharset) {
12641
+ return "L'attribut data-special-charset doit être défini pour utiliser data-min-special-char.";
12363
12642
  }
12364
12643
 
12365
12644
  let numberOfSpecialChars = 0;
12366
12645
  for (const char of fieldValue) {
12367
- if (specialChars.includes(char)) {
12646
+ if (specialCharset.includes(char)) {
12368
12647
  numberOfSpecialChars++;
12369
12648
  }
12370
12649
  }
12371
12650
  if (numberOfSpecialChars < min) {
12372
- const messageAttribute = field.getAttribute(
12373
- "data-min-special-chars-message",
12374
- );
12375
- if (messageAttribute) {
12376
- return messageAttribute;
12377
- }
12378
12651
  if (min === 1) {
12379
12652
  return generateFieldInvalidMessage(
12380
- `{field} doit contenir au moins un caractère spécial. (${specialChars})`,
12653
+ `{field} doit contenir au moins un caractère spécial. (${specialCharset})`,
12381
12654
  { field },
12382
12655
  );
12383
12656
  }
12384
12657
  return generateFieldInvalidMessage(
12385
- `{field} doit contenir au moins ${min} caractères spéciaux (${specialChars})`,
12658
+ `{field} doit contenir au moins ${min} caractères spéciaux (${specialCharset})`,
12386
12659
  { field },
12387
12660
  );
12388
12661
  }
@@ -12392,6 +12665,7 @@ const MIN_SPECIAL_CHARS_CONSTRAINT = {
12392
12665
 
12393
12666
  const READONLY_CONSTRAINT = {
12394
12667
  name: "readonly",
12668
+ messageAttribute: "data-readonly-message",
12395
12669
  check: (field, { skipReadonly }) => {
12396
12670
  if (skipReadonly) {
12397
12671
  return null;
@@ -12405,25 +12679,21 @@ const READONLY_CONSTRAINT = {
12405
12679
  const isButton = field.tagName === "BUTTON";
12406
12680
  const isBusy = field.getAttribute("aria-busy") === "true";
12407
12681
  const readonlySilent = field.hasAttribute("data-readonly-silent");
12408
- const messageAttribute = field.getAttribute("data-readonly-message");
12409
12682
  if (readonlySilent) {
12410
12683
  return { silent: true };
12411
12684
  }
12412
12685
  if (isBusy) {
12413
12686
  return {
12414
12687
  target: field,
12415
- message:
12416
- messageAttribute || `Cette action est en cours. Veuillez patienter.`,
12688
+ message: `Cette action est en cours. Veuillez patienter.`,
12417
12689
  status: "info",
12418
12690
  };
12419
12691
  }
12420
12692
  return {
12421
12693
  target: field,
12422
- message:
12423
- messageAttribute ||
12424
- (isButton
12425
- ? `Cet action n'est pas disponible pour l'instant.`
12426
- : `Cet élément est en lecture seule et ne peut pas être modifié.`),
12694
+ message: isButton
12695
+ ? `Cet action n'est pas disponible pour l'instant.`
12696
+ : `Cet élément est en lecture seule et ne peut pas être modifié.`,
12427
12697
  status: "info",
12428
12698
  };
12429
12699
  },
@@ -12431,6 +12701,7 @@ const READONLY_CONSTRAINT = {
12431
12701
 
12432
12702
  const SAME_AS_CONSTRAINT = {
12433
12703
  name: "same_as",
12704
+ messageAttribute: "data-same-as-message",
12434
12705
  check: (field) => {
12435
12706
  const sameAs = field.getAttribute("data-same-as");
12436
12707
  if (!sameAs) {
@@ -12455,10 +12726,6 @@ const SAME_AS_CONSTRAINT = {
12455
12726
  if (fieldValue === otherFieldValue) {
12456
12727
  return null;
12457
12728
  }
12458
- const messageAttribute = field.getAttribute("data-same-as-message");
12459
- if (messageAttribute) {
12460
- return messageAttribute;
12461
- }
12462
12729
  const type = field.type;
12463
12730
  if (type === "password") {
12464
12731
  return `Ce mot de passe doit être identique au précédent.`;
@@ -12472,6 +12739,7 @@ const SAME_AS_CONSTRAINT = {
12472
12739
 
12473
12740
  const SINGLE_SPACE_CONSTRAINT = {
12474
12741
  name: "single_space",
12742
+ messageAttribute: "data-single-space-message",
12475
12743
  check: (field) => {
12476
12744
  const singleSpace = field.hasAttribute("data-single-space");
12477
12745
  if (!singleSpace) {
@@ -12482,10 +12750,6 @@ const SINGLE_SPACE_CONSTRAINT = {
12482
12750
  const hasTrailingSpace = fieldValue.endsWith(" ");
12483
12751
  const hasDoubleSpace = fieldValue.includes(" ");
12484
12752
  if (hasLeadingSpace || hasDoubleSpace || hasTrailingSpace) {
12485
- const messageAttribute = field.getAttribute("data-single-space-message");
12486
- if (messageAttribute) {
12487
- return messageAttribute;
12488
- }
12489
12753
  if (hasLeadingSpace) {
12490
12754
  return generateFieldInvalidMessage(
12491
12755
  `{field} ne doit pas commencer par un espace.`,
@@ -12516,6 +12780,7 @@ const SINGLE_SPACE_CONSTRAINT = {
12516
12780
  // in our case it's just here in case some code is wrongly calling "requestAction" or "checkValidity" on a disabled element
12517
12781
  const DISABLED_CONSTRAINT = {
12518
12782
  name: "disabled",
12783
+ messageAttribute: "data-disabled-message",
12519
12784
  check: (field) => {
12520
12785
  if (field.disabled) {
12521
12786
  return generateFieldInvalidMessage(`{field} est désactivé.`, { field });
@@ -12526,17 +12791,14 @@ const DISABLED_CONSTRAINT = {
12526
12791
 
12527
12792
  const REQUIRED_CONSTRAINT = {
12528
12793
  name: "required",
12794
+ messageAttribute: "data-required-message",
12529
12795
  check: (field, { registerChange }) => {
12530
12796
  if (!field.required) {
12531
12797
  return null;
12532
12798
  }
12533
- const messageAttribute = field.getAttribute("data-required-message");
12534
12799
 
12535
12800
  if (field.type === "checkbox") {
12536
12801
  if (!field.checked) {
12537
- if (messageAttribute) {
12538
- return messageAttribute;
12539
- }
12540
12802
  return `Veuillez cocher cette case.`;
12541
12803
  }
12542
12804
  return null;
@@ -12547,19 +12809,12 @@ const REQUIRED_CONSTRAINT = {
12547
12809
  if (!name) {
12548
12810
  // If no name, check just this radio
12549
12811
  if (!field.checked) {
12550
- if (messageAttribute) {
12551
- return messageAttribute;
12552
- }
12553
12812
  return `Veuillez sélectionner une option.`;
12554
12813
  }
12555
12814
  return null;
12556
12815
  }
12557
12816
 
12558
12817
  const closestFieldset = field.closest("fieldset");
12559
- const fieldsetRequiredMessage = closestFieldset
12560
- ? closestFieldset.getAttribute("data-required-message")
12561
- : null;
12562
-
12563
12818
  // Find the container (form or closest fieldset)
12564
12819
  const container = field.form || closestFieldset || document;
12565
12820
  // Check if any radio with the same name is checked
@@ -12578,10 +12833,7 @@ const REQUIRED_CONSTRAINT = {
12578
12833
  }
12579
12834
 
12580
12835
  return {
12581
- message:
12582
- messageAttribute ||
12583
- fieldsetRequiredMessage ||
12584
- `Veuillez sélectionner une option.`,
12836
+ message: `Veuillez sélectionner une option.`,
12585
12837
  target: closestFieldset
12586
12838
  ? closestFieldset.querySelector("legend")
12587
12839
  : undefined,
@@ -12590,9 +12842,6 @@ const REQUIRED_CONSTRAINT = {
12590
12842
  if (field.value) {
12591
12843
  return null;
12592
12844
  }
12593
- if (messageAttribute) {
12594
- return messageAttribute;
12595
- }
12596
12845
  if (field.type === "password") {
12597
12846
  return field.hasAttribute("data-same-as")
12598
12847
  ? `Veuillez confirmer le mot de passe.`
@@ -12611,6 +12860,7 @@ const REQUIRED_CONSTRAINT = {
12611
12860
 
12612
12861
  const PATTERN_CONSTRAINT = {
12613
12862
  name: "pattern",
12863
+ messageAttribute: "data-pattern-message",
12614
12864
  check: (field) => {
12615
12865
  const pattern = field.pattern;
12616
12866
  if (!pattern) {
@@ -12624,10 +12874,6 @@ const PATTERN_CONSTRAINT = {
12624
12874
  if (regex.test(value)) {
12625
12875
  return null;
12626
12876
  }
12627
- const messageAttribute = field.getAttribute("data-pattern-message");
12628
- if (messageAttribute) {
12629
- return messageAttribute;
12630
- }
12631
12877
  let message = generateFieldInvalidMessage(
12632
12878
  `{field} ne correspond pas au format requis.`,
12633
12879
  { field },
@@ -12644,6 +12890,7 @@ const emailregex =
12644
12890
  /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
12645
12891
  const TYPE_EMAIL_CONSTRAINT = {
12646
12892
  name: "type_email",
12893
+ messageAttribute: "data-type-message",
12647
12894
  check: (field) => {
12648
12895
  if (field.type !== "email") {
12649
12896
  return null;
@@ -12652,17 +12899,10 @@ const TYPE_EMAIL_CONSTRAINT = {
12652
12899
  if (!value && !field.required) {
12653
12900
  return null;
12654
12901
  }
12655
- const messageAttribute = field.getAttribute("data-type-email-message");
12656
12902
  if (!value.includes("@")) {
12657
- if (messageAttribute) {
12658
- return messageAttribute;
12659
- }
12660
12903
  return `Veuillez inclure "@" dans l'adresse e-mail. Il manque un symbole "@" dans ${value}.`;
12661
12904
  }
12662
12905
  if (!emailregex.test(value)) {
12663
- if (messageAttribute) {
12664
- return messageAttribute;
12665
- }
12666
12906
  return `Veuillez saisir une adresse e-mail valide.`;
12667
12907
  }
12668
12908
  return null;
@@ -12671,6 +12911,7 @@ const TYPE_EMAIL_CONSTRAINT = {
12671
12911
 
12672
12912
  const MIN_LENGTH_CONSTRAINT = {
12673
12913
  name: "min_length",
12914
+ messageAttribute: "data-min-length-message",
12674
12915
  check: (field) => {
12675
12916
  if (field.tagName === "INPUT") {
12676
12917
  if (!INPUT_TYPE_SUPPORTING_MIN_LENGTH_SET.has(field.type)) {
@@ -12692,10 +12933,6 @@ const MIN_LENGTH_CONSTRAINT = {
12692
12933
  if (valueLength >= minLength) {
12693
12934
  return null;
12694
12935
  }
12695
- const messageAttribute = field.getAttribute("data-min-length-message");
12696
- if (messageAttribute) {
12697
- return messageAttribute;
12698
- }
12699
12936
  if (valueLength === 1) {
12700
12937
  return generateFieldInvalidMessage(
12701
12938
  `{field} doit contenir au moins ${minLength} caractère (il contient actuellement un seul caractère).`,
@@ -12719,6 +12956,7 @@ const INPUT_TYPE_SUPPORTING_MIN_LENGTH_SET = new Set([
12719
12956
 
12720
12957
  const MAX_LENGTH_CONSTRAINT = {
12721
12958
  name: "max_length",
12959
+ messageAttribute: "data-max-length-message",
12722
12960
  check: (field) => {
12723
12961
  if (field.tagName === "INPUT") {
12724
12962
  if (!INPUT_TYPE_SUPPORTING_MAX_LENGTH_SET.has(field.type)) {
@@ -12736,10 +12974,6 @@ const MAX_LENGTH_CONSTRAINT = {
12736
12974
  if (valueLength <= maxLength) {
12737
12975
  return null;
12738
12976
  }
12739
- const messageAttribute = field.getAttribute("data-max-length-message");
12740
- if (messageAttribute) {
12741
- return messageAttribute;
12742
- }
12743
12977
  return generateFieldInvalidMessage(
12744
12978
  `{field} doit contenir au maximum ${maxLength} caractères (il contient actuellement ${valueLength} caractères).`,
12745
12979
  { field },
@@ -12752,6 +12986,7 @@ const INPUT_TYPE_SUPPORTING_MAX_LENGTH_SET = new Set(
12752
12986
 
12753
12987
  const TYPE_NUMBER_CONSTRAINT = {
12754
12988
  name: "type_number",
12989
+ messageAttribute: "data-type-message",
12755
12990
  check: (field) => {
12756
12991
  if (field.tagName !== "INPUT") {
12757
12992
  return null;
@@ -12764,10 +12999,6 @@ const TYPE_NUMBER_CONSTRAINT = {
12764
12999
  }
12765
13000
  const value = field.valueAsNumber;
12766
13001
  if (isNaN(value)) {
12767
- const messageAttribute = field.getAttribute("data-type-number-message");
12768
- if (messageAttribute) {
12769
- return messageAttribute;
12770
- }
12771
13002
  return generateFieldInvalidMessage(`{field} doit être un nombre.`, {
12772
13003
  field,
12773
13004
  });
@@ -12778,6 +13009,7 @@ const TYPE_NUMBER_CONSTRAINT = {
12778
13009
 
12779
13010
  const MIN_CONSTRAINT = {
12780
13011
  name: "min",
13012
+ messageAttribute: "data-min-message",
12781
13013
  check: (field) => {
12782
13014
  if (field.tagName !== "INPUT") {
12783
13015
  return null;
@@ -12796,10 +13028,6 @@ const MIN_CONSTRAINT = {
12796
13028
  return null;
12797
13029
  }
12798
13030
  if (valueAsNumber < minNumber) {
12799
- const messageAttribute = field.getAttribute("data-min-message");
12800
- if (messageAttribute) {
12801
- return messageAttribute;
12802
- }
12803
13031
  return generateFieldInvalidMessage(
12804
13032
  `{field} doit être supérieur ou égal à <strong>${minString}</strong>.`,
12805
13033
  { field },
@@ -12815,11 +13043,7 @@ const MIN_CONSTRAINT = {
12815
13043
  const [minHours, minMinutes] = min.split(":").map(Number);
12816
13044
  const value = field.value;
12817
13045
  const [hours, minutes] = value.split(":").map(Number);
12818
- const messageAttribute = field.getAttribute("data-min-message");
12819
13046
  if (hours < minHours || (hours === minHours && minMinutes < minutes)) {
12820
- if (messageAttribute) {
12821
- return messageAttribute;
12822
- }
12823
13047
  return generateFieldInvalidMessage(
12824
13048
  `{field} doit être <strong>${min}</strong> ou plus.`,
12825
13049
  { field },
@@ -12838,6 +13062,7 @@ const MIN_CONSTRAINT = {
12838
13062
 
12839
13063
  const MAX_CONSTRAINT = {
12840
13064
  name: "max",
13065
+ messageAttribute: "data-max-message",
12841
13066
  check: (field) => {
12842
13067
  if (field.tagName !== "INPUT") {
12843
13068
  return null;
@@ -12856,10 +13081,6 @@ const MAX_CONSTRAINT = {
12856
13081
  return null;
12857
13082
  }
12858
13083
  if (valueAsNumber > maxNumber) {
12859
- const messageAttribute = field.getAttribute("data-max-message");
12860
- if (messageAttribute) {
12861
- return messageAttribute;
12862
- }
12863
13084
  return generateFieldInvalidMessage(
12864
13085
  `{field} être <strong>${maxAttribute}</strong> ou plus.`,
12865
13086
  { field },
@@ -12876,10 +13097,6 @@ const MAX_CONSTRAINT = {
12876
13097
  const value = field.value;
12877
13098
  const [hours, minutes] = value.split(":").map(Number);
12878
13099
  if (hours > maxHours || (hours === maxHours && maxMinutes > minutes)) {
12879
- const messageAttribute = field.getAttribute("data-max-message");
12880
- if (messageAttribute) {
12881
- return messageAttribute;
12882
- }
12883
13100
  return generateFieldInvalidMessage(
12884
13101
  `{field} doit être <strong>${max}</strong> ou moins.`,
12885
13102
  { field },
@@ -13055,7 +13272,7 @@ const NAVI_CONSTRAINT_SET = new Set([
13055
13272
  // the order matters here, the last constraint is picked first when multiple constraints fail
13056
13273
  // so it's better to keep the most complex constraints at the beginning of the list
13057
13274
  // so the more basic ones shows up first
13058
- MIN_SPECIAL_CHARS_CONSTRAINT,
13275
+ MIN_SPECIAL_CHAR_CONSTRAINT,
13059
13276
  SINGLE_SPACE_CONSTRAINT,
13060
13277
  MIN_DIGIT_CONSTRAINT,
13061
13278
  MIN_UPPER_LETTER_CONSTRAINT,
@@ -13337,6 +13554,21 @@ const installCustomConstraintValidation = (
13337
13554
  typeof checkResult === "string"
13338
13555
  ? { message: checkResult }
13339
13556
  : checkResult;
13557
+ constraintValidityInfo.messageString = constraintValidityInfo.message;
13558
+
13559
+ if (constraint.messageAttribute) {
13560
+ const messageFromAttribute = getMessageFromAttribute(
13561
+ element,
13562
+ constraint.messageAttribute,
13563
+ constraintValidityInfo.message,
13564
+ );
13565
+ if (messageFromAttribute !== constraintValidityInfo.message) {
13566
+ constraintValidityInfo.message = messageFromAttribute;
13567
+ if (typeof messageFromAttribute === "string") {
13568
+ constraintValidityInfo.messageString = messageFromAttribute;
13569
+ }
13570
+ }
13571
+ }
13340
13572
  const thisConstraintFailureInfo = {
13341
13573
  name: constraint.name,
13342
13574
  constraint,
@@ -13370,7 +13602,7 @@ const installCustomConstraintValidation = (
13370
13602
  if (!hasTitleAttribute) {
13371
13603
  // when a constraint is failing browser displays that constraint message if the element has no title attribute.
13372
13604
  // We want to do the same with our message (overriding the browser in the process to get better messages)
13373
- element.setAttribute("title", failedConstraintInfo.message);
13605
+ element.setAttribute("title", failedConstraintInfo.messageString);
13374
13606
  }
13375
13607
  } else {
13376
13608
  if (!hasTitleAttribute) {
@@ -13835,7 +14067,15 @@ const useCustomValidationRef = (elementRef, targetSelector) => {
13835
14067
  return null;
13836
14068
  }
13837
14069
  let target;
13838
- {
14070
+ if (targetSelector) {
14071
+ target = element.querySelector(targetSelector);
14072
+ if (!target) {
14073
+ console.warn(
14074
+ `useCustomValidationRef: targetSelector "${targetSelector}" did not match in element`,
14075
+ );
14076
+ return null;
14077
+ }
14078
+ } else {
13839
14079
  target = element;
13840
14080
  }
13841
14081
  const unsubscribe = subscribe(element, target);
@@ -13873,7 +14113,28 @@ const unsubscribe = (element) => {
13873
14113
  }
13874
14114
  };
13875
14115
 
13876
- const useConstraints = (elementRef, constraints, targetSelector) => {
14116
+ const NO_CONSTRAINTS = [];
14117
+ const useConstraints = (elementRef, props, { targetSelector } = {}) => {
14118
+ const {
14119
+ constraints = NO_CONSTRAINTS,
14120
+ disabledMessage,
14121
+ requiredMessage,
14122
+ patternMessage,
14123
+ minLengthMessage,
14124
+ maxLengthMessage,
14125
+ typeMessage,
14126
+ minMessage,
14127
+ maxMessage,
14128
+ singleSpaceMessage,
14129
+ sameAsMessage,
14130
+ minDigitMessage,
14131
+ minLowerLetterMessage,
14132
+ minUpperLetterMessage,
14133
+ minSpecialCharMessage,
14134
+ availableMessage,
14135
+ ...remainingProps
14136
+ } = props;
14137
+
13877
14138
  const customValidationRef = useCustomValidationRef(
13878
14139
  elementRef,
13879
14140
  targetSelector,
@@ -13891,6 +14152,96 @@ const useConstraints = (elementRef, constraints, targetSelector) => {
13891
14152
  }
13892
14153
  };
13893
14154
  }, constraints);
14155
+
14156
+ useLayoutEffect(() => {
14157
+ const el = elementRef.current;
14158
+ if (!el) {
14159
+ return null;
14160
+ }
14161
+ const cleanupCallbackSet = new Set();
14162
+ const setupCustomEvent = (el, constraintName, Component) => {
14163
+ const attrName = `data-${constraintName}-message-event`;
14164
+ const customEventName = `${constraintName}_message_jsx`;
14165
+ el.setAttribute(attrName, customEventName);
14166
+ const onCustomEvent = (e) => {
14167
+ e.detail.render(Component);
14168
+ };
14169
+ el.addEventListener(customEventName, onCustomEvent);
14170
+ cleanupCallbackSet.add(() => {
14171
+ el.removeEventListener(customEventName, onCustomEvent);
14172
+ el.removeAttribute(attrName);
14173
+ });
14174
+ };
14175
+
14176
+ if (disabledMessage) {
14177
+ setupCustomEvent(el, "disabled", disabledMessage);
14178
+ }
14179
+ if (requiredMessage) {
14180
+ setupCustomEvent(el, "required", requiredMessage);
14181
+ }
14182
+ if (patternMessage) {
14183
+ setupCustomEvent(el, "pattern", patternMessage);
14184
+ }
14185
+ if (minLengthMessage) {
14186
+ setupCustomEvent(el, "min-length", minLengthMessage);
14187
+ }
14188
+ if (maxLengthMessage) {
14189
+ setupCustomEvent(el, "max-length", maxLengthMessage);
14190
+ }
14191
+ if (typeMessage) {
14192
+ setupCustomEvent(el, "type", typeMessage);
14193
+ }
14194
+ if (minMessage) {
14195
+ setupCustomEvent(el, "min", minMessage);
14196
+ }
14197
+ if (maxMessage) {
14198
+ setupCustomEvent(el, "max", maxMessage);
14199
+ }
14200
+ if (singleSpaceMessage) {
14201
+ setupCustomEvent(el, "single-space", singleSpaceMessage);
14202
+ }
14203
+ if (sameAsMessage) {
14204
+ setupCustomEvent(el, "same-as", sameAsMessage);
14205
+ }
14206
+ if (minDigitMessage) {
14207
+ setupCustomEvent(el, "min-digit", minDigitMessage);
14208
+ }
14209
+ if (minLowerLetterMessage) {
14210
+ setupCustomEvent(el, "min-lower-letter", minLowerLetterMessage);
14211
+ }
14212
+ if (minUpperLetterMessage) {
14213
+ setupCustomEvent(el, "min-upper-letter", minUpperLetterMessage);
14214
+ }
14215
+ if (minSpecialCharMessage) {
14216
+ setupCustomEvent(el, "min-special-char", minSpecialCharMessage);
14217
+ }
14218
+ if (availableMessage) {
14219
+ setupCustomEvent(el, "available", availableMessage);
14220
+ }
14221
+ return () => {
14222
+ for (const cleanupCallback of cleanupCallbackSet) {
14223
+ cleanupCallback();
14224
+ }
14225
+ };
14226
+ }, [
14227
+ disabledMessage,
14228
+ requiredMessage,
14229
+ patternMessage,
14230
+ minLengthMessage,
14231
+ maxLengthMessage,
14232
+ typeMessage,
14233
+ minMessage,
14234
+ maxMessage,
14235
+ singleSpaceMessage,
14236
+ sameAsMessage,
14237
+ minDigitMessage,
14238
+ minLowerLetterMessage,
14239
+ minUpperLetterMessage,
14240
+ minSpecialCharMessage,
14241
+ availableMessage,
14242
+ ]);
14243
+
14244
+ return remainingProps;
13894
14245
  };
13895
14246
 
13896
14247
  const useInitialTextSelection = (ref, textSelection) => {
@@ -14309,7 +14660,9 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14309
14660
  .navi_icon {
14310
14661
  display: inline-block;
14311
14662
  box-sizing: border-box;
14663
+ width: 1em;
14312
14664
  max-width: 100%;
14665
+ height: 1em;
14313
14666
  max-height: 100%;
14314
14667
  }
14315
14668
 
@@ -14348,6 +14701,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14348
14701
  .navi_icon > img {
14349
14702
  width: 100%;
14350
14703
  height: 100%;
14704
+ backface-visibility: hidden;
14351
14705
  }
14352
14706
  .navi_icon[data-width] > svg,
14353
14707
  .navi_icon[data-width] > img {
@@ -14359,6 +14713,11 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14359
14713
  width: auto;
14360
14714
  height: 100%;
14361
14715
  }
14716
+ .navi_icon[data-width][data-height] > svg,
14717
+ .navi_icon[data-width][data-height] > img {
14718
+ width: 100%;
14719
+ height: 100%;
14720
+ }
14362
14721
 
14363
14722
  .navi_icon[data-icon-char] svg,
14364
14723
  .navi_icon[data-icon-char] img {
@@ -14613,6 +14972,8 @@ const useExecuteAction = (
14613
14972
  message = errorMappingResult;
14614
14973
  } else if (Error.isError(errorMappingResult)) {
14615
14974
  message = errorMappingResult;
14975
+ } else if (isValidElement(errorMappingResult)) {
14976
+ message = errorMappingResult;
14616
14977
  } else if (
14617
14978
  typeof errorMappingResult === "object" &&
14618
14979
  errorMappingResult !== null
@@ -15928,7 +16289,6 @@ const ButtonBasic = props => {
15928
16289
  readOnly,
15929
16290
  disabled,
15930
16291
  loading,
15931
- constraints = [],
15932
16292
  autoFocus,
15933
16293
  // visual
15934
16294
  discrete,
@@ -15939,7 +16299,7 @@ const ButtonBasic = props => {
15939
16299
  const defaultRef = useRef();
15940
16300
  const ref = props.ref || defaultRef;
15941
16301
  useAutoFocus(ref, autoFocus);
15942
- useConstraints(ref, constraints);
16302
+ const remainingProps = useConstraints(ref, rest);
15943
16303
  const innerLoading = loading || contextLoading && contextLoadingElement === ref.current;
15944
16304
  const innerReadOnly = readOnly || contextReadOnly || innerLoading;
15945
16305
  const innerDisabled = disabled || contextDisabled;
@@ -15955,7 +16315,7 @@ const ButtonBasic = props => {
15955
16315
  const renderButtonContentMemoized = useCallback(renderButtonContent, [children]);
15956
16316
  return jsxs(Box, {
15957
16317
  "data-readonly-silent": innerLoading ? "" : undefined,
15958
- ...rest,
16318
+ ...remainingProps,
15959
16319
  as: "button",
15960
16320
  ref: ref,
15961
16321
  "data-icon": icon ? "" : undefined,
@@ -16495,7 +16855,6 @@ const LinkPlain = props => {
16495
16855
  disabled,
16496
16856
  autoFocus,
16497
16857
  spaceToClick = true,
16498
- constraints = [],
16499
16858
  onClick,
16500
16859
  onKeyDown,
16501
16860
  href,
@@ -16510,6 +16869,7 @@ const LinkPlain = props => {
16510
16869
  icon,
16511
16870
  spacing,
16512
16871
  revealOnInteraction = Boolean(titleLevel),
16872
+ hrefFallback = !anchor,
16513
16873
  children,
16514
16874
  ...rest
16515
16875
  } = props;
@@ -16517,7 +16877,7 @@ const LinkPlain = props => {
16517
16877
  const ref = props.ref || defaultRef;
16518
16878
  const visited = useIsVisited(href);
16519
16879
  useAutoFocus(ref, autoFocus);
16520
- useConstraints(ref, constraints);
16880
+ const remainingProps = useConstraints(ref, rest);
16521
16881
  const shouldDimColor = readOnly || disabled;
16522
16882
  useDimColorWhen(ref, shouldDimColor);
16523
16883
  // subscribe to document url to re-render and re-compute getHrefTargetInfo
@@ -16553,12 +16913,12 @@ const LinkPlain = props => {
16553
16913
  } else {
16554
16914
  innerIcon = icon;
16555
16915
  }
16556
- const innerChildren = children || href;
16916
+ const innerChildren = children || (hrefFallback ? href : children);
16557
16917
  return jsxs(Box, {
16558
16918
  as: "a",
16559
16919
  color: anchor && !innerChildren ? "inherit" : undefined,
16560
16920
  id: anchor ? href.slice(1) : undefined,
16561
- ...rest,
16921
+ ...remainingProps,
16562
16922
  ref: ref,
16563
16923
  href: href,
16564
16924
  rel: innerRel,
@@ -17161,7 +17521,7 @@ const TabBasic = ({
17161
17521
  });
17162
17522
  };
17163
17523
 
17164
- const createUniqueValueConstraint = (
17524
+ const createAvailableConstraint = (
17165
17525
  // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
17166
17526
  // but this is already nice to help user with what we know
17167
17527
  // it's also possible that front is unsync with backend, preventing user to choose a value
@@ -17172,7 +17532,8 @@ const createUniqueValueConstraint = (
17172
17532
  message = `"{value}" est utilisé. Veuillez entrer une autre valeur.`,
17173
17533
  ) => {
17174
17534
  return {
17175
- name: "unique",
17535
+ name: "available",
17536
+ messageAttribute: "data-available-message",
17176
17537
  check: (field) => {
17177
17538
  const fieldValue = field.value;
17178
17539
  const hasConflict = existingValueSet.has(fieldValue);
@@ -17495,7 +17856,6 @@ const InputCheckboxBasic = props => {
17495
17856
  required,
17496
17857
  loading,
17497
17858
  autoFocus,
17498
- constraints = [],
17499
17859
  onClick,
17500
17860
  onInput,
17501
17861
  color,
@@ -17511,7 +17871,7 @@ const InputCheckboxBasic = props => {
17511
17871
  reportReadOnlyOnLabel?.(innerReadOnly);
17512
17872
  reportDisabledOnLabel?.(innerDisabled);
17513
17873
  useAutoFocus(ref, autoFocus);
17514
- useConstraints(ref, constraints);
17874
+ const remainingProps = useConstraints(ref, rest);
17515
17875
  const checked = Boolean(uiState);
17516
17876
  const innerOnClick = useStableCallback(e => {
17517
17877
  if (innerReadOnly) {
@@ -17558,7 +17918,7 @@ const InputCheckboxBasic = props => {
17558
17918
  }, [color]);
17559
17919
  return jsxs(Box, {
17560
17920
  as: "span",
17561
- ...rest,
17921
+ ...remainingProps,
17562
17922
  ref: undefined,
17563
17923
  baseClassName: "navi_checkbox",
17564
17924
  pseudoStateSelector: ".navi_native_field",
@@ -18053,7 +18413,6 @@ const InputRadioBasic = props => {
18053
18413
  required,
18054
18414
  loading,
18055
18415
  autoFocus,
18056
- constraints = [],
18057
18416
  onClick,
18058
18417
  onInput,
18059
18418
  color,
@@ -18069,7 +18428,7 @@ const InputRadioBasic = props => {
18069
18428
  reportReadOnlyOnLabel?.(innerReadOnly);
18070
18429
  reportDisabledOnLabel?.(innerDisabled);
18071
18430
  useAutoFocus(ref, autoFocus);
18072
- useConstraints(ref, constraints);
18431
+ const remainingProps = useConstraints(ref, rest);
18073
18432
  const checked = Boolean(uiState);
18074
18433
  // we must first dispatch an event to inform all other radios they where unchecked
18075
18434
  // this way each other radio uiStateController knows thery are unchecked
@@ -18143,7 +18502,7 @@ const InputRadioBasic = props => {
18143
18502
  }, [color]);
18144
18503
  return jsxs(Box, {
18145
18504
  as: "span",
18146
- ...rest,
18505
+ ...remainingProps,
18147
18506
  ref: undefined,
18148
18507
  baseClassName: "navi_radio",
18149
18508
  pseudoStateSelector: ".navi_native_field",
@@ -18467,7 +18826,6 @@ const InputRangeBasic = props => {
18467
18826
  onInput,
18468
18827
  readOnly,
18469
18828
  disabled,
18470
- constraints = [],
18471
18829
  loading,
18472
18830
  autoFocus,
18473
18831
  autoFocusVisible,
@@ -18486,7 +18844,7 @@ const InputRangeBasic = props => {
18486
18844
  autoFocusVisible,
18487
18845
  autoSelect
18488
18846
  });
18489
- useConstraints(ref, constraints);
18847
+ const remainingProps = useConstraints(ref, rest);
18490
18848
  const innerOnInput = useStableCallback(onInput);
18491
18849
  const focusProxyId = `input_range_focus_proxy_${useId()}`;
18492
18850
  const inertButFocusable = innerReadOnly && !innerDisabled;
@@ -18576,7 +18934,7 @@ const InputRangeBasic = props => {
18576
18934
  pseudoClasses: InputPseudoClasses$1,
18577
18935
  pseudoElements: InputPseudoElements$1,
18578
18936
  hasChildFunction: true,
18579
- ...rest,
18937
+ ...remainingProps,
18580
18938
  ref: undefined,
18581
18939
  children: [jsx(LoaderBackground, {
18582
18940
  loading: innerLoading,
@@ -18852,7 +19210,6 @@ const InputTextualBasic = props => {
18852
19210
  onInput,
18853
19211
  readOnly,
18854
19212
  disabled,
18855
- constraints = [],
18856
19213
  loading,
18857
19214
  autoFocus,
18858
19215
  autoFocusVisible,
@@ -18871,7 +19228,7 @@ const InputTextualBasic = props => {
18871
19228
  autoFocusVisible,
18872
19229
  autoSelect
18873
19230
  });
18874
- useConstraints(ref, constraints);
19231
+ const remainingProps = useConstraints(ref, rest);
18875
19232
  const innerOnInput = useStableCallback(onInput);
18876
19233
  const renderInput = inputProps => {
18877
19234
  return jsx(Box, {
@@ -18920,7 +19277,7 @@ const InputTextualBasic = props => {
18920
19277
  pseudoClasses: InputPseudoClasses,
18921
19278
  pseudoElements: InputPseudoElements,
18922
19279
  hasChildFunction: true,
18923
- ...rest,
19280
+ ...remainingProps,
18924
19281
  ref: undefined,
18925
19282
  children: [jsx(LoaderBackground, {
18926
19283
  loading: innerLoading,
@@ -19369,7 +19726,7 @@ const FormBasic = props => {
19369
19726
  // instantiation validation to:
19370
19727
  // - receive "requestsubmit" custom event ensure submit is prevented
19371
19728
  // (and also execute action without validation if form.submit() is ever called)
19372
- useConstraints(ref, []);
19729
+ const remainingProps = useConstraints(ref, rest);
19373
19730
  const innerReadOnly = readOnly || loading;
19374
19731
  const formContextValue = useMemo(() => {
19375
19732
  return {
@@ -19377,7 +19734,7 @@ const FormBasic = props => {
19377
19734
  };
19378
19735
  }, [loading]);
19379
19736
  return jsx(Box, {
19380
- ...rest,
19737
+ ...remainingProps,
19381
19738
  as: "form",
19382
19739
  ref: ref,
19383
19740
  onReset: e => {
@@ -24277,5 +24634,5 @@ const UserSvg = () => jsx("svg", {
24277
24634
  })
24278
24635
  });
24279
24636
 
24280
- export { ActionRenderer, ActiveKeyboardShortcuts, BadgeCount, Box, Button, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, addCustomMessage, compareTwoJsValues, createAction, createRequestCanceller, createSelectionKeyboardShortcuts, createUniqueValueConstraint, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useActiveRouteInfo, useCellsAndColumns, useConstraintValidityState, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useUrlSearchParam, valueInLocalStorage };
24637
+ export { ActionRenderer, ActiveKeyboardShortcuts, BadgeCount, Box, Button, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, addCustomMessage, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useActiveRouteInfo, useCalloutClose, useCellsAndColumns, useConstraintValidityState, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useUrlSearchParam, valueInLocalStorage };
24281
24638
  //# sourceMappingURL=jsenv_navi.js.map