@jsenv/navi 0.14.16 → 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
@@ -5625,12 +5642,18 @@ const initPseudoStyles = (
5625
5642
  return teardown;
5626
5643
  };
5627
5644
 
5628
- const applyStyle = (element, style, pseudoState, pseudoNamedStyles) => {
5645
+ const applyStyle = (
5646
+ element,
5647
+ style,
5648
+ pseudoState,
5649
+ pseudoNamedStyles,
5650
+ preventInitialTransition,
5651
+ ) => {
5629
5652
  if (!element) {
5630
5653
  return;
5631
5654
  }
5632
5655
  const styleToApply = getStyleToApply(style, pseudoState, pseudoNamedStyles);
5633
- updateStyle(element, styleToApply);
5656
+ updateStyle(element, styleToApply, preventInitialTransition);
5634
5657
  };
5635
5658
 
5636
5659
  const PSEUDO_STATE_DEFAULT = {};
@@ -5684,11 +5707,10 @@ const getStyleToApply = (styles, pseudoState, pseudoNamedStyles) => {
5684
5707
  };
5685
5708
 
5686
5709
  const styleKeySetWeakMap = new WeakMap();
5687
- const elementTransitionStateWeakMap = new WeakMap();
5710
+ const elementTransitionWeakMap = new WeakMap();
5688
5711
  const elementRenderedWeakSet = new WeakSet();
5689
5712
  const NO_STYLE_KEY_SET = new Set();
5690
-
5691
- const updateStyle = (element, style) => {
5713
+ const updateStyle = (element, style, preventInitialTransition) => {
5692
5714
  const styleKeySet = style ? new Set(Object.keys(style)) : NO_STYLE_KEY_SET;
5693
5715
  const oldStyleKeySet = styleKeySetWeakMap.get(element) || NO_STYLE_KEY_SET;
5694
5716
  // TRANSITION ANTI-FLICKER STRATEGY:
@@ -5703,26 +5725,26 @@ const updateStyle = (element, style) => {
5703
5725
  let styleKeySetToApply = styleKeySet;
5704
5726
  if (!elementRenderedWeakSet.has(element)) {
5705
5727
  const hasTransition = styleKeySet.has("transition");
5706
- if (hasTransition) {
5707
- if (elementTransitionStateWeakMap.has(element)) {
5708
- elementTransitionStateWeakMap.set(element, style.transition);
5728
+ if (hasTransition || preventInitialTransition) {
5729
+ if (elementTransitionWeakMap.has(element)) {
5730
+ elementTransitionWeakMap.set(element, style?.transition);
5709
5731
  } else {
5710
5732
  element.style.transition = "none";
5711
- elementTransitionStateWeakMap.set(element, style.transition);
5733
+ elementTransitionWeakMap.set(element, style?.transition);
5712
5734
  }
5713
5735
  // Don't apply the transition property now - we've set it to "none" temporarily
5714
5736
  styleKeySetToApply = new Set(styleKeySet);
5715
5737
  styleKeySetToApply.delete("transition");
5716
5738
  }
5717
5739
  requestAnimationFrame(() => {
5718
- if (elementTransitionStateWeakMap.has(element)) {
5719
- const transitionToRestore = elementTransitionStateWeakMap.get(element);
5740
+ if (elementTransitionWeakMap.has(element)) {
5741
+ const transitionToRestore = elementTransitionWeakMap.get(element);
5720
5742
  if (transitionToRestore === undefined) {
5721
5743
  element.style.transition = "";
5722
5744
  } else {
5723
5745
  element.style.transition = transitionToRestore;
5724
5746
  }
5725
- elementTransitionStateWeakMap.delete(element);
5747
+ elementTransitionWeakMap.delete(element);
5726
5748
  }
5727
5749
  elementRenderedWeakSet.add(element);
5728
5750
  });
@@ -5798,6 +5820,10 @@ const Box = props => {
5798
5820
  // -> introduced for <Input /> with a wrapped for loading, checkboxes, etc
5799
5821
  pseudoStateSelector,
5800
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,
5801
5827
  children,
5802
5828
  ...rest
5803
5829
  } = props;
@@ -5851,7 +5877,7 @@ const Box = props => {
5851
5877
  // Style context dependencies
5852
5878
  styleCSSVars, pseudoClasses, pseudoElements,
5853
5879
  // Selectors
5854
- visualSelector, pseudoStateSelector];
5880
+ visualSelector, pseudoStateSelector, preventInitialTransition];
5855
5881
  let innerPseudoState;
5856
5882
  if (basePseudoState && pseudoState) {
5857
5883
  innerPseudoState = {};
@@ -6059,7 +6085,7 @@ const Box = props => {
6059
6085
  }
6060
6086
  const updateStyle = useCallback(state => {
6061
6087
  const boxEl = ref.current;
6062
- applyStyle(boxEl, boxStyles, state, boxPseudoNamedStyles);
6088
+ applyStyle(boxEl, boxStyles, state, boxPseudoNamedStyles, preventInitialTransition);
6063
6089
  }, styleDeps);
6064
6090
  const finalStyleDeps = [pseudoStateSelector, innerPseudoState, updateStyle];
6065
6091
  // By default ":hover", ":active" are not tracked.
@@ -11204,6 +11230,20 @@ const useAutoFocus = (
11204
11230
  }, []);
11205
11231
  };
11206
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
+
11207
11247
  installImportMetaCss(import.meta);
11208
11248
  /**
11209
11249
  * A callout component that mimics native browser validation messages.
@@ -11310,6 +11350,10 @@ import.meta.css = /* css */ `
11310
11350
  }
11311
11351
 
11312
11352
  .navi_callout_message {
11353
+ position: relative;
11354
+ display: inline-flex;
11355
+ box-sizing: border-box;
11356
+ box-decoration-break: clone;
11313
11357
  min-width: 0;
11314
11358
  align-self: center;
11315
11359
  word-break: break-word;
@@ -11452,28 +11496,44 @@ const openCallout = (
11452
11496
  closeOnClickOutside = options.closeOnClickOutside;
11453
11497
  }
11454
11498
 
11455
- if (Error.isError(newMessage)) {
11456
- const error = newMessage;
11457
- newMessage = error.message;
11458
- if (showErrorStack && error.stack) {
11459
- 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
+ }
11460
11520
  }
11461
- }
11462
11521
 
11463
- // Check if the message is a full HTML document (starts with DOCTYPE)
11464
- if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
11465
- // Create iframe to isolate the HTML document
11466
- const iframe = document.createElement("iframe");
11467
- iframe.style.border = "none";
11468
- iframe.style.width = "100%";
11469
- iframe.style.backgroundColor = "white";
11470
- 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;
11471
11530
 
11472
- // Clear existing content and add iframe
11473
- calloutMessageElement.innerHTML = "";
11474
- calloutMessageElement.appendChild(iframe);
11475
- } else {
11476
- calloutMessageElement.innerHTML = newMessage;
11531
+ // Clear existing content and add iframe
11532
+ calloutMessageElement.innerHTML = "";
11533
+ calloutMessageElement.appendChild(iframe);
11534
+ } else {
11535
+ calloutMessageElement.innerHTML = newMessage;
11536
+ }
11477
11537
  }
11478
11538
  };
11479
11539
  {
@@ -11502,6 +11562,15 @@ const openCallout = (
11502
11562
  document.removeEventListener("click", handleClickOutside, true);
11503
11563
  });
11504
11564
  }
11565
+ {
11566
+ const handleCustomCloseEvent = () => {
11567
+ close("custom_event");
11568
+ };
11569
+ calloutElement.addEventListener(
11570
+ "navi_callout_close",
11571
+ handleCustomCloseEvent,
11572
+ );
11573
+ }
11505
11574
  Object.assign(callout, {
11506
11575
  element: calloutElement,
11507
11576
  update,
@@ -12234,6 +12303,189 @@ const generateSvgWithoutArrow = (width, height) => {
12234
12303
  </svg>`;
12235
12304
  };
12236
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
+
12237
12489
  const generateFieldInvalidMessage = (template, { field }) => {
12238
12490
  return replaceStringVars(template, {
12239
12491
  "{field}": () => generateThisFieldText(field),
@@ -12271,6 +12523,7 @@ const replaceStringVars = (string, replacers) => {
12271
12523
 
12272
12524
  const MIN_LOWER_LETTER_CONSTRAINT = {
12273
12525
  name: "min_lower_letter",
12526
+ messageAttribute: "data-min-lower-letter-message",
12274
12527
  check: (field) => {
12275
12528
  const fieldValue = field.value;
12276
12529
  if (!fieldValue && !field.required) {
@@ -12288,12 +12541,6 @@ const MIN_LOWER_LETTER_CONSTRAINT = {
12288
12541
  }
12289
12542
  }
12290
12543
  if (numberOfLowercaseChars < min) {
12291
- const messageAttribute = field.getAttribute(
12292
- "data-min-lower-letter-message",
12293
- );
12294
- if (messageAttribute) {
12295
- return messageAttribute;
12296
- }
12297
12544
  if (min === 0) {
12298
12545
  return generateFieldInvalidMessage(
12299
12546
  `{field} doit contenir au moins une lettre minuscule.`,
@@ -12310,6 +12557,7 @@ const MIN_LOWER_LETTER_CONSTRAINT = {
12310
12557
  };
12311
12558
  const MIN_UPPER_LETTER_CONSTRAINT = {
12312
12559
  name: "min_upper_letter",
12560
+ messageAttribute: "data-min-upper-letter-message",
12313
12561
  check: (field) => {
12314
12562
  const fieldValue = field.value;
12315
12563
  if (!fieldValue && !field.required) {
@@ -12327,12 +12575,6 @@ const MIN_UPPER_LETTER_CONSTRAINT = {
12327
12575
  }
12328
12576
  }
12329
12577
  if (numberOfUppercaseChars < min) {
12330
- const messageAttribute = field.getAttribute(
12331
- "data-min-upper-letter-message",
12332
- );
12333
- if (messageAttribute) {
12334
- return messageAttribute;
12335
- }
12336
12578
  if (min === 0) {
12337
12579
  return generateFieldInvalidMessage(
12338
12580
  `{field} doit contenir au moins une lettre majuscule.`,
@@ -12349,6 +12591,7 @@ const MIN_UPPER_LETTER_CONSTRAINT = {
12349
12591
  };
12350
12592
  const MIN_DIGIT_CONSTRAINT = {
12351
12593
  name: "min_digit",
12594
+ messageAttribute: "data-min-digit-message",
12352
12595
  check: (field) => {
12353
12596
  const fieldValue = field.value;
12354
12597
  if (!fieldValue && !field.required) {
@@ -12366,10 +12609,6 @@ const MIN_DIGIT_CONSTRAINT = {
12366
12609
  }
12367
12610
  }
12368
12611
  if (numberOfDigitChars < min) {
12369
- const messageAttribute = field.getAttribute("data-min-digit-message");
12370
- if (messageAttribute) {
12371
- return messageAttribute;
12372
- }
12373
12612
  if (min === 0) {
12374
12613
  return generateFieldInvalidMessage(
12375
12614
  `{field} doit contenir au moins un chiffre.`,
@@ -12384,44 +12623,39 @@ const MIN_DIGIT_CONSTRAINT = {
12384
12623
  return "";
12385
12624
  },
12386
12625
  };
12387
- const MIN_SPECIAL_CHARS_CONSTRAINT = {
12388
- name: "min_special_chars",
12626
+ const MIN_SPECIAL_CHAR_CONSTRAINT = {
12627
+ name: "min_special_char",
12628
+ messageAttribute: "data-min-special-char-message",
12389
12629
  check: (field) => {
12390
12630
  const fieldValue = field.value;
12391
12631
  if (!fieldValue && !field.required) {
12392
12632
  return "";
12393
12633
  }
12394
- const minSpecialChars = field.getAttribute("data-min-special-chars");
12634
+ const minSpecialChars = field.getAttribute("data-min-special-char");
12395
12635
  if (!minSpecialChars) {
12396
12636
  return "";
12397
12637
  }
12398
12638
  const min = parseInt(minSpecialChars, 10);
12399
- const specialChars = field.getAttribute("data-special-chars");
12400
- if (!specialChars) {
12401
- 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.";
12402
12642
  }
12403
12643
 
12404
12644
  let numberOfSpecialChars = 0;
12405
12645
  for (const char of fieldValue) {
12406
- if (specialChars.includes(char)) {
12646
+ if (specialCharset.includes(char)) {
12407
12647
  numberOfSpecialChars++;
12408
12648
  }
12409
12649
  }
12410
12650
  if (numberOfSpecialChars < min) {
12411
- const messageAttribute = field.getAttribute(
12412
- "data-min-special-chars-message",
12413
- );
12414
- if (messageAttribute) {
12415
- return messageAttribute;
12416
- }
12417
12651
  if (min === 1) {
12418
12652
  return generateFieldInvalidMessage(
12419
- `{field} doit contenir au moins un caractère spécial. (${specialChars})`,
12653
+ `{field} doit contenir au moins un caractère spécial. (${specialCharset})`,
12420
12654
  { field },
12421
12655
  );
12422
12656
  }
12423
12657
  return generateFieldInvalidMessage(
12424
- `{field} doit contenir au moins ${min} caractères spéciaux (${specialChars})`,
12658
+ `{field} doit contenir au moins ${min} caractères spéciaux (${specialCharset})`,
12425
12659
  { field },
12426
12660
  );
12427
12661
  }
@@ -12431,6 +12665,7 @@ const MIN_SPECIAL_CHARS_CONSTRAINT = {
12431
12665
 
12432
12666
  const READONLY_CONSTRAINT = {
12433
12667
  name: "readonly",
12668
+ messageAttribute: "data-readonly-message",
12434
12669
  check: (field, { skipReadonly }) => {
12435
12670
  if (skipReadonly) {
12436
12671
  return null;
@@ -12444,25 +12679,21 @@ const READONLY_CONSTRAINT = {
12444
12679
  const isButton = field.tagName === "BUTTON";
12445
12680
  const isBusy = field.getAttribute("aria-busy") === "true";
12446
12681
  const readonlySilent = field.hasAttribute("data-readonly-silent");
12447
- const messageAttribute = field.getAttribute("data-readonly-message");
12448
12682
  if (readonlySilent) {
12449
12683
  return { silent: true };
12450
12684
  }
12451
12685
  if (isBusy) {
12452
12686
  return {
12453
12687
  target: field,
12454
- message:
12455
- messageAttribute || `Cette action est en cours. Veuillez patienter.`,
12688
+ message: `Cette action est en cours. Veuillez patienter.`,
12456
12689
  status: "info",
12457
12690
  };
12458
12691
  }
12459
12692
  return {
12460
12693
  target: field,
12461
- message:
12462
- messageAttribute ||
12463
- (isButton
12464
- ? `Cet action n'est pas disponible pour l'instant.`
12465
- : `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é.`,
12466
12697
  status: "info",
12467
12698
  };
12468
12699
  },
@@ -12470,6 +12701,7 @@ const READONLY_CONSTRAINT = {
12470
12701
 
12471
12702
  const SAME_AS_CONSTRAINT = {
12472
12703
  name: "same_as",
12704
+ messageAttribute: "data-same-as-message",
12473
12705
  check: (field) => {
12474
12706
  const sameAs = field.getAttribute("data-same-as");
12475
12707
  if (!sameAs) {
@@ -12494,10 +12726,6 @@ const SAME_AS_CONSTRAINT = {
12494
12726
  if (fieldValue === otherFieldValue) {
12495
12727
  return null;
12496
12728
  }
12497
- const messageAttribute = field.getAttribute("data-same-as-message");
12498
- if (messageAttribute) {
12499
- return messageAttribute;
12500
- }
12501
12729
  const type = field.type;
12502
12730
  if (type === "password") {
12503
12731
  return `Ce mot de passe doit être identique au précédent.`;
@@ -12511,6 +12739,7 @@ const SAME_AS_CONSTRAINT = {
12511
12739
 
12512
12740
  const SINGLE_SPACE_CONSTRAINT = {
12513
12741
  name: "single_space",
12742
+ messageAttribute: "data-single-space-message",
12514
12743
  check: (field) => {
12515
12744
  const singleSpace = field.hasAttribute("data-single-space");
12516
12745
  if (!singleSpace) {
@@ -12521,10 +12750,6 @@ const SINGLE_SPACE_CONSTRAINT = {
12521
12750
  const hasTrailingSpace = fieldValue.endsWith(" ");
12522
12751
  const hasDoubleSpace = fieldValue.includes(" ");
12523
12752
  if (hasLeadingSpace || hasDoubleSpace || hasTrailingSpace) {
12524
- const messageAttribute = field.getAttribute("data-single-space-message");
12525
- if (messageAttribute) {
12526
- return messageAttribute;
12527
- }
12528
12753
  if (hasLeadingSpace) {
12529
12754
  return generateFieldInvalidMessage(
12530
12755
  `{field} ne doit pas commencer par un espace.`,
@@ -12555,6 +12780,7 @@ const SINGLE_SPACE_CONSTRAINT = {
12555
12780
  // in our case it's just here in case some code is wrongly calling "requestAction" or "checkValidity" on a disabled element
12556
12781
  const DISABLED_CONSTRAINT = {
12557
12782
  name: "disabled",
12783
+ messageAttribute: "data-disabled-message",
12558
12784
  check: (field) => {
12559
12785
  if (field.disabled) {
12560
12786
  return generateFieldInvalidMessage(`{field} est désactivé.`, { field });
@@ -12565,17 +12791,14 @@ const DISABLED_CONSTRAINT = {
12565
12791
 
12566
12792
  const REQUIRED_CONSTRAINT = {
12567
12793
  name: "required",
12794
+ messageAttribute: "data-required-message",
12568
12795
  check: (field, { registerChange }) => {
12569
12796
  if (!field.required) {
12570
12797
  return null;
12571
12798
  }
12572
- const messageAttribute = field.getAttribute("data-required-message");
12573
12799
 
12574
12800
  if (field.type === "checkbox") {
12575
12801
  if (!field.checked) {
12576
- if (messageAttribute) {
12577
- return messageAttribute;
12578
- }
12579
12802
  return `Veuillez cocher cette case.`;
12580
12803
  }
12581
12804
  return null;
@@ -12586,19 +12809,12 @@ const REQUIRED_CONSTRAINT = {
12586
12809
  if (!name) {
12587
12810
  // If no name, check just this radio
12588
12811
  if (!field.checked) {
12589
- if (messageAttribute) {
12590
- return messageAttribute;
12591
- }
12592
12812
  return `Veuillez sélectionner une option.`;
12593
12813
  }
12594
12814
  return null;
12595
12815
  }
12596
12816
 
12597
12817
  const closestFieldset = field.closest("fieldset");
12598
- const fieldsetRequiredMessage = closestFieldset
12599
- ? closestFieldset.getAttribute("data-required-message")
12600
- : null;
12601
-
12602
12818
  // Find the container (form or closest fieldset)
12603
12819
  const container = field.form || closestFieldset || document;
12604
12820
  // Check if any radio with the same name is checked
@@ -12617,10 +12833,7 @@ const REQUIRED_CONSTRAINT = {
12617
12833
  }
12618
12834
 
12619
12835
  return {
12620
- message:
12621
- messageAttribute ||
12622
- fieldsetRequiredMessage ||
12623
- `Veuillez sélectionner une option.`,
12836
+ message: `Veuillez sélectionner une option.`,
12624
12837
  target: closestFieldset
12625
12838
  ? closestFieldset.querySelector("legend")
12626
12839
  : undefined,
@@ -12629,9 +12842,6 @@ const REQUIRED_CONSTRAINT = {
12629
12842
  if (field.value) {
12630
12843
  return null;
12631
12844
  }
12632
- if (messageAttribute) {
12633
- return messageAttribute;
12634
- }
12635
12845
  if (field.type === "password") {
12636
12846
  return field.hasAttribute("data-same-as")
12637
12847
  ? `Veuillez confirmer le mot de passe.`
@@ -12650,6 +12860,7 @@ const REQUIRED_CONSTRAINT = {
12650
12860
 
12651
12861
  const PATTERN_CONSTRAINT = {
12652
12862
  name: "pattern",
12863
+ messageAttribute: "data-pattern-message",
12653
12864
  check: (field) => {
12654
12865
  const pattern = field.pattern;
12655
12866
  if (!pattern) {
@@ -12663,10 +12874,6 @@ const PATTERN_CONSTRAINT = {
12663
12874
  if (regex.test(value)) {
12664
12875
  return null;
12665
12876
  }
12666
- const messageAttribute = field.getAttribute("data-pattern-message");
12667
- if (messageAttribute) {
12668
- return messageAttribute;
12669
- }
12670
12877
  let message = generateFieldInvalidMessage(
12671
12878
  `{field} ne correspond pas au format requis.`,
12672
12879
  { field },
@@ -12683,6 +12890,7 @@ const emailregex =
12683
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])?)*$/;
12684
12891
  const TYPE_EMAIL_CONSTRAINT = {
12685
12892
  name: "type_email",
12893
+ messageAttribute: "data-type-message",
12686
12894
  check: (field) => {
12687
12895
  if (field.type !== "email") {
12688
12896
  return null;
@@ -12691,17 +12899,10 @@ const TYPE_EMAIL_CONSTRAINT = {
12691
12899
  if (!value && !field.required) {
12692
12900
  return null;
12693
12901
  }
12694
- const messageAttribute = field.getAttribute("data-type-email-message");
12695
12902
  if (!value.includes("@")) {
12696
- if (messageAttribute) {
12697
- return messageAttribute;
12698
- }
12699
12903
  return `Veuillez inclure "@" dans l'adresse e-mail. Il manque un symbole "@" dans ${value}.`;
12700
12904
  }
12701
12905
  if (!emailregex.test(value)) {
12702
- if (messageAttribute) {
12703
- return messageAttribute;
12704
- }
12705
12906
  return `Veuillez saisir une adresse e-mail valide.`;
12706
12907
  }
12707
12908
  return null;
@@ -12710,6 +12911,7 @@ const TYPE_EMAIL_CONSTRAINT = {
12710
12911
 
12711
12912
  const MIN_LENGTH_CONSTRAINT = {
12712
12913
  name: "min_length",
12914
+ messageAttribute: "data-min-length-message",
12713
12915
  check: (field) => {
12714
12916
  if (field.tagName === "INPUT") {
12715
12917
  if (!INPUT_TYPE_SUPPORTING_MIN_LENGTH_SET.has(field.type)) {
@@ -12731,10 +12933,6 @@ const MIN_LENGTH_CONSTRAINT = {
12731
12933
  if (valueLength >= minLength) {
12732
12934
  return null;
12733
12935
  }
12734
- const messageAttribute = field.getAttribute("data-min-length-message");
12735
- if (messageAttribute) {
12736
- return messageAttribute;
12737
- }
12738
12936
  if (valueLength === 1) {
12739
12937
  return generateFieldInvalidMessage(
12740
12938
  `{field} doit contenir au moins ${minLength} caractère (il contient actuellement un seul caractère).`,
@@ -12758,6 +12956,7 @@ const INPUT_TYPE_SUPPORTING_MIN_LENGTH_SET = new Set([
12758
12956
 
12759
12957
  const MAX_LENGTH_CONSTRAINT = {
12760
12958
  name: "max_length",
12959
+ messageAttribute: "data-max-length-message",
12761
12960
  check: (field) => {
12762
12961
  if (field.tagName === "INPUT") {
12763
12962
  if (!INPUT_TYPE_SUPPORTING_MAX_LENGTH_SET.has(field.type)) {
@@ -12775,10 +12974,6 @@ const MAX_LENGTH_CONSTRAINT = {
12775
12974
  if (valueLength <= maxLength) {
12776
12975
  return null;
12777
12976
  }
12778
- const messageAttribute = field.getAttribute("data-max-length-message");
12779
- if (messageAttribute) {
12780
- return messageAttribute;
12781
- }
12782
12977
  return generateFieldInvalidMessage(
12783
12978
  `{field} doit contenir au maximum ${maxLength} caractères (il contient actuellement ${valueLength} caractères).`,
12784
12979
  { field },
@@ -12791,6 +12986,7 @@ const INPUT_TYPE_SUPPORTING_MAX_LENGTH_SET = new Set(
12791
12986
 
12792
12987
  const TYPE_NUMBER_CONSTRAINT = {
12793
12988
  name: "type_number",
12989
+ messageAttribute: "data-type-message",
12794
12990
  check: (field) => {
12795
12991
  if (field.tagName !== "INPUT") {
12796
12992
  return null;
@@ -12803,10 +12999,6 @@ const TYPE_NUMBER_CONSTRAINT = {
12803
12999
  }
12804
13000
  const value = field.valueAsNumber;
12805
13001
  if (isNaN(value)) {
12806
- const messageAttribute = field.getAttribute("data-type-number-message");
12807
- if (messageAttribute) {
12808
- return messageAttribute;
12809
- }
12810
13002
  return generateFieldInvalidMessage(`{field} doit être un nombre.`, {
12811
13003
  field,
12812
13004
  });
@@ -12817,6 +13009,7 @@ const TYPE_NUMBER_CONSTRAINT = {
12817
13009
 
12818
13010
  const MIN_CONSTRAINT = {
12819
13011
  name: "min",
13012
+ messageAttribute: "data-min-message",
12820
13013
  check: (field) => {
12821
13014
  if (field.tagName !== "INPUT") {
12822
13015
  return null;
@@ -12835,10 +13028,6 @@ const MIN_CONSTRAINT = {
12835
13028
  return null;
12836
13029
  }
12837
13030
  if (valueAsNumber < minNumber) {
12838
- const messageAttribute = field.getAttribute("data-min-message");
12839
- if (messageAttribute) {
12840
- return messageAttribute;
12841
- }
12842
13031
  return generateFieldInvalidMessage(
12843
13032
  `{field} doit être supérieur ou égal à <strong>${minString}</strong>.`,
12844
13033
  { field },
@@ -12854,11 +13043,7 @@ const MIN_CONSTRAINT = {
12854
13043
  const [minHours, minMinutes] = min.split(":").map(Number);
12855
13044
  const value = field.value;
12856
13045
  const [hours, minutes] = value.split(":").map(Number);
12857
- const messageAttribute = field.getAttribute("data-min-message");
12858
13046
  if (hours < minHours || (hours === minHours && minMinutes < minutes)) {
12859
- if (messageAttribute) {
12860
- return messageAttribute;
12861
- }
12862
13047
  return generateFieldInvalidMessage(
12863
13048
  `{field} doit être <strong>${min}</strong> ou plus.`,
12864
13049
  { field },
@@ -12877,6 +13062,7 @@ const MIN_CONSTRAINT = {
12877
13062
 
12878
13063
  const MAX_CONSTRAINT = {
12879
13064
  name: "max",
13065
+ messageAttribute: "data-max-message",
12880
13066
  check: (field) => {
12881
13067
  if (field.tagName !== "INPUT") {
12882
13068
  return null;
@@ -12895,10 +13081,6 @@ const MAX_CONSTRAINT = {
12895
13081
  return null;
12896
13082
  }
12897
13083
  if (valueAsNumber > maxNumber) {
12898
- const messageAttribute = field.getAttribute("data-max-message");
12899
- if (messageAttribute) {
12900
- return messageAttribute;
12901
- }
12902
13084
  return generateFieldInvalidMessage(
12903
13085
  `{field} être <strong>${maxAttribute}</strong> ou plus.`,
12904
13086
  { field },
@@ -12915,10 +13097,6 @@ const MAX_CONSTRAINT = {
12915
13097
  const value = field.value;
12916
13098
  const [hours, minutes] = value.split(":").map(Number);
12917
13099
  if (hours > maxHours || (hours === maxHours && maxMinutes > minutes)) {
12918
- const messageAttribute = field.getAttribute("data-max-message");
12919
- if (messageAttribute) {
12920
- return messageAttribute;
12921
- }
12922
13100
  return generateFieldInvalidMessage(
12923
13101
  `{field} doit être <strong>${max}</strong> ou moins.`,
12924
13102
  { field },
@@ -13094,7 +13272,7 @@ const NAVI_CONSTRAINT_SET = new Set([
13094
13272
  // the order matters here, the last constraint is picked first when multiple constraints fail
13095
13273
  // so it's better to keep the most complex constraints at the beginning of the list
13096
13274
  // so the more basic ones shows up first
13097
- MIN_SPECIAL_CHARS_CONSTRAINT,
13275
+ MIN_SPECIAL_CHAR_CONSTRAINT,
13098
13276
  SINGLE_SPACE_CONSTRAINT,
13099
13277
  MIN_DIGIT_CONSTRAINT,
13100
13278
  MIN_UPPER_LETTER_CONSTRAINT,
@@ -13376,6 +13554,21 @@ const installCustomConstraintValidation = (
13376
13554
  typeof checkResult === "string"
13377
13555
  ? { message: checkResult }
13378
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
+ }
13379
13572
  const thisConstraintFailureInfo = {
13380
13573
  name: constraint.name,
13381
13574
  constraint,
@@ -13409,7 +13602,7 @@ const installCustomConstraintValidation = (
13409
13602
  if (!hasTitleAttribute) {
13410
13603
  // when a constraint is failing browser displays that constraint message if the element has no title attribute.
13411
13604
  // We want to do the same with our message (overriding the browser in the process to get better messages)
13412
- element.setAttribute("title", failedConstraintInfo.message);
13605
+ element.setAttribute("title", failedConstraintInfo.messageString);
13413
13606
  }
13414
13607
  } else {
13415
13608
  if (!hasTitleAttribute) {
@@ -13874,7 +14067,15 @@ const useCustomValidationRef = (elementRef, targetSelector) => {
13874
14067
  return null;
13875
14068
  }
13876
14069
  let target;
13877
- {
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 {
13878
14079
  target = element;
13879
14080
  }
13880
14081
  const unsubscribe = subscribe(element, target);
@@ -13912,7 +14113,28 @@ const unsubscribe = (element) => {
13912
14113
  }
13913
14114
  };
13914
14115
 
13915
- 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
+
13916
14138
  const customValidationRef = useCustomValidationRef(
13917
14139
  elementRef,
13918
14140
  targetSelector,
@@ -13930,6 +14152,96 @@ const useConstraints = (elementRef, constraints, targetSelector) => {
13930
14152
  }
13931
14153
  };
13932
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;
13933
14245
  };
13934
14246
 
13935
14247
  const useInitialTextSelection = (ref, textSelection) => {
@@ -14660,6 +14972,8 @@ const useExecuteAction = (
14660
14972
  message = errorMappingResult;
14661
14973
  } else if (Error.isError(errorMappingResult)) {
14662
14974
  message = errorMappingResult;
14975
+ } else if (isValidElement(errorMappingResult)) {
14976
+ message = errorMappingResult;
14663
14977
  } else if (
14664
14978
  typeof errorMappingResult === "object" &&
14665
14979
  errorMappingResult !== null
@@ -15975,7 +16289,6 @@ const ButtonBasic = props => {
15975
16289
  readOnly,
15976
16290
  disabled,
15977
16291
  loading,
15978
- constraints = [],
15979
16292
  autoFocus,
15980
16293
  // visual
15981
16294
  discrete,
@@ -15986,7 +16299,7 @@ const ButtonBasic = props => {
15986
16299
  const defaultRef = useRef();
15987
16300
  const ref = props.ref || defaultRef;
15988
16301
  useAutoFocus(ref, autoFocus);
15989
- useConstraints(ref, constraints);
16302
+ const remainingProps = useConstraints(ref, rest);
15990
16303
  const innerLoading = loading || contextLoading && contextLoadingElement === ref.current;
15991
16304
  const innerReadOnly = readOnly || contextReadOnly || innerLoading;
15992
16305
  const innerDisabled = disabled || contextDisabled;
@@ -16002,7 +16315,7 @@ const ButtonBasic = props => {
16002
16315
  const renderButtonContentMemoized = useCallback(renderButtonContent, [children]);
16003
16316
  return jsxs(Box, {
16004
16317
  "data-readonly-silent": innerLoading ? "" : undefined,
16005
- ...rest,
16318
+ ...remainingProps,
16006
16319
  as: "button",
16007
16320
  ref: ref,
16008
16321
  "data-icon": icon ? "" : undefined,
@@ -16542,7 +16855,6 @@ const LinkPlain = props => {
16542
16855
  disabled,
16543
16856
  autoFocus,
16544
16857
  spaceToClick = true,
16545
- constraints = [],
16546
16858
  onClick,
16547
16859
  onKeyDown,
16548
16860
  href,
@@ -16565,7 +16877,7 @@ const LinkPlain = props => {
16565
16877
  const ref = props.ref || defaultRef;
16566
16878
  const visited = useIsVisited(href);
16567
16879
  useAutoFocus(ref, autoFocus);
16568
- useConstraints(ref, constraints);
16880
+ const remainingProps = useConstraints(ref, rest);
16569
16881
  const shouldDimColor = readOnly || disabled;
16570
16882
  useDimColorWhen(ref, shouldDimColor);
16571
16883
  // subscribe to document url to re-render and re-compute getHrefTargetInfo
@@ -16606,7 +16918,7 @@ const LinkPlain = props => {
16606
16918
  as: "a",
16607
16919
  color: anchor && !innerChildren ? "inherit" : undefined,
16608
16920
  id: anchor ? href.slice(1) : undefined,
16609
- ...rest,
16921
+ ...remainingProps,
16610
16922
  ref: ref,
16611
16923
  href: href,
16612
16924
  rel: innerRel,
@@ -17209,7 +17521,7 @@ const TabBasic = ({
17209
17521
  });
17210
17522
  };
17211
17523
 
17212
- const createUniqueValueConstraint = (
17524
+ const createAvailableConstraint = (
17213
17525
  // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
17214
17526
  // but this is already nice to help user with what we know
17215
17527
  // it's also possible that front is unsync with backend, preventing user to choose a value
@@ -17220,7 +17532,8 @@ const createUniqueValueConstraint = (
17220
17532
  message = `"{value}" est utilisé. Veuillez entrer une autre valeur.`,
17221
17533
  ) => {
17222
17534
  return {
17223
- name: "unique",
17535
+ name: "available",
17536
+ messageAttribute: "data-available-message",
17224
17537
  check: (field) => {
17225
17538
  const fieldValue = field.value;
17226
17539
  const hasConflict = existingValueSet.has(fieldValue);
@@ -17543,7 +17856,6 @@ const InputCheckboxBasic = props => {
17543
17856
  required,
17544
17857
  loading,
17545
17858
  autoFocus,
17546
- constraints = [],
17547
17859
  onClick,
17548
17860
  onInput,
17549
17861
  color,
@@ -17559,7 +17871,7 @@ const InputCheckboxBasic = props => {
17559
17871
  reportReadOnlyOnLabel?.(innerReadOnly);
17560
17872
  reportDisabledOnLabel?.(innerDisabled);
17561
17873
  useAutoFocus(ref, autoFocus);
17562
- useConstraints(ref, constraints);
17874
+ const remainingProps = useConstraints(ref, rest);
17563
17875
  const checked = Boolean(uiState);
17564
17876
  const innerOnClick = useStableCallback(e => {
17565
17877
  if (innerReadOnly) {
@@ -17606,7 +17918,7 @@ const InputCheckboxBasic = props => {
17606
17918
  }, [color]);
17607
17919
  return jsxs(Box, {
17608
17920
  as: "span",
17609
- ...rest,
17921
+ ...remainingProps,
17610
17922
  ref: undefined,
17611
17923
  baseClassName: "navi_checkbox",
17612
17924
  pseudoStateSelector: ".navi_native_field",
@@ -18101,7 +18413,6 @@ const InputRadioBasic = props => {
18101
18413
  required,
18102
18414
  loading,
18103
18415
  autoFocus,
18104
- constraints = [],
18105
18416
  onClick,
18106
18417
  onInput,
18107
18418
  color,
@@ -18117,7 +18428,7 @@ const InputRadioBasic = props => {
18117
18428
  reportReadOnlyOnLabel?.(innerReadOnly);
18118
18429
  reportDisabledOnLabel?.(innerDisabled);
18119
18430
  useAutoFocus(ref, autoFocus);
18120
- useConstraints(ref, constraints);
18431
+ const remainingProps = useConstraints(ref, rest);
18121
18432
  const checked = Boolean(uiState);
18122
18433
  // we must first dispatch an event to inform all other radios they where unchecked
18123
18434
  // this way each other radio uiStateController knows thery are unchecked
@@ -18191,7 +18502,7 @@ const InputRadioBasic = props => {
18191
18502
  }, [color]);
18192
18503
  return jsxs(Box, {
18193
18504
  as: "span",
18194
- ...rest,
18505
+ ...remainingProps,
18195
18506
  ref: undefined,
18196
18507
  baseClassName: "navi_radio",
18197
18508
  pseudoStateSelector: ".navi_native_field",
@@ -18515,7 +18826,6 @@ const InputRangeBasic = props => {
18515
18826
  onInput,
18516
18827
  readOnly,
18517
18828
  disabled,
18518
- constraints = [],
18519
18829
  loading,
18520
18830
  autoFocus,
18521
18831
  autoFocusVisible,
@@ -18534,7 +18844,7 @@ const InputRangeBasic = props => {
18534
18844
  autoFocusVisible,
18535
18845
  autoSelect
18536
18846
  });
18537
- useConstraints(ref, constraints);
18847
+ const remainingProps = useConstraints(ref, rest);
18538
18848
  const innerOnInput = useStableCallback(onInput);
18539
18849
  const focusProxyId = `input_range_focus_proxy_${useId()}`;
18540
18850
  const inertButFocusable = innerReadOnly && !innerDisabled;
@@ -18624,7 +18934,7 @@ const InputRangeBasic = props => {
18624
18934
  pseudoClasses: InputPseudoClasses$1,
18625
18935
  pseudoElements: InputPseudoElements$1,
18626
18936
  hasChildFunction: true,
18627
- ...rest,
18937
+ ...remainingProps,
18628
18938
  ref: undefined,
18629
18939
  children: [jsx(LoaderBackground, {
18630
18940
  loading: innerLoading,
@@ -18900,7 +19210,6 @@ const InputTextualBasic = props => {
18900
19210
  onInput,
18901
19211
  readOnly,
18902
19212
  disabled,
18903
- constraints = [],
18904
19213
  loading,
18905
19214
  autoFocus,
18906
19215
  autoFocusVisible,
@@ -18919,7 +19228,7 @@ const InputTextualBasic = props => {
18919
19228
  autoFocusVisible,
18920
19229
  autoSelect
18921
19230
  });
18922
- useConstraints(ref, constraints);
19231
+ const remainingProps = useConstraints(ref, rest);
18923
19232
  const innerOnInput = useStableCallback(onInput);
18924
19233
  const renderInput = inputProps => {
18925
19234
  return jsx(Box, {
@@ -18968,7 +19277,7 @@ const InputTextualBasic = props => {
18968
19277
  pseudoClasses: InputPseudoClasses,
18969
19278
  pseudoElements: InputPseudoElements,
18970
19279
  hasChildFunction: true,
18971
- ...rest,
19280
+ ...remainingProps,
18972
19281
  ref: undefined,
18973
19282
  children: [jsx(LoaderBackground, {
18974
19283
  loading: innerLoading,
@@ -19417,7 +19726,7 @@ const FormBasic = props => {
19417
19726
  // instantiation validation to:
19418
19727
  // - receive "requestsubmit" custom event ensure submit is prevented
19419
19728
  // (and also execute action without validation if form.submit() is ever called)
19420
- useConstraints(ref, []);
19729
+ const remainingProps = useConstraints(ref, rest);
19421
19730
  const innerReadOnly = readOnly || loading;
19422
19731
  const formContextValue = useMemo(() => {
19423
19732
  return {
@@ -19425,7 +19734,7 @@ const FormBasic = props => {
19425
19734
  };
19426
19735
  }, [loading]);
19427
19736
  return jsx(Box, {
19428
- ...rest,
19737
+ ...remainingProps,
19429
19738
  as: "form",
19430
19739
  ref: ref,
19431
19740
  onReset: e => {
@@ -24325,5 +24634,5 @@ const UserSvg = () => jsx("svg", {
24325
24634
  })
24326
24635
  });
24327
24636
 
24328
- 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 };
24329
24638
  //# sourceMappingURL=jsenv_navi.js.map