@jsenv/navi 0.12.30 → 0.12.31

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.
@@ -1,8 +1,8 @@
1
1
  import { installImportMetaCss } from "./jsenv_navi_side_effects.js";
2
- import { createIterableWeakSet, createPubSub, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, activeElementSignal, canInterceptKeys, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, stringifyStyle, mergeOneStyle, mergeTwoStyles, normalizeStyles, resolveCSSSize, findBefore, findAfter, hasCSSSizeUnit, initFocusGroup, elementIsFocusable, pickLightOrDark, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
2
+ import { createIterableWeakSet, createPubSub, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, activeElementSignal, canInterceptKeys, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, stringifyStyle, mergeOneStyle, mergeTwoStyles, normalizeStyles, resolveCSSSize, findBefore, findAfter, hasCSSSizeUnit, pickLightOrDark, resolveColorLuminance, initFocusGroup, elementIsFocusable, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
3
3
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
4
4
  import { effect, signal, computed, batch, useSignal } from "@preact/signals";
5
- import { useEffect, useRef, useCallback, useContext, useState, useLayoutEffect, useMemo, useErrorBoundary, useImperativeHandle, useId } from "preact/hooks";
5
+ import { useEffect, useRef, useCallback, useContext, useState, useLayoutEffect, useMemo, useImperativeHandle, useErrorBoundary, useId } from "preact/hooks";
6
6
  import { createContext, toChildArray, createRef, cloneElement } from "preact";
7
7
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
8
8
  import { createPortal, forwardRef } from "preact/compat";
@@ -7690,7 +7690,9 @@ const createRoute = (urlPatternInput) => {
7690
7690
  // always remove the wildcard part for URL building since it's optional
7691
7691
  if (relativeUrl.endsWith("/*?")) {
7692
7692
  // Always remove the optional wildcard part for URL building
7693
- relativeUrl = relativeUrl.replace(/\/\*\?$/, "");
7693
+ relativeUrl = relativeUrl.slice(0, -"/*?".length);
7694
+ } else if (relativeUrl.endsWith("{/}?*")) {
7695
+ relativeUrl = relativeUrl.slice(0, -"{/}?*".length);
7694
7696
  } else {
7695
7697
  // For required wildcards (/*) or other patterns, replace normally
7696
7698
  let wildcardIndex = 0;
@@ -13851,7 +13853,7 @@ const TextBasic = ({
13851
13853
  as: "span",
13852
13854
  baseClassName: "navi_text",
13853
13855
  ...rest,
13854
- children: rest.as === "pre" ? children : applySpacingOnTextChildren(children, spacing)
13856
+ children: rest.as === "pre" || rest.box || rest.column || rest.row ? children : applySpacingOnTextChildren(children, spacing)
13855
13857
  });
13856
13858
  };
13857
13859
 
@@ -15623,559 +15625,557 @@ const RouteLink = ({
15623
15625
  };
15624
15626
 
15625
15627
  installImportMetaCss(import.meta);import.meta.css = /* css */`
15626
- .action_error {
15627
- padding: 20px;
15628
- background: #fdd;
15629
- border: 1px solid red;
15630
- margin-top: 0;
15631
- margin-bottom: 20px;
15632
- }
15633
- `;
15634
- const renderIdleDefault = () => null;
15635
- const renderLoadingDefault = () => null;
15636
- const renderAbortedDefault = () => null;
15637
- const renderErrorDefault = error => {
15638
- let routeErrorText = error && error.message ? error.message : error;
15639
- return jsxs("p", {
15640
- className: "action_error",
15641
- children: ["An error occured: ", routeErrorText]
15642
- });
15643
- };
15644
- const renderCompletedDefault = () => null;
15645
- const ActionRenderer = ({
15646
- action,
15647
- children,
15648
- disabled
15649
- }) => {
15650
- const {
15651
- idle: renderIdle = renderIdleDefault,
15652
- loading: renderLoading = renderLoadingDefault,
15653
- aborted: renderAborted = renderAbortedDefault,
15654
- error: renderError = renderErrorDefault,
15655
- completed: renderCompleted,
15656
- always: renderAlways
15657
- } = typeof children === "function" ? {
15658
- completed: children
15659
- } : children || {};
15660
- if (disabled) {
15661
- return null;
15628
+ .navi_tablist {
15629
+ display: flex;
15630
+ justify-content: space-between;
15631
+ overflow-x: auto;
15632
+ overflow-y: hidden;
15662
15633
  }
15663
- if (action === undefined) {
15664
- throw new Error("ActionRenderer requires an action to render, but none was provided.");
15634
+
15635
+ .navi_tablist > ul {
15636
+ display: flex;
15637
+ margin: 0;
15638
+ padding: 0;
15639
+ align-items: center;
15640
+ gap: 0.5rem;
15641
+ list-style: none;
15665
15642
  }
15666
- const {
15667
- idle,
15668
- loading,
15669
- aborted,
15670
- error,
15671
- data
15672
- } = useActionStatus(action);
15673
- const UIRenderedPromise = useUIRenderedPromise(action);
15674
- const [errorBoundary, resetErrorBoundary] = useErrorBoundary();
15675
15643
 
15676
- // Mark this action as bound to UI components (has renderers)
15677
- // This tells the action system that errors should be caught and stored
15678
- // in the action's error state rather than bubbling up
15679
- useLayoutEffect(() => {
15680
- if (action) {
15681
- const {
15682
- ui
15683
- } = getActionPrivateProperties(action);
15684
- ui.hasRenderers = true;
15685
- }
15686
- }, [action]);
15687
- useLayoutEffect(() => {
15688
- resetErrorBoundary();
15689
- }, [action, loading, idle, resetErrorBoundary]);
15690
- useLayoutEffect(() => {
15691
- UIRenderedPromise.resolve();
15692
- return () => {
15693
- actionUIRenderedPromiseWeakMap.delete(action);
15694
- };
15695
- }, [action]);
15644
+ .navi_tablist > ul > li {
15645
+ position: relative;
15646
+ display: inline-flex;
15647
+ }
15696
15648
 
15697
- // If renderAlways is provided, it wins and handles all rendering
15698
- if (renderAlways) {
15699
- return renderAlways({
15700
- idle,
15701
- loading,
15702
- aborted,
15703
- error,
15704
- data
15705
- });
15649
+ .navi_tab {
15650
+ display: flex;
15651
+ flex-direction: column;
15652
+ white-space: nowrap;
15706
15653
  }
15707
- if (idle) {
15708
- return renderIdle(action);
15654
+
15655
+ .navi_tab_content {
15656
+ display: flex;
15657
+ padding: 0 0.5rem;
15658
+ text-decoration: none;
15659
+ line-height: 30px;
15660
+ border-radius: 6px;
15661
+ transition: background 0.12s ease-out;
15709
15662
  }
15710
- if (errorBoundary) {
15711
- return renderError(errorBoundary, "ui_error", action);
15663
+
15664
+ .navi_tab:hover .navi_tab_content {
15665
+ color: #010409;
15666
+ background: #dae0e7;
15712
15667
  }
15713
- if (error) {
15714
- return renderError(error, "action_error", action);
15668
+
15669
+ .navi_tab .active_marker {
15670
+ z-index: 1;
15671
+ display: flex;
15672
+ width: 100%;
15673
+ height: 2px;
15674
+ margin-top: 5px;
15675
+ background: transparent;
15676
+ border-radius: 0.1px;
15715
15677
  }
15716
- if (aborted) {
15717
- return renderAborted(action);
15678
+
15679
+ /* Hidden bold clone to reserve space for bold width without affecting height */
15680
+ .navi_tab_content_bold_clone {
15681
+ display: block; /* in-flow so it contributes to width */
15682
+ height: 0; /* zero height so it doesn't change layout height */
15683
+ font-weight: 600; /* force bold to compute max width */
15684
+ visibility: hidden; /* not visible */
15685
+ pointer-events: none; /* inert */
15686
+ overflow: hidden; /* avoid any accidental height */
15718
15687
  }
15719
- let renderCompletedSafe;
15720
- if (renderCompleted) {
15721
- renderCompletedSafe = renderCompleted;
15722
- } else {
15723
- const {
15724
- ui
15725
- } = getActionPrivateProperties(action);
15726
- if (ui.renderCompleted) {
15727
- renderCompletedSafe = ui.renderCompleted;
15728
- } else {
15729
- renderCompletedSafe = renderCompletedDefault;
15730
- }
15688
+
15689
+ .navi_tab[aria-selected="true"] .active_marker {
15690
+ background: rgb(205, 52, 37);
15731
15691
  }
15732
- if (loading) {
15733
- if (action.canDisplayOldData && data !== undefined) {
15734
- return renderCompletedSafe(data, action);
15735
- }
15736
- return renderLoading(action);
15692
+
15693
+ .navi_tab[aria-selected="true"] .navi_tab_content {
15694
+ font-weight: 600;
15737
15695
  }
15738
- return renderCompletedSafe(data, action);
15696
+ `;
15697
+ const TabList = ({
15698
+ children,
15699
+ ...props
15700
+ }) => {
15701
+ return jsx(Box, {
15702
+ as: "nav",
15703
+ baseClassName: "navi_tablist",
15704
+ role: "tablist",
15705
+ ...props,
15706
+ children: jsx("ul", {
15707
+ role: "list",
15708
+ children: children.map(child => {
15709
+ return jsx("li", {
15710
+ children: child
15711
+ }, child.props.key);
15712
+ })
15713
+ })
15714
+ });
15739
15715
  };
15740
- const defaultPromise = Promise.resolve();
15741
- defaultPromise.resolve = () => {};
15742
- const actionUIRenderedPromiseWeakMap = new WeakMap();
15743
- const useUIRenderedPromise = action => {
15744
- if (!action) {
15745
- return defaultPromise;
15746
- }
15747
- const actionUIRenderedPromise = actionUIRenderedPromiseWeakMap.get(action);
15748
- if (actionUIRenderedPromise) {
15749
- return actionUIRenderedPromise;
15750
- }
15751
- let resolve;
15752
- const promise = new Promise(res => {
15753
- resolve = res;
15716
+ const Tab = ({
15717
+ children,
15718
+ selected,
15719
+ ...props
15720
+ }) => {
15721
+ return jsxs(Box, {
15722
+ baseClassName: "navi_tab",
15723
+ role: "tab",
15724
+ "aria-selected": selected ? "true" : "false",
15725
+ ...props,
15726
+ children: [jsx("div", {
15727
+ className: "navi_tab_content",
15728
+ children: children
15729
+ }), jsx("div", {
15730
+ className: "navi_tab_content_bold_clone",
15731
+ "aria-hidden": "true",
15732
+ children: children
15733
+ }), jsx("span", {
15734
+ className: "active_marker"
15735
+ })]
15754
15736
  });
15755
- promise.resolve = resolve;
15756
- actionUIRenderedPromiseWeakMap.set(action, promise);
15757
- return promise;
15758
15737
  };
15759
15738
 
15760
- const useFocusGroup = (
15761
- elementRef,
15762
- { enabled = true, direction, skipTab, loop, name } = {},
15763
- ) => {
15764
- useLayoutEffect(() => {
15765
- if (!enabled) {
15766
- return null;
15739
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
15740
+ @layer navi {
15741
+ label {
15742
+ cursor: pointer;
15767
15743
  }
15768
- const focusGroup = initFocusGroup(elementRef.current, {
15769
- direction,
15770
- skipTab,
15771
- loop,
15772
- name,
15773
- });
15774
- return focusGroup.cleanup;
15775
- }, [direction, skipTab, loop, name]);
15776
- };
15777
15744
 
15778
- installImportMetaCss(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
15779
- const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
15780
- import.meta.css = /* css */`
15781
- .summary_marker {
15782
- width: 1em;
15783
- height: 1em;
15784
- line-height: 1em;
15785
- }
15786
- .summary_marker_svg .arrow {
15787
- animation-duration: 0.3s;
15788
- animation-fill-mode: forwards;
15789
- animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
15790
- }
15791
- .summary_marker_svg .arrow[data-animation-target="down"] {
15792
- animation-name: morph-to-down;
15793
- }
15794
- @keyframes morph-to-down {
15795
- from {
15796
- d: path("${rightArrowPath}");
15797
- }
15798
- to {
15799
- d: path("${downArrowPath}");
15800
- }
15801
- }
15802
- .summary_marker_svg .arrow[data-animation-target="right"] {
15803
- animation-name: morph-to-right;
15804
- }
15805
- @keyframes morph-to-right {
15806
- from {
15807
- d: path("${downArrowPath}");
15808
- }
15809
- to {
15810
- d: path("${rightArrowPath}");
15745
+ label[data-readonly],
15746
+ label[data-disabled] {
15747
+ color: rgba(0, 0, 0, 0.5);
15748
+ cursor: default;
15811
15749
  }
15812
15750
  }
15751
+ `;
15752
+ const ReportReadOnlyOnLabelContext = createContext();
15753
+ const ReportDisabledOnLabelContext = createContext();
15754
+ const Label = props => {
15755
+ const {
15756
+ readOnly,
15757
+ disabled,
15758
+ children,
15759
+ ...rest
15760
+ } = props;
15761
+ const [inputReadOnly, setInputReadOnly] = useState(false);
15762
+ const innerReadOnly = readOnly || inputReadOnly;
15763
+ const [inputDisabled, setInputDisabled] = useState(false);
15764
+ const innerDisabled = disabled || inputDisabled;
15765
+ return jsx(Box, {
15766
+ ...rest,
15767
+ as: "label",
15768
+ basePseudoState: {
15769
+ readOnly: innerReadOnly,
15770
+ disabled: innerDisabled
15771
+ },
15772
+ children: jsx(ReportReadOnlyOnLabelContext.Provider, {
15773
+ value: setInputReadOnly,
15774
+ children: jsx(ReportDisabledOnLabelContext.Provider, {
15775
+ value: setInputDisabled,
15776
+ children: children
15777
+ })
15778
+ })
15779
+ });
15780
+ };
15813
15781
 
15814
- .summary_marker_svg .foreground_circle {
15815
- stroke-dasharray: 503 1507; /* ~25% of circle perimeter */
15816
- stroke-dashoffset: 0;
15817
- animation: progress-around-circle 1.5s linear infinite;
15818
- }
15819
- @keyframes progress-around-circle {
15820
- 0% {
15821
- stroke-dashoffset: 0;
15782
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
15783
+ @layer navi {
15784
+ .navi_checkbox {
15785
+ --outline-offset: 1px;
15786
+ --outline-width: 2px;
15787
+ --border-width: 1px;
15788
+ --border-radius: 2px;
15789
+ --width: 13px;
15790
+ --height: 13px;
15791
+
15792
+ --outline-color: var(--navi-focus-outline-color);
15793
+ --loader-color: var(--navi-loader-color);
15794
+ --border-color: light-dark(#767676, #8e8e93);
15795
+ --background-color: white;
15796
+ --color: light-dark(#4476ff, #3b82f6);
15797
+ --checkmark-color-light: white;
15798
+ --checkmark-color-dark: rgb(55, 55, 55);
15799
+ --checkmark-color: var(--checkmark-color-light);
15800
+
15801
+ --color-mix-light: black;
15802
+ --color-mix-dark: white;
15803
+ --color-mix: var(--color-mix-light);
15804
+
15805
+ /* Hover */
15806
+ --border-color-hover: color-mix(in srgb, var(--border-color) 60%, black);
15807
+ --border-color-hover-checked: color-mix(
15808
+ in srgb,
15809
+ var(--color) 80%,
15810
+ var(--color-mix)
15811
+ );
15812
+ --background-color-hover-checked: color-mix(
15813
+ in srgb,
15814
+ var(--color) 80%,
15815
+ var(--color-mix)
15816
+ );
15817
+ /* Readonly */
15818
+ --border-color-readonly: color-mix(
15819
+ in srgb,
15820
+ var(--border-color) 30%,
15821
+ white
15822
+ );
15823
+ --border-color-readonly-checked: #d3d3d3;
15824
+ --background-color-readonly-checked: grey;
15825
+ --checkmark-color-readonly: #eeeeee;
15826
+ /* Disabled */
15827
+ --border-color-disabled: var(--border-color-readonly);
15828
+ --background-color-disabled: rgba(248, 248, 248, 0.7);
15829
+ --checkmark-color-disabled: #eeeeee;
15830
+ --border-color-disabled-checked: #d3d3d3;
15831
+ --background-color-disabled-checked: #d3d3d3;
15822
15832
  }
15823
- 100% {
15824
- stroke-dashoffset: -2010;
15833
+
15834
+ .navi_checkbox[data-dark] {
15835
+ --color-mix: var(--color-mix-dark);
15836
+ --checkmark-color: var(--navi-checkmark-color-dark);
15825
15837
  }
15826
15838
  }
15827
15839
 
15828
- /* fading and scaling */
15829
- .summary_marker_svg .arrow {
15830
- transition: opacity 0.3s ease-in-out;
15831
- opacity: 1;
15832
- }
15833
- .summary_marker_svg .loading_container {
15834
- transition: transform 0.3s linear;
15835
- transform: scale(0.3);
15836
- }
15837
- .summary_marker_svg .background_circle,
15838
- .summary_marker_svg .foreground_circle {
15839
- transition: opacity 0.3s ease-in-out;
15840
- opacity: 0;
15840
+ .navi_checkbox {
15841
+ position: relative;
15842
+ display: inline-flex;
15843
+ box-sizing: content-box;
15844
+ margin: 3px 3px 3px 4px;
15845
+
15846
+ --x-border-radius: var(--border-radius);
15847
+ --x-outline-offset: var(--outline-offset);
15848
+ --x-outline-width: var(--outline-width);
15849
+ --x-border-width: var(--border-width);
15850
+ --x-width: var(--width);
15851
+ --x-height: var(--height);
15852
+ --x-outline-color: var(--outline-color);
15853
+ --x-background-color: var(--background-color);
15854
+ --x-border-color: var(--border-color);
15855
+ --x-color: var(--color);
15856
+ --x-checkmark-color: var(--checkmark-color);
15841
15857
  }
15842
- .summary_marker_svg[data-loading] .arrow {
15858
+ .navi_checkbox .navi_native_field {
15859
+ position: absolute;
15860
+ inset: 0;
15861
+ margin: 0;
15862
+ padding: 0;
15863
+ border: none;
15843
15864
  opacity: 0;
15865
+ cursor: inherit;
15844
15866
  }
15845
- .summary_marker_svg[data-loading] .loading_container {
15846
- transform: scale(1);
15867
+ .navi_checkbox .navi_checkbox_field {
15868
+ display: inline-flex;
15869
+ box-sizing: border-box;
15870
+ width: var(--x-width);
15871
+ height: var(--x-height);
15872
+ background-color: var(--x-background-color);
15873
+ border-width: var(--x-border-width);
15874
+ border-style: solid;
15875
+ border-color: var(--x-border-color);
15876
+ border-radius: var(--x-border-radius);
15877
+ outline-width: var(--x-outline-width);
15878
+ outline-style: none;
15879
+ outline-color: var(--x-outline-color);
15880
+ outline-offset: var(--x-outline-offset);
15847
15881
  }
15848
- .summary_marker_svg[data-loading] .background_circle {
15849
- opacity: 0.2;
15882
+ .navi_checkbox_marker {
15883
+ width: 100%;
15884
+ height: 100%;
15885
+ opacity: 0;
15886
+ stroke: var(--x-checkmark-color);
15887
+ transform: scale(0.5);
15888
+ transition: all 0.15s ease;
15889
+ pointer-events: none;
15850
15890
  }
15851
- .summary_marker_svg[data-loading] .foreground_circle {
15891
+ .navi_checkbox[data-checked] .navi_checkbox_marker {
15852
15892
  opacity: 1;
15893
+ transform: scale(1);
15853
15894
  }
15854
- `;
15855
- const SummaryMarker = ({
15856
- open,
15857
- loading
15858
- }) => {
15859
- const showLoading = useDebounceTrue(loading, 300);
15860
- const mountedRef = useRef(false);
15861
- const prevOpenRef = useRef(open);
15862
- useLayoutEffect(() => {
15863
- mountedRef.current = true;
15864
- return () => {
15865
- mountedRef.current = false;
15866
- };
15867
- }, []);
15868
- const shouldAnimate = mountedRef.current && prevOpenRef.current !== open;
15869
- prevOpenRef.current = open;
15870
- return jsx("span", {
15871
- className: "summary_marker",
15872
- children: jsxs("svg", {
15873
- className: "summary_marker_svg",
15874
- viewBox: "0 -960 960 960",
15875
- xmlns: "http://www.w3.org/2000/svg",
15876
- "data-loading": open ? showLoading || undefined : undefined,
15877
- children: [jsxs("g", {
15878
- className: "loading_container",
15879
- "transform-origin": "480px -480px",
15880
- children: [jsx("circle", {
15881
- className: "background_circle",
15882
- cx: "480",
15883
- cy: "-480",
15884
- r: "320",
15885
- stroke: "currentColor",
15886
- fill: "none",
15887
- strokeWidth: "60",
15888
- opacity: "0.2"
15889
- }), jsx("circle", {
15890
- className: "foreground_circle",
15891
- cx: "480",
15892
- cy: "-480",
15893
- r: "320",
15894
- stroke: "currentColor",
15895
- fill: "none",
15896
- strokeWidth: "60",
15897
- strokeLinecap: "round",
15898
- strokeDasharray: "503 1507"
15899
- })]
15900
- }), jsx("g", {
15901
- className: "arrow_container",
15902
- "transform-origin": "480px -480px",
15903
- children: jsx("path", {
15904
- className: "arrow",
15905
- fill: "currentColor",
15906
- "data-animation-target": shouldAnimate ? open ? "down" : "right" : undefined,
15907
- d: open ? downArrowPath : rightArrowPath
15908
- })
15909
- })]
15910
- })
15911
- });
15912
- };
15913
15895
 
15914
- installImportMetaCss(import.meta);import.meta.css = /* css */`
15915
- .navi_details {
15916
- position: relative;
15917
- z-index: 1;
15918
- display: flex;
15919
- flex-shrink: 0;
15920
- flex-direction: column;
15896
+ /* Focus */
15897
+ .navi_checkbox[data-focus-visible] .navi_checkbox_field {
15898
+ outline-style: solid;
15921
15899
  }
15922
-
15923
- .navi_details > summary {
15924
- display: flex;
15925
- flex-shrink: 0;
15926
- flex-direction: column;
15927
- cursor: pointer;
15928
- user-select: none;
15900
+ /* Hover */
15901
+ .navi_checkbox[data-hover] {
15902
+ --x-border-color: var(--border-color-hover);
15929
15903
  }
15930
- .summary_body {
15931
- display: flex;
15932
- width: 100%;
15933
- flex-direction: row;
15934
- align-items: center;
15935
- gap: 0.2em;
15904
+ .navi_checkbox[data-checked][data-hover] {
15905
+ --x-border-color: var(--border-color-hover-checked);
15906
+ --x-background-color: var(--background-color-hover-checked);
15936
15907
  }
15937
- .summary_label {
15938
- display: flex;
15939
- padding-right: 10px;
15940
- flex: 1;
15941
- align-items: center;
15942
- gap: 0.2em;
15908
+ /* Checked */
15909
+ .navi_checkbox[data-checked] {
15910
+ --x-background-color: var(--x-color);
15911
+ --x-border-color: var(--x-color);
15943
15912
  }
15944
-
15945
- .navi_details > summary:focus {
15946
- z-index: 1;
15913
+ /* Readonly */
15914
+ .navi_checkbox[data-readonly],
15915
+ .navi_checkbox[data-readonly][data-hover] {
15916
+ --x-border-color: var(--border-color-readonly);
15917
+ --x-background-color: var(--background-color-readonly);
15918
+ }
15919
+ .navi_checkbox[data-readonly][data-checked] {
15920
+ --x-border-color: var(--border-color-readonly-checked);
15921
+ --x-background-color: var(--background-color-readonly-checked);
15922
+ --x-checkmark-color: var(--checkmark-color-readonly);
15923
+ }
15924
+ /* Disabled */
15925
+ .navi_checkbox[data-disabled] {
15926
+ --x-border-color: var(--border-color-disabled);
15927
+ --x-background-color: var(--background-color-disabled);
15928
+ }
15929
+ .navi_checkbox[data-disabled][data-checked] {
15930
+ --x-border-color: var(--border-color-disabled-checked);
15931
+ --x-background-color: var(--background-color-disabled-checked);
15932
+ --x-checkmark-color: var(--checkmark-color-disabled);
15947
15933
  }
15948
15934
  `;
15949
- const Details = forwardRef((props, ref) => {
15950
- return renderActionableComponent(props, ref);
15951
- });
15952
- const DetailsBasic = forwardRef((props, ref) => {
15935
+ const InputCheckbox = props => {
15953
15936
  const {
15954
- id,
15955
- label = "Summary",
15956
- open,
15957
- loading,
15958
- className,
15959
- focusGroup,
15960
- focusGroupDirection,
15961
- arrowKeyShortcuts = true,
15962
- openKeyShortcut = "ArrowRight",
15963
- closeKeyShortcut = "ArrowLeft",
15964
- onToggle,
15965
- children,
15966
- ...rest
15937
+ value = "on"
15967
15938
  } = props;
15968
- const innerRef = useRef();
15969
- useImperativeHandle(ref, () => innerRef.current);
15970
- const [navState, setNavState] = useNavState(id);
15971
- const [innerOpen, innerOpenSetter] = useState(open || navState);
15972
- useFocusGroup(innerRef, {
15973
- enabled: focusGroup,
15974
- name: typeof focusGroup === "string" ? focusGroup : undefined,
15975
- direction: focusGroupDirection
15939
+ const uiStateController = useUIStateController(props, "checkbox", {
15940
+ statePropName: "checked",
15941
+ defaultStatePropName: "defaultChecked",
15942
+ fallbackState: false,
15943
+ getStateFromProp: checked => checked ? value : undefined,
15944
+ getPropFromState: Boolean
15976
15945
  });
15946
+ const uiState = useUIState(uiStateController);
15947
+ const checkbox = renderActionableComponent(props, {
15948
+ Basic: InputCheckboxBasic,
15949
+ WithAction: InputCheckboxWithAction,
15950
+ InsideForm: InputCheckboxInsideForm
15951
+ });
15952
+ return jsx(UIStateControllerContext.Provider, {
15953
+ value: uiStateController,
15954
+ children: jsx(UIStateContext.Provider, {
15955
+ value: uiState,
15956
+ children: checkbox
15957
+ })
15958
+ });
15959
+ };
15960
+ const CheckboxStyleCSSVars = {
15961
+ "outlineWidth": "--outline-width",
15962
+ "borderWidth": "--border-width",
15963
+ "borderRadius": "--border-radius",
15964
+ "backgroundColor": "--background-color",
15965
+ "borderColor": "--border-color",
15966
+ "color": "--color",
15967
+ ":hover": {
15968
+ backgroundColor: "--background-color-hover",
15969
+ borderColor: "--border-color-hover",
15970
+ color: "--color-hover"
15971
+ },
15972
+ ":active": {
15973
+ borderColor: "--border-color-active"
15974
+ },
15975
+ ":read-only": {
15976
+ backgroundColor: "--background-color-readonly",
15977
+ borderColor: "--border-color-readonly",
15978
+ color: "--color-readonly"
15979
+ },
15980
+ ":disabled": {
15981
+ backgroundColor: "--background-color-disabled",
15982
+ borderColor: "--border-color-disabled",
15983
+ color: "--color-disabled"
15984
+ }
15985
+ };
15986
+ const CheckboxPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
15987
+ const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
15988
+ const InputCheckboxBasic = props => {
15989
+ const contextFieldName = useContext(FieldNameContext);
15990
+ const contextReadOnly = useContext(ReadOnlyContext);
15991
+ const contextDisabled = useContext(DisabledContext);
15992
+ const contextRequired = useContext(RequiredContext);
15993
+ const contextLoading = useContext(LoadingContext);
15994
+ const loadingElement = useContext(LoadingElementContext);
15995
+ const uiStateController = useContext(UIStateControllerContext);
15996
+ const uiState = useContext(UIStateContext);
15997
+ const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
15998
+ const reportDisabledOnLabel = useContext(ReportDisabledOnLabelContext);
15999
+ const {
16000
+ /* eslint-disable no-unused-vars */
16001
+ type,
16002
+ defaultChecked,
16003
+ /* eslint-enable no-unused-vars */
15977
16004
 
15978
- /**
15979
- * Browser will dispatch "toggle" event even if we set open={true}
15980
- * When rendering the component for the first time
15981
- * We have to ensure the initial "toggle" event is ignored.
15982
- *
15983
- * If we don't do that code will think the details has changed and run logic accordingly
15984
- * For example it will try to navigate to the current url while we are already there
15985
- *
15986
- * See:
15987
- * - https://techblog.thescore.com/2024/10/08/why-we-decided-to-change-how-the-details-element-works/
15988
- * - https://github.com/whatwg/html/issues/4500
15989
- * - https://stackoverflow.com/questions/58942600/react-html-details-toggles-uncontrollably-when-starts-open
15990
- *
15991
- */
15992
-
15993
- const summaryRef = useRef(null);
15994
- useKeyboardShortcuts(innerRef, [{
15995
- key: openKeyShortcut,
15996
- enabled: arrowKeyShortcuts,
15997
- when: e => document.activeElement === summaryRef.current &&
15998
- // avoid handling openKeyShortcut twice when keydown occurs inside nested details
15999
- !e.defaultPrevented,
16000
- action: e => {
16001
- const details = innerRef.current;
16002
- if (!details.open) {
16003
- e.preventDefault();
16004
- details.open = true;
16005
- return;
16006
- }
16007
- const summary = summaryRef.current;
16008
- const firstFocusableElementInDetails = findAfter(summary, elementIsFocusable, {
16009
- root: details
16010
- });
16011
- if (!firstFocusableElementInDetails) {
16012
- return;
16013
- }
16005
+ name,
16006
+ readOnly,
16007
+ disabled,
16008
+ required,
16009
+ loading,
16010
+ autoFocus,
16011
+ constraints = [],
16012
+ onClick,
16013
+ onInput,
16014
+ color,
16015
+ ...rest
16016
+ } = props;
16017
+ const defaultRef = useRef();
16018
+ const ref = props.ref || defaultRef;
16019
+ const innerName = name || contextFieldName;
16020
+ const innerDisabled = disabled || contextDisabled;
16021
+ const innerRequired = required || contextRequired;
16022
+ const innerLoading = loading || contextLoading && loadingElement === ref.current;
16023
+ const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
16024
+ reportReadOnlyOnLabel?.(innerReadOnly);
16025
+ reportDisabledOnLabel?.(innerDisabled);
16026
+ useAutoFocus(ref, autoFocus);
16027
+ useConstraints(ref, constraints);
16028
+ const checked = Boolean(uiState);
16029
+ const innerOnClick = useStableCallback(e => {
16030
+ if (innerReadOnly) {
16014
16031
  e.preventDefault();
16015
- firstFocusableElementInDetails.focus();
16016
16032
  }
16017
- }, {
16018
- key: closeKeyShortcut,
16019
- enabled: arrowKeyShortcuts,
16020
- when: () => {
16021
- const details = innerRef.current;
16022
- return details.open;
16033
+ onClick?.(e);
16034
+ });
16035
+ const innerOnInput = useStableCallback(e => {
16036
+ const checkbox = e.target;
16037
+ const checkboxIsChecked = checkbox.checked;
16038
+ uiStateController.setUIState(checkboxIsChecked, e);
16039
+ onInput?.(e);
16040
+ });
16041
+ const renderCheckbox = checkboxProps => jsx(Box, {
16042
+ ...checkboxProps,
16043
+ as: "input",
16044
+ ref: ref,
16045
+ type: "checkbox",
16046
+ name: innerName,
16047
+ checked: checked,
16048
+ required: innerRequired,
16049
+ baseClassName: "navi_native_field",
16050
+ "data-callout-arrow-x": "center",
16051
+ onClick: innerOnClick,
16052
+ onInput: innerOnInput,
16053
+ onresetuistate: e => {
16054
+ uiStateController.resetUIState(e);
16023
16055
  },
16024
- action: e => {
16025
- const details = innerRef.current;
16026
- const summary = summaryRef.current;
16027
- if (document.activeElement === summary) {
16028
- e.preventDefault();
16029
- summary.focus();
16030
- details.open = false;
16031
- } else {
16032
- e.preventDefault();
16033
- summary.focus();
16034
- }
16056
+ onsetuistate: e => {
16057
+ uiStateController.setUIState(e.detail.value, e);
16035
16058
  }
16036
- }]);
16037
- const mountedRef = useRef(false);
16038
- useEffect(() => {
16039
- mountedRef.current = true;
16040
- }, []);
16041
- return jsxs("details", {
16059
+ });
16060
+ const renderCheckboxMemoized = useCallback(renderCheckbox, [innerName, checked, innerRequired]);
16061
+ useLayoutEffect(() => {
16062
+ const naviCheckbox = ref.current;
16063
+ const lightColor = "var(--checkmark-color-light)";
16064
+ const darkColor = "var(--checkmark-color-dark)";
16065
+ const colorPicked = pickLightOrDark("var(--color)", lightColor, darkColor, naviCheckbox);
16066
+ if (colorPicked === lightColor) {
16067
+ naviCheckbox.removeAttribute("data-dark");
16068
+ } else {
16069
+ naviCheckbox.setAttribute("data-dark", "");
16070
+ }
16071
+ }, [color]);
16072
+ return jsxs(Box, {
16073
+ as: "span",
16042
16074
  ...rest,
16043
- ref: innerRef,
16044
- id: id,
16045
- className: ["navi_details", ...(className ? className.split(" ") : [])].join(" "),
16046
- onToggle: e => {
16047
- const isOpen = e.newState === "open";
16048
- if (mountedRef.current) {
16049
- if (isOpen) {
16050
- innerOpenSetter(true);
16051
- setNavState(true);
16052
- } else {
16053
- innerOpenSetter(false);
16054
- setNavState(undefined);
16055
- }
16056
- }
16057
- onToggle?.(e);
16075
+ ref: ref,
16076
+ baseClassName: "navi_checkbox",
16077
+ pseudoStateSelector: ".navi_native_field",
16078
+ styleCSSVars: CheckboxStyleCSSVars,
16079
+ pseudoClasses: CheckboxPseudoClasses,
16080
+ pseudoElements: CheckboxPseudoElements,
16081
+ basePseudoState: {
16082
+ ":read-only": innerReadOnly,
16083
+ ":disabled": innerDisabled,
16084
+ ":-navi-loading": innerLoading
16058
16085
  },
16059
- open: innerOpen,
16060
- children: [jsx("summary", {
16061
- ref: summaryRef,
16062
- children: jsxs("div", {
16063
- className: "summary_body",
16064
- children: [jsx(SummaryMarker, {
16065
- open: innerOpen,
16066
- loading: loading
16067
- }), jsx("div", {
16068
- className: "summary_label",
16069
- children: label
16070
- })]
16086
+ color: color,
16087
+ hasChildFunction: true,
16088
+ children: [jsx(LoaderBackground, {
16089
+ loading: innerLoading,
16090
+ inset: -1,
16091
+ color: "var(--loader-color)"
16092
+ }), renderCheckboxMemoized, jsx("div", {
16093
+ className: "navi_checkbox_field",
16094
+ children: jsx("svg", {
16095
+ viewBox: "0 0 12 12",
16096
+ "aria-hidden": "true",
16097
+ className: "navi_checkbox_marker",
16098
+ children: jsx("path", {
16099
+ d: "M10.5 2L4.5 9L1.5 5.5",
16100
+ fill: "none",
16101
+ strokeWidth: "2"
16102
+ })
16071
16103
  })
16072
- }), children]
16104
+ })]
16073
16105
  });
16074
- });
16075
- forwardRef((props, ref) => {
16106
+ };
16107
+ const InputCheckboxWithAction = props => {
16108
+ const uiStateController = useContext(UIStateControllerContext);
16109
+ const uiState = useContext(UIStateContext);
16076
16110
  const {
16077
16111
  action,
16078
- loading,
16079
- onToggle,
16112
+ onCancel,
16113
+ onChange,
16114
+ actionErrorEffect,
16080
16115
  onActionPrevented,
16081
16116
  onActionStart,
16117
+ onActionAbort,
16082
16118
  onActionError,
16083
16119
  onActionEnd,
16084
- children,
16120
+ loading,
16085
16121
  ...rest
16086
16122
  } = props;
16087
- const innerRef = useRef();
16088
- useImperativeHandle(ref, () => innerRef.current);
16089
- const effectiveAction = useAction(action);
16123
+ const defaultRef = useRef();
16124
+ const ref = props.ref || defaultRef;
16125
+ const [actionBoundToUIState] = useActionBoundToOneParam(action, uiState);
16090
16126
  const {
16091
16127
  loading: actionLoading
16092
- } = useActionStatus(effectiveAction);
16093
- const executeAction = useExecuteAction(innerRef, {
16094
- // the error will be displayed by actionRenderer inside <details>
16095
- errorEffect: "none"
16128
+ } = useActionStatus(actionBoundToUIState);
16129
+ const executeAction = useExecuteAction(ref, {
16130
+ errorEffect: actionErrorEffect
16096
16131
  });
16097
- useActionEvents(innerRef, {
16098
- onPrevented: onActionPrevented,
16099
- onAction: e => {
16100
- executeAction(e);
16132
+
16133
+ // In this situation updating the ui state === calling associated action
16134
+ // so cance/abort/error have to revert the ui state to the one before user interaction
16135
+ // to show back the real state of the checkbox (not the one user tried to set)
16136
+ useActionEvents(ref, {
16137
+ onCancel: (e, reason) => {
16138
+ if (reason === "blur_invalid") {
16139
+ return;
16140
+ }
16141
+ uiStateController.resetUIState(e);
16142
+ onCancel?.(e, reason);
16101
16143
  },
16144
+ onPrevented: onActionPrevented,
16145
+ onAction: executeAction,
16102
16146
  onStart: onActionStart,
16103
- onError: onActionError,
16104
- onEnd: onActionEnd
16147
+ onAbort: e => {
16148
+ uiStateController.resetUIState(e);
16149
+ onActionAbort?.(e);
16150
+ },
16151
+ onError: e => {
16152
+ uiStateController.resetUIState(e);
16153
+ onActionError?.(e);
16154
+ },
16155
+ onEnd: e => {
16156
+ onActionEnd?.(e);
16157
+ }
16105
16158
  });
16106
- return jsx(DetailsBasic, {
16159
+ return jsx(InputCheckboxBasic, {
16160
+ "data-action": actionBoundToUIState.name,
16107
16161
  ...rest,
16108
- ref: innerRef,
16162
+ ref: ref,
16109
16163
  loading: loading || actionLoading,
16110
- onToggle: toggleEvent => {
16111
- const isOpen = toggleEvent.newState === "open";
16112
- if (isOpen) {
16113
- requestAction(toggleEvent.target, effectiveAction, {
16114
- event: toggleEvent,
16115
- method: "run"
16116
- });
16117
- } else {
16118
- effectiveAction.abort();
16119
- }
16120
- onToggle?.(toggleEvent);
16121
- },
16122
- children: jsx(ActionRenderer, {
16123
- action: effectiveAction,
16124
- children: children
16125
- })
16126
- });
16127
- });
16128
-
16129
- installImportMetaCss(import.meta);import.meta.css = /* css */`
16130
- @layer navi {
16131
- label {
16132
- cursor: pointer;
16133
- }
16134
-
16135
- label[data-readonly],
16136
- label[data-disabled] {
16137
- color: rgba(0, 0, 0, 0.5);
16138
- cursor: default;
16164
+ onChange: e => {
16165
+ requestAction(e.target, actionBoundToUIState, {
16166
+ event: e
16167
+ });
16168
+ onChange?.(e);
16139
16169
  }
16140
- }
16141
- `;
16142
- const ReportReadOnlyOnLabelContext = createContext();
16143
- const ReportDisabledOnLabelContext = createContext();
16144
- const Label = props => {
16145
- const {
16146
- readOnly,
16147
- disabled,
16148
- children,
16149
- ...rest
16150
- } = props;
16151
- const [inputReadOnly, setInputReadOnly] = useState(false);
16152
- const innerReadOnly = readOnly || inputReadOnly;
16153
- const [inputDisabled, setInputDisabled] = useState(false);
16154
- const innerDisabled = disabled || inputDisabled;
16155
- return jsx(Box, {
16156
- ...rest,
16157
- as: "label",
16158
- basePseudoState: {
16159
- readOnly: innerReadOnly,
16160
- disabled: innerDisabled
16161
- },
16162
- children: jsx(ReportReadOnlyOnLabelContext.Provider, {
16163
- value: setInputReadOnly,
16164
- children: jsx(ReportDisabledOnLabelContext.Provider, {
16165
- value: setInputDisabled,
16166
- children: children
16167
- })
16168
- })
16169
16170
  });
16170
16171
  };
16172
+ const InputCheckboxInsideForm = InputCheckboxBasic;
16171
16173
 
16172
16174
  installImportMetaCss(import.meta);import.meta.css = /* css */`
16173
16175
  @layer navi {
16174
- .navi_checkbox {
16176
+ .navi_radio {
16175
16177
  --outline-offset: 1px;
16176
16178
  --outline-width: 2px;
16177
- --border-width: 1px;
16178
- --border-radius: 2px;
16179
16179
  --width: 13px;
16180
16180
  --height: 13px;
16181
16181
 
@@ -16184,12 +16184,11 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
16184
16184
  --border-color: light-dark(#767676, #8e8e93);
16185
16185
  --background-color: white;
16186
16186
  --color: light-dark(#4476ff, #3b82f6);
16187
- --checkmark-color-light: white;
16188
- --checkmark-color-dark: rgb(55, 55, 55);
16189
- --checkmark-color: var(--checkmark-color-light);
16187
+ --radiomark-color: var(--color);
16188
+ --border-color-checked: var(--color);
16190
16189
 
16191
- --color-mix-light: black;
16192
- --color-mix-dark: white;
16190
+ --color-mix-light: white;
16191
+ --color-mix-dark: black;
16193
16192
  --color-mix: var(--color-mix-light);
16194
16193
 
16195
16194
  /* Hover */
@@ -16199,7 +16198,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
16199
16198
  var(--color) 80%,
16200
16199
  var(--color-mix)
16201
16200
  );
16202
- --background-color-hover-checked: color-mix(
16201
+ --radiomark-color-hover: color-mix(
16203
16202
  in srgb,
16204
16203
  var(--color) 80%,
16205
16204
  var(--color-mix)
@@ -16210,30 +16209,31 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
16210
16209
  var(--border-color) 30%,
16211
16210
  white
16212
16211
  );
16212
+ --background-color-readonly: var(--background-color);
16213
+ --radiomark-color-readonly: grey;
16213
16214
  --border-color-readonly-checked: #d3d3d3;
16214
- --background-color-readonly-checked: grey;
16215
- --checkmark-color-readonly: #eeeeee;
16215
+ --background-color-readonly-checked: #d3d3d3;
16216
16216
  /* Disabled */
16217
16217
  --border-color-disabled: var(--border-color-readonly);
16218
16218
  --background-color-disabled: rgba(248, 248, 248, 0.7);
16219
- --checkmark-color-disabled: #eeeeee;
16220
- --border-color-disabled-checked: #d3d3d3;
16221
- --background-color-disabled-checked: #d3d3d3;
16219
+ --radiomark-color-disabled: #d3d3d3;
16220
+ --border-color-checked-disabled: #d3d3d3;
16221
+ --background-color-disabled-checked: var(--background-color);
16222
16222
  }
16223
16223
 
16224
- .navi_checkbox[data-dark] {
16224
+ .navi_radio[data-dark] {
16225
16225
  --color-mix: var(--color-mix-dark);
16226
- --checkmark-color: var(--navi-checkmark-color-dark);
16227
16226
  }
16228
16227
  }
16229
16228
 
16230
- .navi_checkbox {
16229
+ .navi_radio {
16231
16230
  position: relative;
16232
16231
  display: inline-flex;
16233
16232
  box-sizing: content-box;
16234
- margin: 3px 3px 3px 4px;
16233
+ margin-top: 3px;
16234
+ margin-right: 3px;
16235
+ margin-left: 5px;
16235
16236
 
16236
- --x-border-radius: var(--border-radius);
16237
16237
  --x-outline-offset: var(--outline-offset);
16238
16238
  --x-outline-width: var(--outline-width);
16239
16239
  --x-border-width: var(--border-width);
@@ -16243,111 +16243,128 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
16243
16243
  --x-background-color: var(--background-color);
16244
16244
  --x-border-color: var(--border-color);
16245
16245
  --x-color: var(--color);
16246
- --x-checkmark-color: var(--checkmark-color);
16246
+ --x-radiomark-color: var(--radiomark-color);
16247
16247
  }
16248
- .navi_checkbox .navi_native_field {
16248
+ .navi_radio .navi_native_field {
16249
16249
  position: absolute;
16250
16250
  inset: 0;
16251
16251
  margin: 0;
16252
16252
  padding: 0;
16253
- border: none;
16254
16253
  opacity: 0;
16255
16254
  cursor: inherit;
16256
16255
  }
16257
- .navi_checkbox .navi_checkbox_field {
16256
+ .navi_radio .navi_radio_field {
16258
16257
  display: inline-flex;
16259
16258
  box-sizing: border-box;
16260
16259
  width: var(--x-width);
16261
16260
  height: var(--x-height);
16262
- background-color: var(--x-background-color);
16263
- border-width: var(--x-border-width);
16264
- border-style: solid;
16265
- border-color: var(--x-border-color);
16266
- border-radius: var(--x-border-radius);
16261
+ align-items: center;
16262
+ justify-content: center;
16263
+ border-radius: 50%;
16267
16264
  outline-width: var(--x-outline-width);
16268
16265
  outline-style: none;
16269
16266
  outline-color: var(--x-outline-color);
16270
16267
  outline-offset: var(--x-outline-offset);
16271
16268
  }
16272
- .navi_checkbox_marker {
16269
+ .navi_radio_field svg {
16270
+ overflow: visible;
16271
+ }
16272
+ .navi_radio_border {
16273
+ fill: var(--x-background-color);
16274
+ stroke: var(--x-border-color);
16275
+ }
16276
+ .navi_radio_marker {
16273
16277
  width: 100%;
16274
16278
  height: 100%;
16275
16279
  opacity: 0;
16276
- stroke: var(--x-checkmark-color);
16277
- transform: scale(0.5);
16278
- transition: all 0.15s ease;
16280
+ fill: var(--x-radiomark-color);
16281
+ transform: scale(0.3);
16282
+ transform-origin: center;
16279
16283
  pointer-events: none;
16280
16284
  }
16281
- .navi_checkbox[data-checked] .navi_checkbox_marker {
16282
- opacity: 1;
16283
- transform: scale(1);
16284
- }
16285
-
16286
- /* Focus */
16287
- .navi_checkbox[data-focus-visible] .navi_checkbox_field {
16288
- outline-style: solid;
16285
+ .navi_radio_dashed_border {
16286
+ display: none;
16287
+ }
16288
+ .navi_radio[data-transition] .navi_radio_marker {
16289
+ transition: all 0.15s ease;
16290
+ }
16291
+ .navi_radio[data-transition] .navi_radio_dashed_border {
16292
+ transition: all 0.15s ease;
16293
+ }
16294
+ .navi_radio[data-transition] .navi_radio_border {
16295
+ transition: all 0.15s ease;
16296
+ }
16297
+
16298
+ /* Focus */
16299
+ .navi_radio[data-focus-visible] .navi_radio_field {
16300
+ outline-style: solid;
16289
16301
  }
16290
16302
  /* Hover */
16291
- .navi_checkbox[data-hover] {
16303
+ .navi_radio[data-hover] {
16292
16304
  --x-border-color: var(--border-color-hover);
16293
- }
16294
- .navi_checkbox[data-checked][data-hover] {
16295
- --x-border-color: var(--border-color-hover-checked);
16296
- --x-background-color: var(--background-color-hover-checked);
16305
+ --x-radiomark-color: var(--radiomark-color-hover);
16297
16306
  }
16298
16307
  /* Checked */
16299
- .navi_checkbox[data-checked] {
16300
- --x-background-color: var(--x-color);
16301
- --x-border-color: var(--x-color);
16308
+ .navi_radio[data-checked] {
16309
+ --x-border-color: var(--border-color-checked);
16310
+ }
16311
+ .navi_radio[data-checked] .navi_radio_marker {
16312
+ opacity: 1;
16313
+ transform: scale(1);
16314
+ }
16315
+ .navi_radio[data-hover][data-checked] {
16316
+ --x-border-color: var(--border-color-hover-checked);
16302
16317
  }
16303
16318
  /* Readonly */
16304
- .navi_checkbox[data-readonly],
16305
- .navi_checkbox[data-readonly][data-hover] {
16306
- --x-border-color: var(--border-color-readonly);
16319
+ .navi_radio[data-readonly] {
16307
16320
  --x-background-color: var(--background-color-readonly);
16321
+ --x-border-color: var(--border-color-readonly);
16322
+ --x-radiomark-color: var(--radiomark-color-readonly);
16308
16323
  }
16309
- .navi_checkbox[data-readonly][data-checked] {
16310
- --x-border-color: var(--border-color-readonly-checked);
16324
+ .navi_radio[data-readonly] .navi_radio_dashed_border {
16325
+ display: none;
16326
+ }
16327
+ .navi_radio[data-readonly][data-checked] {
16311
16328
  --x-background-color: var(--background-color-readonly-checked);
16312
- --x-checkmark-color: var(--checkmark-color-readonly);
16329
+ --x-border-color: var(--border-color-readonly-checked);
16330
+ --x-radiomark-color: var(--radiomark-color-readonly);
16313
16331
  }
16314
16332
  /* Disabled */
16315
- .navi_checkbox[data-disabled] {
16316
- --x-border-color: var(--border-color-disabled);
16333
+ .navi_radio[data-disabled] {
16317
16334
  --x-background-color: var(--background-color-disabled);
16335
+ --x-border-color: var(--border-color-disabled);
16336
+ --x-radiomark-color: var(--radiomark-color-disabled);
16318
16337
  }
16319
- .navi_checkbox[data-disabled][data-checked] {
16320
- --x-border-color: var(--border-color-disabled-checked);
16321
- --x-background-color: var(--background-color-disabled-checked);
16322
- --x-checkmark-color: var(--checkmark-color-disabled);
16338
+ .navi_radio[data-disabled][data-checked] {
16339
+ --x-border-color: var(--border-color-disabled);
16340
+ --x-radiomark-color: var(--radiomark-color-disabled);
16323
16341
  }
16324
16342
  `;
16325
- const InputCheckbox = props => {
16343
+ const InputRadio = props => {
16326
16344
  const {
16327
16345
  value = "on"
16328
16346
  } = props;
16329
- const uiStateController = useUIStateController(props, "checkbox", {
16347
+ const uiStateController = useUIStateController(props, "radio", {
16330
16348
  statePropName: "checked",
16331
- defaultStatePropName: "defaultChecked",
16332
16349
  fallbackState: false,
16333
16350
  getStateFromProp: checked => checked ? value : undefined,
16334
16351
  getPropFromState: Boolean
16335
16352
  });
16336
16353
  const uiState = useUIState(uiStateController);
16337
- const checkbox = renderActionableComponent(props, {
16338
- Basic: InputCheckboxBasic,
16339
- WithAction: InputCheckboxWithAction,
16340
- InsideForm: InputCheckboxInsideForm
16354
+ const radio = renderActionableComponent(props, {
16355
+ Basic: InputRadioBasic,
16356
+ WithAction: InputRadioWithAction,
16357
+ InsideForm: InputRadioInsideForm
16341
16358
  });
16342
16359
  return jsx(UIStateControllerContext.Provider, {
16343
16360
  value: uiStateController,
16344
16361
  children: jsx(UIStateContext.Provider, {
16345
16362
  value: uiState,
16346
- children: checkbox
16363
+ children: radio
16347
16364
  })
16348
16365
  });
16349
16366
  };
16350
- const CheckboxStyleCSSVars = {
16367
+ const RadioStyleCSSVars = {
16351
16368
  "outlineWidth": "--outline-width",
16352
16369
  "borderWidth": "--border-width",
16353
16370
  "borderRadius": "--border-radius",
@@ -16373,15 +16390,15 @@ const CheckboxStyleCSSVars = {
16373
16390
  color: "--color-disabled"
16374
16391
  }
16375
16392
  };
16376
- const CheckboxPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
16377
- const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
16378
- const InputCheckboxBasic = props => {
16379
- const contextFieldName = useContext(FieldNameContext);
16393
+ const RadioPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
16394
+ const RadioPseudoElements = ["::-navi-loader", "::-navi-radiomark"];
16395
+ const InputRadioBasic = props => {
16396
+ const contextName = useContext(FieldNameContext);
16380
16397
  const contextReadOnly = useContext(ReadOnlyContext);
16381
16398
  const contextDisabled = useContext(DisabledContext);
16382
16399
  const contextRequired = useContext(RequiredContext);
16383
16400
  const contextLoading = useContext(LoadingContext);
16384
- const loadingElement = useContext(LoadingElementContext);
16401
+ const contextLoadingElement = useContext(LoadingElementContext);
16385
16402
  const uiStateController = useContext(UIStateControllerContext);
16386
16403
  const uiState = useContext(UIStateContext);
16387
16404
  const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
@@ -16389,7 +16406,6 @@ const InputCheckboxBasic = props => {
16389
16406
  const {
16390
16407
  /* eslint-disable no-unused-vars */
16391
16408
  type,
16392
- defaultChecked,
16393
16409
  /* eslint-enable no-unused-vars */
16394
16410
 
16395
16411
  name,
@@ -16406,35 +16422,64 @@ const InputCheckboxBasic = props => {
16406
16422
  } = props;
16407
16423
  const defaultRef = useRef();
16408
16424
  const ref = props.ref || defaultRef;
16409
- const innerName = name || contextFieldName;
16425
+ const innerName = name || contextName;
16410
16426
  const innerDisabled = disabled || contextDisabled;
16411
16427
  const innerRequired = required || contextRequired;
16412
- const innerLoading = loading || contextLoading && loadingElement === ref.current;
16428
+ const innerLoading = loading || contextLoading && contextLoadingElement === ref.current;
16413
16429
  const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
16414
16430
  reportReadOnlyOnLabel?.(innerReadOnly);
16415
16431
  reportDisabledOnLabel?.(innerDisabled);
16416
16432
  useAutoFocus(ref, autoFocus);
16417
16433
  useConstraints(ref, constraints);
16418
16434
  const checked = Boolean(uiState);
16435
+ // we must first dispatch an event to inform all other radios they where unchecked
16436
+ // this way each other radio uiStateController knows thery are unchecked
16437
+ // we do this on "input"
16438
+ // but also when we are becoming checked from outside (hence the useLayoutEffect)
16439
+ const updateOtherRadiosInGroup = () => {
16440
+ const thisRadio = ref.current;
16441
+ const radioList = thisRadio.closest("[data-radio-list]");
16442
+ if (!radioList) {
16443
+ return;
16444
+ }
16445
+ const radioInputs = radioList.querySelectorAll(`input[type="radio"][name="${thisRadio.name}"]`);
16446
+ for (const radioInput of radioInputs) {
16447
+ if (radioInput === thisRadio) {
16448
+ continue;
16449
+ }
16450
+ radioInput.dispatchEvent(new CustomEvent("setuistate", {
16451
+ detail: false
16452
+ }));
16453
+ }
16454
+ };
16455
+ useLayoutEffect(() => {
16456
+ if (checked) {
16457
+ updateOtherRadiosInGroup();
16458
+ }
16459
+ }, [checked]);
16460
+ const innerOnInput = useStableCallback(e => {
16461
+ const radio = e.target;
16462
+ const radioIsChecked = radio.checked;
16463
+ if (radioIsChecked) {
16464
+ updateOtherRadiosInGroup();
16465
+ }
16466
+ uiStateController.setUIState(radioIsChecked, e);
16467
+ onInput?.(e);
16468
+ });
16419
16469
  const innerOnClick = useStableCallback(e => {
16420
16470
  if (innerReadOnly) {
16421
16471
  e.preventDefault();
16422
16472
  }
16423
16473
  onClick?.(e);
16424
16474
  });
16425
- const innerOnInput = useStableCallback(e => {
16426
- const checkbox = e.target;
16427
- const checkboxIsChecked = checkbox.checked;
16428
- uiStateController.setUIState(checkboxIsChecked, e);
16429
- onInput?.(e);
16430
- });
16431
- const renderCheckbox = checkboxProps => jsx(Box, {
16432
- ...checkboxProps,
16475
+ const renderRadio = radioProps => jsx(Box, {
16476
+ ...radioProps,
16433
16477
  as: "input",
16434
16478
  ref: ref,
16435
- type: "checkbox",
16479
+ type: "radio",
16436
16480
  name: innerName,
16437
16481
  checked: checked,
16482
+ disabled: innerDisabled,
16438
16483
  required: innerRequired,
16439
16484
  baseClassName: "navi_native_field",
16440
16485
  "data-callout-arrow-x": "center",
@@ -16447,27 +16492,25 @@ const InputCheckboxBasic = props => {
16447
16492
  uiStateController.setUIState(e.detail.value, e);
16448
16493
  }
16449
16494
  });
16450
- const renderCheckboxMemoized = useCallback(renderCheckbox, [innerName, checked, innerRequired]);
16495
+ const renderRadioMemoized = useCallback(renderRadio, [innerName, checked, innerRequired]);
16451
16496
  useLayoutEffect(() => {
16452
- const naviCheckbox = ref.current;
16453
- const lightColor = "var(--checkmark-color-light)";
16454
- const darkColor = "var(--checkmark-color-dark)";
16455
- const colorPicked = pickLightOrDark("var(--color)", lightColor, darkColor, naviCheckbox);
16456
- if (colorPicked === lightColor) {
16457
- naviCheckbox.removeAttribute("data-dark");
16497
+ const naviRadio = ref.current;
16498
+ const luminance = resolveColorLuminance("var(--color)", naviRadio);
16499
+ if (luminance < 0.3) {
16500
+ naviRadio.setAttribute("data-dark", "");
16458
16501
  } else {
16459
- naviCheckbox.setAttribute("data-dark", "");
16502
+ naviRadio.removeAttribute("data-dark");
16460
16503
  }
16461
16504
  }, [color]);
16462
16505
  return jsxs(Box, {
16463
16506
  as: "span",
16464
16507
  ...rest,
16465
16508
  ref: ref,
16466
- baseClassName: "navi_checkbox",
16509
+ baseClassName: "navi_radio",
16467
16510
  pseudoStateSelector: ".navi_native_field",
16468
- styleCSSVars: CheckboxStyleCSSVars,
16469
- pseudoClasses: CheckboxPseudoClasses,
16470
- pseudoElements: CheckboxPseudoElements,
16511
+ styleCSSVars: RadioStyleCSSVars,
16512
+ pseudoClasses: RadioPseudoClasses,
16513
+ pseudoElements: RadioPseudoElements,
16471
16514
  basePseudoState: {
16472
16515
  ":read-only": innerReadOnly,
16473
16516
  ":disabled": innerDisabled,
@@ -16478,286 +16521,186 @@ const InputCheckboxBasic = props => {
16478
16521
  children: [jsx(LoaderBackground, {
16479
16522
  loading: innerLoading,
16480
16523
  inset: -1,
16524
+ targetSelector: ".navi_radio_field",
16481
16525
  color: "var(--loader-color)"
16482
- }), renderCheckboxMemoized, jsx("div", {
16483
- className: "navi_checkbox_field",
16484
- children: jsx("svg", {
16526
+ }), renderRadioMemoized, jsx("span", {
16527
+ className: "navi_radio_field",
16528
+ children: jsxs("svg", {
16485
16529
  viewBox: "0 0 12 12",
16486
16530
  "aria-hidden": "true",
16487
- className: "navi_checkbox_marker",
16488
- children: jsx("path", {
16489
- d: "M10.5 2L4.5 9L1.5 5.5",
16490
- fill: "none",
16491
- strokeWidth: "2"
16492
- })
16531
+ preserveAspectRatio: "xMidYMid meet",
16532
+ children: [jsx("circle", {
16533
+ className: "navi_radio_border",
16534
+ cx: "6",
16535
+ cy: "6",
16536
+ r: "5.5",
16537
+ strokeWidth: "1"
16538
+ }), jsx("circle", {
16539
+ className: "navi_radio_dashed_border",
16540
+ cx: "6",
16541
+ cy: "6",
16542
+ r: "5.5",
16543
+ strokeWidth: "1",
16544
+ strokeDasharray: "2.16 2.16",
16545
+ strokeDashoffset: "0"
16546
+ }), jsx("circle", {
16547
+ className: "navi_radio_marker",
16548
+ cx: "6",
16549
+ cy: "6",
16550
+ r: "3.5"
16551
+ })]
16493
16552
  })
16494
16553
  })]
16495
16554
  });
16496
16555
  };
16497
- const InputCheckboxWithAction = props => {
16498
- const uiStateController = useContext(UIStateControllerContext);
16499
- const uiState = useContext(UIStateContext);
16500
- const {
16501
- action,
16502
- onCancel,
16503
- onChange,
16504
- actionErrorEffect,
16505
- onActionPrevented,
16506
- onActionStart,
16507
- onActionAbort,
16508
- onActionError,
16509
- onActionEnd,
16510
- loading,
16511
- ...rest
16512
- } = props;
16513
- const defaultRef = useRef();
16514
- const ref = props.ref || defaultRef;
16515
- const [actionBoundToUIState] = useActionBoundToOneParam(action, uiState);
16516
- const {
16517
- loading: actionLoading
16518
- } = useActionStatus(actionBoundToUIState);
16519
- const executeAction = useExecuteAction(ref, {
16520
- errorEffect: actionErrorEffect
16521
- });
16522
-
16523
- // In this situation updating the ui state === calling associated action
16524
- // so cance/abort/error have to revert the ui state to the one before user interaction
16525
- // to show back the real state of the checkbox (not the one user tried to set)
16526
- useActionEvents(ref, {
16527
- onCancel: (e, reason) => {
16528
- if (reason === "blur_invalid") {
16529
- return;
16530
- }
16531
- uiStateController.resetUIState(e);
16532
- onCancel?.(e, reason);
16533
- },
16534
- onPrevented: onActionPrevented,
16535
- onAction: executeAction,
16536
- onStart: onActionStart,
16537
- onAbort: e => {
16538
- uiStateController.resetUIState(e);
16539
- onActionAbort?.(e);
16540
- },
16541
- onError: e => {
16542
- uiStateController.resetUIState(e);
16543
- onActionError?.(e);
16544
- },
16545
- onEnd: e => {
16546
- onActionEnd?.(e);
16547
- }
16548
- });
16549
- return jsx(InputCheckboxBasic, {
16550
- "data-action": actionBoundToUIState.name,
16551
- ...rest,
16552
- ref: ref,
16553
- loading: loading || actionLoading,
16554
- onChange: e => {
16555
- requestAction(e.target, actionBoundToUIState, {
16556
- event: e
16557
- });
16558
- onChange?.(e);
16559
- }
16560
- });
16561
- };
16562
- const InputCheckboxInsideForm = InputCheckboxBasic;
16556
+ const InputRadioWithAction = () => {
16557
+ throw new Error(`<Input type="radio" /> with an action make no sense. Use <RadioList action={something} /> instead`);
16558
+ };
16559
+ const InputRadioInsideForm = InputRadio;
16563
16560
 
16564
16561
  installImportMetaCss(import.meta);import.meta.css = /* css */`
16565
16562
  @layer navi {
16566
- .navi_radio {
16567
- --outline-offset: 1px;
16568
- --outline-width: 2px;
16569
- --width: 13px;
16570
- --height: 13px;
16563
+ .navi_input {
16564
+ --border-radius: 2px;
16565
+ --border-width: 1px;
16566
+ --outline-width: 1px;
16567
+ --outer-width: calc(var(--border-width) + var(--outline-width));
16571
16568
 
16569
+ /* Default */
16572
16570
  --outline-color: var(--navi-focus-outline-color);
16573
16571
  --loader-color: var(--navi-loader-color);
16574
16572
  --border-color: light-dark(#767676, #8e8e93);
16575
16573
  --background-color: white;
16576
- --color: light-dark(#4476ff, #3b82f6);
16577
- --radiomark-color: var(--color);
16578
- --border-color-checked: var(--color);
16579
-
16580
- --color-mix-light: white;
16581
- --color-mix-dark: black;
16582
- --color-mix: var(--color-mix-light);
16583
-
16574
+ --color: currentColor;
16575
+ --color-dimmed: color-mix(in srgb, currentColor 60%, transparent);
16576
+ --placeholder-color: var(--color-dimmed);
16584
16577
  /* Hover */
16585
- --border-color-hover: color-mix(in srgb, var(--border-color) 60%, black);
16586
- --border-color-hover-checked: color-mix(
16587
- in srgb,
16588
- var(--color) 80%,
16589
- var(--color-mix)
16590
- );
16591
- --radiomark-color-hover: color-mix(
16578
+ --border-color-hover: color-mix(in srgb, var(--border-color) 70%, black);
16579
+ --background-color-hover: color-mix(
16592
16580
  in srgb,
16593
- var(--color) 80%,
16594
- var(--color-mix)
16581
+ var(--background-color) 95%,
16582
+ black
16595
16583
  );
16584
+ --color-hover: var(--color);
16585
+ /* Active */
16586
+ --border-color-active: color-mix(in srgb, var(--border-color) 90%, black);
16596
16587
  /* Readonly */
16597
16588
  --border-color-readonly: color-mix(
16598
16589
  in srgb,
16599
- var(--border-color) 30%,
16600
- white
16590
+ var(--border-color) 45%,
16591
+ transparent
16601
16592
  );
16602
16593
  --background-color-readonly: var(--background-color);
16603
- --radiomark-color-readonly: grey;
16604
- --border-color-readonly-checked: #d3d3d3;
16605
- --background-color-readonly-checked: #d3d3d3;
16594
+ --color-readonly: var(--color-dimmed);
16606
16595
  /* Disabled */
16607
16596
  --border-color-disabled: var(--border-color-readonly);
16608
- --background-color-disabled: rgba(248, 248, 248, 0.7);
16609
- --radiomark-color-disabled: #d3d3d3;
16610
- --border-color-checked-disabled: #d3d3d3;
16611
- --background-color-disabled-checked: var(--background-color);
16612
- }
16613
-
16614
- .navi_radio[data-dark] {
16615
- --color-mix: var(--color-mix-dark);
16597
+ --background-color-disabled: color-mix(
16598
+ in srgb,
16599
+ var(--background-color) 95%,
16600
+ grey
16601
+ );
16602
+ --color-disabled: color-mix(in srgb, var(--color) 95%, grey);
16616
16603
  }
16617
16604
  }
16618
16605
 
16619
- .navi_radio {
16606
+ .navi_input {
16620
16607
  position: relative;
16621
- display: inline-flex;
16622
- box-sizing: content-box;
16623
- margin-top: 3px;
16624
- margin-right: 3px;
16625
- margin-left: 5px;
16608
+ box-sizing: border-box;
16609
+ width: fit-content;
16610
+ height: fit-content;
16611
+ flex-direction: inherit;
16612
+ border-radius: inherit;
16613
+ cursor: inherit;
16626
16614
 
16627
- --x-outline-offset: var(--outline-offset);
16628
16615
  --x-outline-width: var(--outline-width);
16616
+ --x-border-radius: var(--border-radius);
16629
16617
  --x-border-width: var(--border-width);
16630
- --x-width: var(--width);
16631
- --x-height: var(--height);
16618
+ --x-outer-width: calc(var(--x-border-width) + var(--x-outline-width));
16632
16619
  --x-outline-color: var(--outline-color);
16633
- --x-background-color: var(--background-color);
16634
16620
  --x-border-color: var(--border-color);
16621
+ --x-background-color: var(--background-color);
16635
16622
  --x-color: var(--color);
16636
- --x-radiomark-color: var(--radiomark-color);
16637
- }
16638
- .navi_radio .navi_native_field {
16639
- position: absolute;
16640
- inset: 0;
16641
- margin: 0;
16642
- padding: 0;
16643
- opacity: 0;
16644
- cursor: inherit;
16645
- }
16646
- .navi_radio .navi_radio_field {
16647
- display: inline-flex;
16648
- box-sizing: border-box;
16649
- width: var(--x-width);
16650
- height: var(--x-height);
16651
- align-items: center;
16652
- justify-content: center;
16653
- border-radius: 50%;
16654
- outline-width: var(--x-outline-width);
16655
- outline-style: none;
16656
- outline-color: var(--x-outline-color);
16657
- outline-offset: var(--x-outline-offset);
16658
- }
16659
- .navi_radio_field svg {
16660
- overflow: visible;
16661
- }
16662
- .navi_radio_border {
16663
- fill: var(--x-background-color);
16664
- stroke: var(--x-border-color);
16665
- }
16666
- .navi_radio_marker {
16667
- width: 100%;
16668
- height: 100%;
16669
- opacity: 0;
16670
- fill: var(--x-radiomark-color);
16671
- transform: scale(0.3);
16672
- transform-origin: center;
16673
- pointer-events: none;
16674
- }
16675
- .navi_radio_dashed_border {
16676
- display: none;
16677
- }
16678
- .navi_radio[data-transition] .navi_radio_marker {
16679
- transition: all 0.15s ease;
16680
- }
16681
- .navi_radio[data-transition] .navi_radio_dashed_border {
16682
- transition: all 0.15s ease;
16683
- }
16684
- .navi_radio[data-transition] .navi_radio_border {
16685
- transition: all 0.15s ease;
16623
+ --x-placeholder-color: var(--placeholder-color);
16686
16624
  }
16687
16625
 
16688
- /* Focus */
16689
- .navi_radio[data-focus-visible] .navi_radio_field {
16626
+ .navi_input .navi_native_input {
16627
+ box-sizing: border-box;
16628
+ padding-top: var(--padding-top, var(--padding-y, var(--padding, 1px)));
16629
+ padding-right: var(--padding-right, var(--padding-x, var(--padding, 2px)));
16630
+ padding-bottom: var(
16631
+ --padding-bottom,
16632
+ var(--padding-y, var(--padding, 1px))
16633
+ );
16634
+ padding-left: var(--padding-left, var(--padding-x, var(--padding, 2px)));
16635
+ color: var(--x-color);
16636
+ background-color: var(--x-background-color);
16637
+ border-width: var(--x-outer-width);
16638
+ border-width: var(--x-outer-width);
16639
+ border-style: solid;
16640
+ border-color: transparent;
16641
+ border-radius: var(--x-border-radius);
16642
+ outline-width: var(--x-border-width);
16690
16643
  outline-style: solid;
16644
+ outline-color: var(--x-border-color);
16645
+ outline-offset: calc(-1 * (var(--x-border-width)));
16691
16646
  }
16692
- /* Hover */
16693
- .navi_radio[data-hover] {
16694
- --x-border-color: var(--border-color-hover);
16695
- --x-radiomark-color: var(--radiomark-color-hover);
16696
- }
16697
- /* Checked */
16698
- .navi_radio[data-checked] {
16699
- --x-border-color: var(--border-color-checked);
16700
- }
16701
- .navi_radio[data-checked] .navi_radio_marker {
16702
- opacity: 1;
16703
- transform: scale(1);
16647
+ .navi_input .navi_native_input::placeholder {
16648
+ color: var(--x-placeholder-color);
16704
16649
  }
16705
- .navi_radio[data-hover][data-checked] {
16706
- --x-border-color: var(--border-color-hover-checked);
16650
+ .navi_input .navi_native_input:-internal-autofill-selected {
16651
+ /* Webkit is putting some nasty styles after automplete that look as follow */
16652
+ /* input:-internal-autofill-selected { color: FieldText !important; } */
16653
+ /* Fortunately we can override it as follow */
16654
+ -webkit-text-fill-color: var(--x-color) !important;
16707
16655
  }
16708
16656
  /* Readonly */
16709
- .navi_radio[data-readonly] {
16710
- --x-background-color: var(--background-color-readonly);
16657
+ .navi_input[data-readonly] {
16711
16658
  --x-border-color: var(--border-color-readonly);
16712
- --x-radiomark-color: var(--radiomark-color-readonly);
16713
- }
16714
- .navi_radio[data-readonly] .navi_radio_dashed_border {
16715
- display: none;
16659
+ --x-background-color: var(--background-color-readonly);
16660
+ --x-color: var(--color-readonly);
16716
16661
  }
16717
- .navi_radio[data-readonly][data-checked] {
16718
- --x-background-color: var(--background-color-readonly-checked);
16719
- --x-border-color: var(--border-color-readonly-checked);
16720
- --x-radiomark-color: var(--radiomark-color-readonly);
16662
+ /* Focus */
16663
+ .navi_input[data-focus] .navi_native_input,
16664
+ .navi_input[data-focus-visible] .navi_native_input {
16665
+ outline-width: var(--x-outer-width);
16666
+ outline-offset: calc(-1 * var(--x-outer-width));
16667
+ --x-border-color: var(--x-outline-color);
16721
16668
  }
16722
16669
  /* Disabled */
16723
- .navi_radio[data-disabled] {
16724
- --x-background-color: var(--background-color-disabled);
16670
+ .navi_input[data-disabled] {
16725
16671
  --x-border-color: var(--border-color-disabled);
16726
- --x-radiomark-color: var(--radiomark-color-disabled);
16672
+ --x-background-color: var(--background-color-disabled);
16673
+ --x-color: var(--color-disabled);
16727
16674
  }
16728
- .navi_radio[data-disabled][data-checked] {
16729
- --x-border-color: var(--border-color-disabled);
16730
- --x-radiomark-color: var(--radiomark-color-disabled);
16675
+ /* Callout (info, warning, error) */
16676
+ .navi_input[data-callout] {
16677
+ --x-border-color: var(--callout-color);
16731
16678
  }
16732
16679
  `;
16733
- const InputRadio = props => {
16734
- const {
16735
- value = "on"
16736
- } = props;
16737
- const uiStateController = useUIStateController(props, "radio", {
16738
- statePropName: "checked",
16739
- fallbackState: false,
16740
- getStateFromProp: checked => checked ? value : undefined,
16741
- getPropFromState: Boolean
16742
- });
16680
+ const InputTextual = props => {
16681
+ const uiStateController = useUIStateController(props, "input");
16743
16682
  const uiState = useUIState(uiStateController);
16744
- const radio = renderActionableComponent(props, {
16745
- Basic: InputRadioBasic,
16746
- WithAction: InputRadioWithAction,
16747
- InsideForm: InputRadioInsideForm
16683
+ const input = renderActionableComponent(props, {
16684
+ Basic: InputTextualBasic,
16685
+ WithAction: InputTextualWithAction,
16686
+ InsideForm: InputTextualInsideForm
16748
16687
  });
16749
16688
  return jsx(UIStateControllerContext.Provider, {
16750
16689
  value: uiStateController,
16751
16690
  children: jsx(UIStateContext.Provider, {
16752
16691
  value: uiState,
16753
- children: radio
16692
+ children: input
16754
16693
  })
16755
16694
  });
16756
16695
  };
16757
- const RadioStyleCSSVars = {
16696
+ const InputStyleCSSVars = {
16758
16697
  "outlineWidth": "--outline-width",
16759
16698
  "borderWidth": "--border-width",
16760
16699
  "borderRadius": "--border-radius",
16700
+ "paddingTop": "--padding-top",
16701
+ "paddingRight": "--padding-right",
16702
+ "paddingBottom": "--padding-bottom",
16703
+ "paddingLeft": "--padding-left",
16761
16704
  "backgroundColor": "--background-color",
16762
16705
  "borderColor": "--border-color",
16763
16706
  "color": "--color",
@@ -16780,814 +16723,728 @@ const RadioStyleCSSVars = {
16780
16723
  color: "--color-disabled"
16781
16724
  }
16782
16725
  };
16783
- const RadioPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
16784
- const RadioPseudoElements = ["::-navi-loader", "::-navi-radiomark"];
16785
- const InputRadioBasic = props => {
16786
- const contextName = useContext(FieldNameContext);
16726
+ const InputPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
16727
+ const InputPseudoElements = ["::-navi-loader"];
16728
+ const InputTextualBasic = props => {
16787
16729
  const contextReadOnly = useContext(ReadOnlyContext);
16788
16730
  const contextDisabled = useContext(DisabledContext);
16789
- const contextRequired = useContext(RequiredContext);
16790
16731
  const contextLoading = useContext(LoadingContext);
16791
16732
  const contextLoadingElement = useContext(LoadingElementContext);
16733
+ const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
16792
16734
  const uiStateController = useContext(UIStateControllerContext);
16793
16735
  const uiState = useContext(UIStateContext);
16794
- const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
16795
- const reportDisabledOnLabel = useContext(ReportDisabledOnLabelContext);
16796
16736
  const {
16797
- /* eslint-disable no-unused-vars */
16798
16737
  type,
16799
- /* eslint-enable no-unused-vars */
16800
-
16801
- name,
16738
+ onInput,
16802
16739
  readOnly,
16803
16740
  disabled,
16804
- required,
16741
+ constraints = [],
16805
16742
  loading,
16806
16743
  autoFocus,
16807
- constraints = [],
16808
- onClick,
16809
- onInput,
16810
- color,
16744
+ autoFocusVisible,
16745
+ autoSelect,
16811
16746
  ...rest
16812
16747
  } = props;
16813
16748
  const defaultRef = useRef();
16814
16749
  const ref = props.ref || defaultRef;
16815
- const innerName = name || contextName;
16816
- const innerDisabled = disabled || contextDisabled;
16817
- const innerRequired = required || contextRequired;
16750
+ const innerValue = type === "datetime-local" ? convertToLocalTimezone(uiState) : uiState;
16818
16751
  const innerLoading = loading || contextLoading && contextLoadingElement === ref.current;
16819
16752
  const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
16753
+ const innerDisabled = disabled || contextDisabled;
16754
+ // infom any <label> parent of our readOnly state
16820
16755
  reportReadOnlyOnLabel?.(innerReadOnly);
16821
- reportDisabledOnLabel?.(innerDisabled);
16822
- useAutoFocus(ref, autoFocus);
16756
+ useAutoFocus(ref, autoFocus, {
16757
+ autoFocusVisible,
16758
+ autoSelect
16759
+ });
16823
16760
  useConstraints(ref, constraints);
16824
- const checked = Boolean(uiState);
16825
- // we must first dispatch an event to inform all other radios they where unchecked
16826
- // this way each other radio uiStateController knows thery are unchecked
16827
- // we do this on "input"
16828
- // but also when we are becoming checked from outside (hence the useLayoutEffect)
16829
- const updateOtherRadiosInGroup = () => {
16830
- const thisRadio = ref.current;
16831
- const radioList = thisRadio.closest("[data-radio-list]");
16832
- if (!radioList) {
16833
- return;
16834
- }
16835
- const radioInputs = radioList.querySelectorAll(`input[type="radio"][name="${thisRadio.name}"]`);
16836
- for (const radioInput of radioInputs) {
16837
- if (radioInput === thisRadio) {
16838
- continue;
16761
+ const innerOnInput = useStableCallback(onInput);
16762
+ const renderInput = inputProps => {
16763
+ return jsx(Box, {
16764
+ ...inputProps,
16765
+ as: "input",
16766
+ ref: ref,
16767
+ type: type,
16768
+ "data-value": uiState,
16769
+ value: innerValue,
16770
+ onInput: e => {
16771
+ let inputValue;
16772
+ if (type === "number") {
16773
+ inputValue = e.target.valueAsNumber;
16774
+ } else if (type === "datetime-local") {
16775
+ inputValue = convertToUTCTimezone(e.target.value);
16776
+ } else {
16777
+ inputValue = e.target.value;
16778
+ }
16779
+ uiStateController.setUIState(inputValue, e);
16780
+ innerOnInput?.(e);
16781
+ },
16782
+ onresetuistate: e => {
16783
+ uiStateController.resetUIState(e);
16784
+ },
16785
+ onsetuistate: e => {
16786
+ uiStateController.setUIState(e.detail.value, e);
16839
16787
  }
16840
- radioInput.dispatchEvent(new CustomEvent("setuistate", {
16841
- detail: false
16842
- }));
16843
- }
16788
+ // style management
16789
+ ,
16790
+ baseClassName: "navi_native_input"
16791
+ });
16844
16792
  };
16845
- useLayoutEffect(() => {
16846
- if (checked) {
16847
- updateOtherRadiosInGroup();
16848
- }
16849
- }, [checked]);
16850
- const innerOnInput = useStableCallback(e => {
16851
- const radio = e.target;
16852
- const radioIsChecked = radio.checked;
16853
- if (radioIsChecked) {
16854
- updateOtherRadiosInGroup();
16855
- }
16856
- uiStateController.setUIState(radioIsChecked, e);
16857
- onInput?.(e);
16858
- });
16859
- const innerOnClick = useStableCallback(e => {
16860
- if (innerReadOnly) {
16861
- e.preventDefault();
16862
- }
16863
- onClick?.(e);
16864
- });
16865
- const renderRadio = radioProps => jsx(Box, {
16866
- ...radioProps,
16867
- as: "input",
16868
- ref: ref,
16869
- type: "radio",
16870
- name: innerName,
16871
- checked: checked,
16872
- disabled: innerDisabled,
16873
- required: innerRequired,
16874
- baseClassName: "navi_native_field",
16875
- "data-callout-arrow-x": "center",
16876
- onClick: innerOnClick,
16877
- onInput: innerOnInput,
16878
- onresetuistate: e => {
16879
- uiStateController.resetUIState(e);
16880
- },
16881
- onsetuistate: e => {
16882
- uiStateController.setUIState(e.detail.value, e);
16883
- }
16884
- });
16885
- const renderRadioMemoized = useCallback(renderRadio, [innerName, checked, innerRequired]);
16886
- useLayoutEffect(() => {
16887
- const naviRadio = ref.current;
16888
- const luminance = resolveColorLuminance("var(--color)", naviRadio);
16889
- if (luminance < 0.3) {
16890
- naviRadio.setAttribute("data-dark", "");
16891
- } else {
16892
- naviRadio.removeAttribute("data-dark");
16893
- }
16894
- }, [color]);
16793
+ const renderInputMemoized = useCallback(renderInput, [type, uiState, innerValue, innerOnInput]);
16895
16794
  return jsxs(Box, {
16896
16795
  as: "span",
16897
- ...rest,
16898
- ref: ref,
16899
- baseClassName: "navi_radio",
16900
- pseudoStateSelector: ".navi_native_field",
16901
- styleCSSVars: RadioStyleCSSVars,
16902
- pseudoClasses: RadioPseudoClasses,
16903
- pseudoElements: RadioPseudoElements,
16796
+ box: true,
16797
+ baseClassName: "navi_input",
16798
+ styleCSSVars: InputStyleCSSVars,
16799
+ pseudoStateSelector: ".navi_native_input",
16800
+ visualSelector: ".navi_native_input",
16904
16801
  basePseudoState: {
16905
16802
  ":read-only": innerReadOnly,
16906
16803
  ":disabled": innerDisabled,
16907
16804
  ":-navi-loading": innerLoading
16908
16805
  },
16909
- color: color,
16806
+ pseudoClasses: InputPseudoClasses,
16807
+ pseudoElements: InputPseudoElements,
16910
16808
  hasChildFunction: true,
16809
+ ...rest,
16911
16810
  children: [jsx(LoaderBackground, {
16912
16811
  loading: innerLoading,
16913
- inset: -1,
16914
- targetSelector: ".navi_radio_field",
16915
- color: "var(--loader-color)"
16916
- }), renderRadioMemoized, jsx("span", {
16917
- className: "navi_radio_field",
16918
- children: jsxs("svg", {
16919
- viewBox: "0 0 12 12",
16920
- "aria-hidden": "true",
16921
- preserveAspectRatio: "xMidYMid meet",
16922
- children: [jsx("circle", {
16923
- className: "navi_radio_border",
16924
- cx: "6",
16925
- cy: "6",
16926
- r: "5.5",
16927
- strokeWidth: "1"
16928
- }), jsx("circle", {
16929
- className: "navi_radio_dashed_border",
16930
- cx: "6",
16931
- cy: "6",
16932
- r: "5.5",
16933
- strokeWidth: "1",
16934
- strokeDasharray: "2.16 2.16",
16935
- strokeDashoffset: "0"
16936
- }), jsx("circle", {
16937
- className: "navi_radio_marker",
16938
- cx: "6",
16939
- cy: "6",
16940
- r: "3.5"
16941
- })]
16942
- })
16943
- })]
16812
+ color: "var(--loader-color)",
16813
+ inset: -1
16814
+ }), renderInputMemoized]
16944
16815
  });
16945
16816
  };
16946
- const InputRadioWithAction = () => {
16947
- throw new Error(`<Input type="radio" /> with an action make no sense. Use <RadioList action={something} /> instead`);
16817
+ const InputTextualWithAction = props => {
16818
+ const uiState = useContext(UIStateContext);
16819
+ const {
16820
+ action,
16821
+ loading,
16822
+ onCancel,
16823
+ onActionPrevented,
16824
+ onActionStart,
16825
+ onActionError,
16826
+ onActionEnd,
16827
+ cancelOnBlurInvalid,
16828
+ cancelOnEscape,
16829
+ actionErrorEffect,
16830
+ ...rest
16831
+ } = props;
16832
+ const defaultRef = useRef();
16833
+ const ref = props.ref || defaultRef;
16834
+ const [boundAction] = useActionBoundToOneParam(action, uiState);
16835
+ const {
16836
+ loading: actionLoading
16837
+ } = useActionStatus(boundAction);
16838
+ const executeAction = useExecuteAction(ref, {
16839
+ errorEffect: actionErrorEffect
16840
+ });
16841
+ // here updating the input won't call the associated action
16842
+ // (user have to blur or press enter for this to happen)
16843
+ // so we can keep the ui state on cancel/abort/error and let user decide
16844
+ // to update ui state or retry via blur/enter as is
16845
+ useActionEvents(ref, {
16846
+ onCancel: (e, reason) => {
16847
+ if (reason.startsWith("blur_invalid")) {
16848
+ if (!cancelOnBlurInvalid) {
16849
+ return;
16850
+ }
16851
+ if (
16852
+ // error prevent cancellation until the user closes it (or something closes it)
16853
+ e.detail.failedConstraintInfo.level === "error" && e.detail.failedConstraintInfo.reportStatus !== "closed") {
16854
+ return;
16855
+ }
16856
+ }
16857
+ if (reason === "escape_key") {
16858
+ if (!cancelOnEscape) {
16859
+ return;
16860
+ }
16861
+ }
16862
+ onCancel?.(e, reason);
16863
+ },
16864
+ onRequested: e => {
16865
+ forwardActionRequested(e, boundAction);
16866
+ },
16867
+ onPrevented: onActionPrevented,
16868
+ onAction: executeAction,
16869
+ onStart: onActionStart,
16870
+ onError: onActionError,
16871
+ onEnd: onActionEnd
16872
+ });
16873
+ return jsx(InputTextualBasic, {
16874
+ "data-action": boundAction.name,
16875
+ ...rest,
16876
+ ref: ref,
16877
+ loading: loading || actionLoading
16878
+ });
16879
+ };
16880
+ const InputTextualInsideForm = props => {
16881
+ const {
16882
+ // We destructure formContext to avoid passing it to the underlying input element
16883
+ // eslint-disable-next-line no-unused-vars
16884
+ formContext,
16885
+ ...rest
16886
+ } = props;
16887
+ return jsx(InputTextualBasic, {
16888
+ ...rest
16889
+ });
16948
16890
  };
16949
- const InputRadioInsideForm = InputRadio;
16950
-
16951
- installImportMetaCss(import.meta);import.meta.css = /* css */`
16952
- @layer navi {
16953
- .navi_input {
16954
- --border-radius: 2px;
16955
- --border-width: 1px;
16956
- --outline-width: 1px;
16957
- --outer-width: calc(var(--border-width) + var(--outline-width));
16958
16891
 
16959
- /* Default */
16960
- --outline-color: var(--navi-focus-outline-color);
16961
- --loader-color: var(--navi-loader-color);
16962
- --border-color: light-dark(#767676, #8e8e93);
16963
- --background-color: white;
16964
- --color: currentColor;
16965
- --color-dimmed: color-mix(in srgb, currentColor 60%, transparent);
16966
- --placeholder-color: var(--color-dimmed);
16967
- /* Hover */
16968
- --border-color-hover: color-mix(in srgb, var(--border-color) 70%, black);
16969
- --background-color-hover: color-mix(
16970
- in srgb,
16971
- var(--background-color) 95%,
16972
- black
16973
- );
16974
- --color-hover: var(--color);
16975
- /* Active */
16976
- --border-color-active: color-mix(in srgb, var(--border-color) 90%, black);
16977
- /* Readonly */
16978
- --border-color-readonly: color-mix(
16979
- in srgb,
16980
- var(--border-color) 45%,
16981
- transparent
16982
- );
16983
- --background-color-readonly: var(--background-color);
16984
- --color-readonly: var(--color-dimmed);
16985
- /* Disabled */
16986
- --border-color-disabled: var(--border-color-readonly);
16987
- --background-color-disabled: color-mix(
16988
- in srgb,
16989
- var(--background-color) 95%,
16990
- grey
16991
- );
16992
- --color-disabled: color-mix(in srgb, var(--color) 95%, grey);
16993
- }
16892
+ // As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
16893
+ // datetime-local does not support timezones
16894
+ const convertToLocalTimezone = dateTimeString => {
16895
+ const date = new Date(dateTimeString);
16896
+ // Check if the date is valid
16897
+ if (isNaN(date.getTime())) {
16898
+ return dateTimeString;
16994
16899
  }
16995
16900
 
16996
- .navi_input {
16997
- position: relative;
16998
- box-sizing: border-box;
16999
- width: fit-content;
17000
- height: fit-content;
17001
- flex-direction: inherit;
17002
- border-radius: inherit;
17003
- cursor: inherit;
17004
-
17005
- --x-outline-width: var(--outline-width);
17006
- --x-border-radius: var(--border-radius);
17007
- --x-border-width: var(--border-width);
17008
- --x-outer-width: calc(var(--x-border-width) + var(--x-outline-width));
17009
- --x-outline-color: var(--outline-color);
17010
- --x-border-color: var(--border-color);
17011
- --x-background-color: var(--background-color);
17012
- --x-color: var(--color);
17013
- --x-placeholder-color: var(--placeholder-color);
16901
+ // Format to YYYY-MM-DDThh:mm:ss
16902
+ const year = date.getFullYear();
16903
+ const month = String(date.getMonth() + 1).padStart(2, "0");
16904
+ const day = String(date.getDate()).padStart(2, "0");
16905
+ const hours = String(date.getHours()).padStart(2, "0");
16906
+ const minutes = String(date.getMinutes()).padStart(2, "0");
16907
+ const seconds = String(date.getSeconds()).padStart(2, "0");
16908
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
16909
+ };
16910
+ /**
16911
+ * Converts a datetime string without timezone (local time) to UTC format with 'Z' notation
16912
+ *
16913
+ * @param {string} localDateTimeString - Local datetime string without timezone (e.g., "2023-07-15T14:30:00")
16914
+ * @returns {string} Datetime string in UTC with 'Z' notation (e.g., "2023-07-15T12:30:00Z")
16915
+ */
16916
+ const convertToUTCTimezone = localDateTimeString => {
16917
+ if (!localDateTimeString) {
16918
+ return localDateTimeString;
17014
16919
  }
16920
+ try {
16921
+ // Create a Date object using the local time string
16922
+ // The browser will interpret this as local timezone
16923
+ const localDate = new Date(localDateTimeString);
17015
16924
 
17016
- .navi_input .navi_native_input {
17017
- box-sizing: border-box;
17018
- padding-top: var(--padding-top, var(--padding-y, var(--padding, 1px)));
17019
- padding-right: var(--padding-right, var(--padding-x, var(--padding, 2px)));
17020
- padding-bottom: var(
17021
- --padding-bottom,
17022
- var(--padding-y, var(--padding, 1px))
17023
- );
17024
- padding-left: var(--padding-left, var(--padding-x, var(--padding, 2px)));
17025
- color: var(--x-color);
17026
- background-color: var(--x-background-color);
17027
- border-width: var(--x-outer-width);
17028
- border-width: var(--x-outer-width);
17029
- border-style: solid;
17030
- border-color: transparent;
17031
- border-radius: var(--x-border-radius);
17032
- outline-width: var(--x-border-width);
17033
- outline-style: solid;
17034
- outline-color: var(--x-border-color);
17035
- outline-offset: calc(-1 * (var(--x-border-width)));
17036
- }
17037
- .navi_input .navi_native_input::placeholder {
17038
- color: var(--x-placeholder-color);
17039
- }
17040
- .navi_input .navi_native_input:-internal-autofill-selected {
17041
- /* Webkit is putting some nasty styles after automplete that look as follow */
17042
- /* input:-internal-autofill-selected { color: FieldText !important; } */
17043
- /* Fortunately we can override it as follow */
17044
- -webkit-text-fill-color: var(--x-color) !important;
17045
- }
17046
- /* Readonly */
17047
- .navi_input[data-readonly] {
17048
- --x-border-color: var(--border-color-readonly);
17049
- --x-background-color: var(--background-color-readonly);
17050
- --x-color: var(--color-readonly);
17051
- }
17052
- /* Focus */
17053
- .navi_input[data-focus] .navi_native_input,
17054
- .navi_input[data-focus-visible] .navi_native_input {
17055
- outline-width: var(--x-outer-width);
17056
- outline-offset: calc(-1 * var(--x-outer-width));
17057
- --x-border-color: var(--x-outline-color);
16925
+ // Check if the date is valid
16926
+ if (isNaN(localDate.getTime())) {
16927
+ return localDateTimeString;
16928
+ }
16929
+
16930
+ // Convert to UTC ISO string
16931
+ const utcString = localDate.toISOString();
16932
+
16933
+ // Return the UTC string (which includes the 'Z' notation)
16934
+ return utcString;
16935
+ } catch (error) {
16936
+ console.error("Error converting local datetime to UTC:", error);
16937
+ return localDateTimeString;
17058
16938
  }
17059
- /* Disabled */
17060
- .navi_input[data-disabled] {
17061
- --x-border-color: var(--border-color-disabled);
17062
- --x-background-color: var(--background-color-disabled);
17063
- --x-color: var(--color-disabled);
16939
+ };
16940
+
16941
+ const Input = forwardRef((props, ref) => {
16942
+ const {
16943
+ type
16944
+ } = props;
16945
+ if (type === "radio") {
16946
+ return jsx(InputRadio, {
16947
+ ...props,
16948
+ ref: ref
16949
+ });
17064
16950
  }
17065
- /* Callout (info, warning, error) */
17066
- .navi_input[data-callout] {
17067
- --x-border-color: var(--callout-color);
16951
+ if (type === "checkbox") {
16952
+ return jsx(InputCheckbox, {
16953
+ ...props,
16954
+ ref: ref
16955
+ });
17068
16956
  }
17069
- `;
17070
- const InputTextual = props => {
17071
- const uiStateController = useUIStateController(props, "input");
17072
- const uiState = useUIState(uiStateController);
17073
- const input = renderActionableComponent(props, {
17074
- Basic: InputTextualBasic,
17075
- WithAction: InputTextualWithAction,
17076
- InsideForm: InputTextualInsideForm
17077
- });
17078
- return jsx(UIStateControllerContext.Provider, {
17079
- value: uiStateController,
17080
- children: jsx(UIStateContext.Provider, {
17081
- value: uiState,
17082
- children: input
17083
- })
16957
+ return jsx(InputTextual, {
16958
+ ...props,
16959
+ ref: ref
17084
16960
  });
17085
- };
17086
- const InputStyleCSSVars = {
17087
- "outlineWidth": "--outline-width",
17088
- "borderWidth": "--border-width",
17089
- "borderRadius": "--border-radius",
17090
- "paddingTop": "--padding-top",
17091
- "paddingRight": "--padding-right",
17092
- "paddingBottom": "--padding-bottom",
17093
- "paddingLeft": "--padding-left",
17094
- "backgroundColor": "--background-color",
17095
- "borderColor": "--border-color",
17096
- "color": "--color",
17097
- ":hover": {
17098
- backgroundColor: "--background-color-hover",
17099
- borderColor: "--border-color-hover",
17100
- color: "--color-hover"
17101
- },
17102
- ":active": {
17103
- borderColor: "--border-color-active"
17104
- },
17105
- ":read-only": {
17106
- backgroundColor: "--background-color-readonly",
17107
- borderColor: "--border-color-readonly",
17108
- color: "--color-readonly"
17109
- },
17110
- ":disabled": {
17111
- backgroundColor: "--background-color-disabled",
17112
- borderColor: "--border-color-disabled",
17113
- color: "--color-disabled"
16961
+ });
16962
+
16963
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
16964
+ .navi_editable_wrapper {
16965
+ position: absolute;
16966
+ inset: 0;
17114
16967
  }
16968
+ `;
16969
+ const useEditionController = () => {
16970
+ const [editing, editingSetter] = useState(null);
16971
+ const startEditing = useCallback(event => {
16972
+ editingSetter(current => {
16973
+ return current || {
16974
+ event
16975
+ };
16976
+ });
16977
+ }, []);
16978
+ const stopEditing = useCallback(() => {
16979
+ editingSetter(null);
16980
+ }, []);
16981
+ const prevEditingRef = useRef(editing);
16982
+ const editionJustEnded = prevEditingRef.current && !editing;
16983
+ prevEditingRef.current = editing;
16984
+ return {
16985
+ editing,
16986
+ startEditing,
16987
+ stopEditing,
16988
+ editionJustEnded
16989
+ };
17115
16990
  };
17116
- const InputPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
17117
- const InputPseudoElements = ["::-navi-loader"];
17118
- const InputTextualBasic = props => {
17119
- const contextReadOnly = useContext(ReadOnlyContext);
17120
- const contextDisabled = useContext(DisabledContext);
17121
- const contextLoading = useContext(LoadingContext);
17122
- const contextLoadingElement = useContext(LoadingElementContext);
17123
- const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
17124
- const uiStateController = useContext(UIStateControllerContext);
17125
- const uiState = useContext(UIStateContext);
17126
- const {
16991
+ const Editable = forwardRef((props, ref) => {
16992
+ let {
16993
+ children,
16994
+ action,
16995
+ editing,
16996
+ name,
16997
+ value,
16998
+ valueSignal,
16999
+ onEditEnd,
17000
+ constraints,
17127
17001
  type,
17128
- onInput,
17002
+ required,
17003
+ readOnly,
17004
+ min,
17005
+ max,
17006
+ step,
17007
+ minLength,
17008
+ maxLength,
17009
+ pattern,
17010
+ wrapperProps,
17011
+ autoSelect = true,
17012
+ width,
17013
+ height,
17014
+ ...rest
17015
+ } = props;
17016
+ const innerRef = useRef();
17017
+ useImperativeHandle(ref, () => innerRef.current);
17018
+ if (valueSignal) {
17019
+ value = valueSignal.value;
17020
+ }
17021
+ const editingPreviousRef = useRef(editing);
17022
+ const valueWhenEditStartRef = useRef(editing ? value : undefined);
17023
+ if (editingPreviousRef.current !== editing) {
17024
+ if (editing) {
17025
+ valueWhenEditStartRef.current = value; // Always store the external value
17026
+ }
17027
+ editingPreviousRef.current = editing;
17028
+ }
17029
+
17030
+ // Simulate typing the initial value when editing starts with a custom value
17031
+ useLayoutEffect(() => {
17032
+ if (!editing) {
17033
+ return;
17034
+ }
17035
+ const editingEvent = editing.event;
17036
+ if (!editingEvent) {
17037
+ return;
17038
+ }
17039
+ const editingEventInitialValue = editingEvent.detail?.initialValue;
17040
+ if (editingEventInitialValue === undefined) {
17041
+ return;
17042
+ }
17043
+ const input = innerRef.current;
17044
+ input.value = editingEventInitialValue;
17045
+ input.dispatchEvent(new CustomEvent("input", {
17046
+ bubbles: false
17047
+ }));
17048
+ }, [editing]);
17049
+ const input = jsx(Input, {
17050
+ ref: innerRef,
17051
+ ...rest,
17052
+ type: type,
17053
+ name: name,
17054
+ value: value,
17055
+ valueSignal: valueSignal,
17056
+ autoFocus: true,
17057
+ autoFocusVisible: true,
17058
+ autoSelect: autoSelect,
17059
+ cancelOnEscape: true,
17060
+ cancelOnBlurInvalid: true,
17061
+ constraints: constraints,
17062
+ required: required,
17063
+ readOnly: readOnly,
17064
+ min: min,
17065
+ max: max,
17066
+ step: step,
17067
+ minLength: minLength,
17068
+ maxLength: maxLength,
17069
+ pattern: pattern,
17070
+ width: width,
17071
+ height: height,
17072
+ onCancel: e => {
17073
+ if (valueSignal) {
17074
+ valueSignal.value = valueWhenEditStartRef.current;
17075
+ }
17076
+ onEditEnd({
17077
+ cancelled: true,
17078
+ event: e
17079
+ });
17080
+ },
17081
+ onBlur: e => {
17082
+ const value = type === "number" ? e.target.valueAsNumber : e.target.value;
17083
+ const valueWhenEditStart = valueWhenEditStartRef.current;
17084
+ if (value === valueWhenEditStart) {
17085
+ onEditEnd({
17086
+ cancelled: true,
17087
+ event: e
17088
+ });
17089
+ return;
17090
+ }
17091
+ },
17092
+ action: action || (() => {}),
17093
+ onActionEnd: e => {
17094
+ onEditEnd({
17095
+ success: true,
17096
+ event: e
17097
+ });
17098
+ }
17099
+ });
17100
+ return jsxs(Fragment, {
17101
+ children: [children || jsx("span", {
17102
+ children: value
17103
+ }), editing && jsx("div", {
17104
+ ...wrapperProps,
17105
+ className: ["navi_editable_wrapper", ...(wrapperProps?.className || "").split(" ")].join(" "),
17106
+ children: input
17107
+ })]
17108
+ });
17109
+ });
17110
+
17111
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
17112
+ @layer navi {
17113
+ .navi_checkbox_list {
17114
+ display: flex;
17115
+ flex-direction: column;
17116
+ }
17117
+ }
17118
+ `;
17119
+ const CheckboxList = forwardRef((props, ref) => {
17120
+ const uiStateController = useUIGroupStateController(props, "checkbox_list", {
17121
+ childComponentType: "checkbox",
17122
+ aggregateChildStates: childUIStateControllers => {
17123
+ const values = [];
17124
+ for (const childUIStateController of childUIStateControllers) {
17125
+ if (childUIStateController.uiState) {
17126
+ values.push(childUIStateController.uiState);
17127
+ }
17128
+ }
17129
+ return values.length === 0 ? undefined : values;
17130
+ }
17131
+ });
17132
+ const uiState = useUIState(uiStateController);
17133
+ const checkboxList = renderActionableComponent(props, ref);
17134
+ return jsx(UIStateControllerContext.Provider, {
17135
+ value: uiStateController,
17136
+ children: jsx(UIStateContext.Provider, {
17137
+ value: uiState,
17138
+ children: checkboxList
17139
+ })
17140
+ });
17141
+ });
17142
+ const Checkbox = InputCheckbox;
17143
+ const CheckboxListBasic = forwardRef((props, ref) => {
17144
+ const contextReadOnly = useContext(ReadOnlyContext);
17145
+ const contextDisabled = useContext(DisabledContext);
17146
+ const contextLoading = useContext(LoadingContext);
17147
+ const uiStateController = useContext(UIStateControllerContext);
17148
+ const {
17149
+ name,
17129
17150
  readOnly,
17130
17151
  disabled,
17131
- constraints = [],
17152
+ required,
17132
17153
  loading,
17133
- autoFocus,
17134
- autoFocusVisible,
17135
- autoSelect,
17154
+ children,
17136
17155
  ...rest
17137
17156
  } = props;
17138
- const defaultRef = useRef();
17139
- const ref = props.ref || defaultRef;
17140
- const innerValue = type === "datetime-local" ? convertToLocalTimezone(uiState) : uiState;
17141
- const innerLoading = loading || contextLoading && contextLoadingElement === ref.current;
17157
+ const innerRef = useRef();
17158
+ useImperativeHandle(ref, () => innerRef.current);
17159
+ const innerLoading = loading || contextLoading;
17142
17160
  const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
17143
17161
  const innerDisabled = disabled || contextDisabled;
17144
- // infom any <label> parent of our readOnly state
17145
- reportReadOnlyOnLabel?.(innerReadOnly);
17146
- useAutoFocus(ref, autoFocus, {
17147
- autoFocusVisible,
17148
- autoSelect
17149
- });
17150
- useConstraints(ref, constraints);
17151
- const innerOnInput = useStableCallback(onInput);
17152
- const renderInput = inputProps => {
17153
- return jsx(Box, {
17154
- ...inputProps,
17155
- as: "input",
17156
- ref: ref,
17157
- type: type,
17158
- "data-value": uiState,
17159
- value: innerValue,
17160
- onInput: e => {
17161
- let inputValue;
17162
- if (type === "number") {
17163
- inputValue = e.target.valueAsNumber;
17164
- } else if (type === "datetime-local") {
17165
- inputValue = convertToUTCTimezone(e.target.value);
17166
- } else {
17167
- inputValue = e.target.value;
17168
- }
17169
- uiStateController.setUIState(inputValue, e);
17170
- innerOnInput?.(e);
17171
- },
17172
- onresetuistate: e => {
17173
- uiStateController.resetUIState(e);
17174
- },
17175
- onsetuistate: e => {
17176
- uiStateController.setUIState(e.detail.value, e);
17177
- }
17178
- // style management
17179
- ,
17180
- baseClassName: "navi_native_input"
17181
- });
17182
- };
17183
- const renderInputMemoized = useCallback(renderInput, [type, uiState, innerValue, innerOnInput]);
17184
- return jsxs(Box, {
17185
- as: "span",
17186
- box: true,
17187
- baseClassName: "navi_input",
17188
- styleCSSVars: InputStyleCSSVars,
17189
- pseudoStateSelector: ".navi_native_input",
17190
- visualSelector: ".navi_native_input",
17191
- basePseudoState: {
17192
- ":read-only": innerReadOnly,
17193
- ":disabled": innerDisabled,
17194
- ":-navi-loading": innerLoading
17195
- },
17196
- pseudoClasses: InputPseudoClasses,
17197
- pseudoElements: InputPseudoElements,
17198
- hasChildFunction: true,
17162
+ return jsx("div", {
17199
17163
  ...rest,
17200
- children: [jsx(LoaderBackground, {
17201
- loading: innerLoading,
17202
- color: "var(--loader-color)",
17203
- inset: -1
17204
- }), renderInputMemoized]
17164
+ ref: innerRef,
17165
+ name: name,
17166
+ className: "navi_checkbox_list",
17167
+ "data-checkbox-list": true
17168
+ // eslint-disable-next-line react/no-unknown-property
17169
+ ,
17170
+ onresetuistate: e => {
17171
+ uiStateController.resetUIState(e);
17172
+ },
17173
+ children: jsx(ParentUIStateControllerContext.Provider, {
17174
+ value: uiStateController,
17175
+ children: jsx(FieldNameContext.Provider, {
17176
+ value: name,
17177
+ children: jsx(ReadOnlyContext.Provider, {
17178
+ value: innerReadOnly,
17179
+ children: jsx(DisabledContext.Provider, {
17180
+ value: innerDisabled,
17181
+ children: jsx(RequiredContext.Provider, {
17182
+ value: required,
17183
+ children: jsx(LoadingContext.Provider, {
17184
+ value: innerLoading,
17185
+ children: children
17186
+ })
17187
+ })
17188
+ })
17189
+ })
17190
+ })
17191
+ })
17205
17192
  });
17206
- };
17207
- const InputTextualWithAction = props => {
17193
+ });
17194
+ forwardRef((props, ref) => {
17195
+ const uiStateController = useContext(UIStateControllerContext);
17208
17196
  const uiState = useContext(UIStateContext);
17209
17197
  const {
17210
17198
  action,
17211
- loading,
17199
+ actionErrorEffect,
17212
17200
  onCancel,
17213
17201
  onActionPrevented,
17214
17202
  onActionStart,
17203
+ onActionAbort,
17215
17204
  onActionError,
17216
17205
  onActionEnd,
17217
- cancelOnBlurInvalid,
17218
- cancelOnEscape,
17219
- actionErrorEffect,
17206
+ loading,
17207
+ children,
17220
17208
  ...rest
17221
17209
  } = props;
17222
- const defaultRef = useRef();
17223
- const ref = props.ref || defaultRef;
17224
- const [boundAction] = useActionBoundToOneParam(action, uiState);
17210
+ const innerRef = useRef();
17211
+ useImperativeHandle(ref, () => innerRef.current);
17212
+ const [boundAction] = useActionBoundToOneArrayParam(action, uiState);
17225
17213
  const {
17226
17214
  loading: actionLoading
17227
17215
  } = useActionStatus(boundAction);
17228
- const executeAction = useExecuteAction(ref, {
17216
+ const executeAction = useExecuteAction(innerRef, {
17229
17217
  errorEffect: actionErrorEffect
17230
17218
  });
17231
- // here updating the input won't call the associated action
17232
- // (user have to blur or press enter for this to happen)
17233
- // so we can keep the ui state on cancel/abort/error and let user decide
17234
- // to update ui state or retry via blur/enter as is
17235
- useActionEvents(ref, {
17219
+ const [actionRequester, setActionRequester] = useState(null);
17220
+ useActionEvents(innerRef, {
17236
17221
  onCancel: (e, reason) => {
17237
- if (reason.startsWith("blur_invalid")) {
17238
- if (!cancelOnBlurInvalid) {
17239
- return;
17240
- }
17241
- if (
17242
- // error prevent cancellation until the user closes it (or something closes it)
17243
- e.detail.failedConstraintInfo.level === "error" && e.detail.failedConstraintInfo.reportStatus !== "closed") {
17244
- return;
17245
- }
17246
- }
17247
- if (reason === "escape_key") {
17248
- if (!cancelOnEscape) {
17249
- return;
17250
- }
17251
- }
17222
+ uiStateController.resetUIState(e);
17252
17223
  onCancel?.(e, reason);
17253
17224
  },
17254
- onRequested: e => {
17255
- forwardActionRequested(e, boundAction);
17256
- },
17257
17225
  onPrevented: onActionPrevented,
17258
- onAction: executeAction,
17226
+ onAction: actionEvent => {
17227
+ setActionRequester(actionEvent.detail.requester);
17228
+ executeAction(actionEvent);
17229
+ },
17259
17230
  onStart: onActionStart,
17260
- onError: onActionError,
17261
- onEnd: onActionEnd
17231
+ onAbort: e => {
17232
+ uiStateController.resetUIState(e);
17233
+ onActionAbort?.(e);
17234
+ },
17235
+ onError: e => {
17236
+ uiStateController.resetUIState(e);
17237
+ onActionError?.(e);
17238
+ },
17239
+ onEnd: e => {
17240
+ onActionEnd?.(e);
17241
+ }
17262
17242
  });
17263
- return jsx(InputTextualBasic, {
17243
+ return jsx(CheckboxListBasic, {
17264
17244
  "data-action": boundAction.name,
17265
17245
  ...rest,
17266
- ref: ref,
17267
- loading: loading || actionLoading
17268
- });
17269
- };
17270
- const InputTextualInsideForm = props => {
17271
- const {
17272
- // We destructure formContext to avoid passing it to the underlying input element
17273
- // eslint-disable-next-line no-unused-vars
17274
- formContext,
17275
- ...rest
17276
- } = props;
17277
- return jsx(InputTextualBasic, {
17278
- ...rest
17246
+ ref: innerRef,
17247
+ onChange: event => {
17248
+ const checkboxList = innerRef.current;
17249
+ const checkbox = event.target;
17250
+ requestAction(checkboxList, boundAction, {
17251
+ event,
17252
+ requester: checkbox
17253
+ });
17254
+ },
17255
+ loading: loading || actionLoading,
17256
+ children: jsx(LoadingElementContext.Provider, {
17257
+ value: actionRequester,
17258
+ children: children
17259
+ })
17279
17260
  });
17280
- };
17261
+ });
17281
17262
 
17282
- // As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
17283
- // datetime-local does not support timezones
17284
- const convertToLocalTimezone = dateTimeString => {
17285
- const date = new Date(dateTimeString);
17286
- // Check if the date is valid
17287
- if (isNaN(date.getTime())) {
17288
- return dateTimeString;
17263
+ const collectFormElementValues = (element) => {
17264
+ let formElements;
17265
+ if (element.tagName === "FORM") {
17266
+ formElements = element.elements;
17267
+ } else {
17268
+ // fieldset or anything else
17269
+ formElements = element.querySelectorAll(
17270
+ "input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button[name]:not([disabled])",
17271
+ );
17289
17272
  }
17290
17273
 
17291
- // Format to YYYY-MM-DDThh:mm:ss
17292
- const year = date.getFullYear();
17293
- const month = String(date.getMonth() + 1).padStart(2, "0");
17294
- const day = String(date.getDate()).padStart(2, "0");
17295
- const hours = String(date.getHours()).padStart(2, "0");
17296
- const minutes = String(date.getMinutes()).padStart(2, "0");
17297
- const seconds = String(date.getSeconds()).padStart(2, "0");
17298
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
17299
- };
17300
- /**
17301
- * Converts a datetime string without timezone (local time) to UTC format with 'Z' notation
17302
- *
17303
- * @param {string} localDateTimeString - Local datetime string without timezone (e.g., "2023-07-15T14:30:00")
17304
- * @returns {string} Datetime string in UTC with 'Z' notation (e.g., "2023-07-15T12:30:00Z")
17305
- */
17306
- const convertToUTCTimezone = localDateTimeString => {
17307
- if (!localDateTimeString) {
17308
- return localDateTimeString;
17274
+ const values = {};
17275
+ const checkboxArrayNameSet = new Set();
17276
+ for (const formElement of formElements) {
17277
+ if (formElement.type === "checkbox" && formElement.name) {
17278
+ const name = formElement.name;
17279
+ const endsWithBrackets = name.endsWith("[]");
17280
+ if (endsWithBrackets) {
17281
+ checkboxArrayNameSet.add(name);
17282
+ values[name] = [];
17283
+ continue;
17284
+ }
17285
+ const closestDataCheckboxList = formElement.closest(
17286
+ "[data-checkbox-list]",
17287
+ );
17288
+ if (closestDataCheckboxList) {
17289
+ checkboxArrayNameSet.add(name);
17290
+ values[name] = [];
17291
+ }
17292
+ }
17309
17293
  }
17310
- try {
17311
- // Create a Date object using the local time string
17312
- // The browser will interpret this as local timezone
17313
- const localDate = new Date(localDateTimeString);
17314
17294
 
17315
- // Check if the date is valid
17316
- if (isNaN(localDate.getTime())) {
17317
- return localDateTimeString;
17295
+ for (const formElement of formElements) {
17296
+ const name = formElement.name;
17297
+ if (!name) {
17298
+ continue;
17299
+ }
17300
+ const value = getFormElementValue(formElement);
17301
+ if (value === undefined) {
17302
+ continue; // Skip unchecked checkboxes/radios
17303
+ }
17304
+ if (formElement.type === "checkbox" && checkboxArrayNameSet.has(name)) {
17305
+ values[name].push(value);
17306
+ } else {
17307
+ values[name] = value;
17318
17308
  }
17309
+ }
17310
+ return values;
17311
+ };
17319
17312
 
17320
- // Convert to UTC ISO string
17321
- const utcString = localDate.toISOString();
17313
+ const getFormElementValue = (formElement) => {
17314
+ const { type, tagName } = formElement;
17322
17315
 
17323
- // Return the UTC string (which includes the 'Z' notation)
17324
- return utcString;
17325
- } catch (error) {
17326
- console.error("Error converting local datetime to UTC:", error);
17327
- return localDateTimeString;
17316
+ if (tagName === "SELECT") {
17317
+ if (formElement.multiple) {
17318
+ return Array.from(formElement.selectedOptions, (option) =>
17319
+ getValue(option),
17320
+ );
17321
+ }
17322
+ return formElement.value;
17328
17323
  }
17329
- };
17330
17324
 
17331
- const Input = forwardRef((props, ref) => {
17332
- const {
17333
- type
17334
- } = props;
17335
- if (type === "radio") {
17336
- return jsx(InputRadio, {
17337
- ...props,
17338
- ref: ref
17339
- });
17325
+ if (type === "checkbox" || type === "radio") {
17326
+ return formElement.checked ? getValue(formElement) : undefined;
17340
17327
  }
17341
- if (type === "checkbox") {
17342
- return jsx(InputCheckbox, {
17343
- ...props,
17344
- ref: ref
17345
- });
17328
+
17329
+ if (type === "file") {
17330
+ return formElement.files; // Return FileList for special handling
17346
17331
  }
17347
- return jsx(InputTextual, {
17348
- ...props,
17349
- ref: ref
17350
- });
17351
- });
17352
17332
 
17353
- installImportMetaCss(import.meta);import.meta.css = /* css */`
17354
- .navi_editable_wrapper {
17355
- position: absolute;
17356
- inset: 0;
17333
+ return getValue(formElement);
17334
+ };
17335
+
17336
+ const getValue = (formElement) => {
17337
+ const hasDataValueAttribute = formElement.hasAttribute("data-value");
17338
+ if (hasDataValueAttribute) {
17339
+ // happens for "datetime-local" inputs to keep the timezone
17340
+ // consistent when sending to the server
17341
+ return formElement.getAttribute("data-value");
17357
17342
  }
17358
- `;
17359
- const useEditionController = () => {
17360
- const [editing, editingSetter] = useState(null);
17361
- const startEditing = useCallback(event => {
17362
- editingSetter(current => {
17363
- return current || {
17364
- event
17365
- };
17366
- });
17367
- }, []);
17368
- const stopEditing = useCallback(() => {
17369
- editingSetter(null);
17370
- }, []);
17371
- const prevEditingRef = useRef(editing);
17372
- const editionJustEnded = prevEditingRef.current && !editing;
17373
- prevEditingRef.current = editing;
17374
- return {
17375
- editing,
17376
- startEditing,
17377
- stopEditing,
17378
- editionJustEnded
17379
- };
17343
+ return formElement.value;
17380
17344
  };
17381
- const Editable = forwardRef((props, ref) => {
17382
- let {
17383
- children,
17384
- action,
17385
- editing,
17386
- name,
17387
- value,
17388
- valueSignal,
17389
- onEditEnd,
17390
- constraints,
17391
- type,
17392
- required,
17393
- readOnly,
17394
- min,
17395
- max,
17396
- step,
17397
- minLength,
17398
- maxLength,
17399
- pattern,
17400
- wrapperProps,
17401
- autoSelect = true,
17402
- width,
17403
- height,
17404
- ...rest
17405
- } = props;
17406
- const innerRef = useRef();
17407
- useImperativeHandle(ref, () => innerRef.current);
17408
- if (valueSignal) {
17409
- value = valueSignal.value;
17410
- }
17411
- const editingPreviousRef = useRef(editing);
17412
- const valueWhenEditStartRef = useRef(editing ? value : undefined);
17413
- if (editingPreviousRef.current !== editing) {
17414
- if (editing) {
17415
- valueWhenEditStartRef.current = value; // Always store the external value
17416
- }
17417
- editingPreviousRef.current = editing;
17418
- }
17419
17345
 
17420
- // Simulate typing the initial value when editing starts with a custom value
17421
- useLayoutEffect(() => {
17422
- if (!editing) {
17423
- return;
17424
- }
17425
- const editingEvent = editing.event;
17426
- if (!editingEvent) {
17427
- return;
17428
- }
17429
- const editingEventInitialValue = editingEvent.detail?.initialValue;
17430
- if (editingEventInitialValue === undefined) {
17431
- return;
17432
- }
17433
- const input = innerRef.current;
17434
- input.value = editingEventInitialValue;
17435
- input.dispatchEvent(new CustomEvent("input", {
17436
- bubbles: false
17437
- }));
17438
- }, [editing]);
17439
- const input = jsx(Input, {
17440
- ref: innerRef,
17441
- ...rest,
17442
- type: type,
17443
- name: name,
17444
- value: value,
17445
- valueSignal: valueSignal,
17446
- autoFocus: true,
17447
- autoFocusVisible: true,
17448
- autoSelect: autoSelect,
17449
- cancelOnEscape: true,
17450
- cancelOnBlurInvalid: true,
17451
- constraints: constraints,
17452
- required: required,
17453
- readOnly: readOnly,
17454
- min: min,
17455
- max: max,
17456
- step: step,
17457
- minLength: minLength,
17458
- maxLength: maxLength,
17459
- pattern: pattern,
17460
- width: width,
17461
- height: height,
17462
- onCancel: e => {
17463
- if (valueSignal) {
17464
- valueSignal.value = valueWhenEditStartRef.current;
17465
- }
17466
- onEditEnd({
17467
- cancelled: true,
17468
- event: e
17469
- });
17470
- },
17471
- onBlur: e => {
17472
- const value = type === "number" ? e.target.valueAsNumber : e.target.value;
17473
- const valueWhenEditStart = valueWhenEditStartRef.current;
17474
- if (value === valueWhenEditStart) {
17475
- onEditEnd({
17476
- cancelled: true,
17477
- event: e
17478
- });
17479
- return;
17480
- }
17481
- },
17482
- action: action || (() => {}),
17483
- onActionEnd: e => {
17484
- onEditEnd({
17485
- success: true,
17486
- event: e
17487
- });
17488
- }
17489
- });
17490
- return jsxs(Fragment, {
17491
- children: [children || jsx("span", {
17492
- children: value
17493
- }), editing && jsx("div", {
17494
- ...wrapperProps,
17495
- className: ["navi_editable_wrapper", ...(wrapperProps?.className || "").split(" ")].join(" "),
17496
- children: input
17497
- })]
17498
- });
17499
- });
17346
+ /**
17347
+ *
17348
+ * Here we want the same behaviour as web standards:
17349
+ *
17350
+ * 1. When submitting the form URL does not change
17351
+ * 2. When form submission id done user is redirected (by default the current one)
17352
+ * (we can configure this using target)
17353
+ * So for example user might be reidrect to a page with the resource he just created
17354
+ * I could create an example where we would put a link on the page to let user see what he created
17355
+ * but by default user stays on the form allowing to create multiple resources at once
17356
+ * And an other where he is redirected to the resource he created
17357
+ * 3. If form submission fails ideally we should display this somewhere on the UI
17358
+ * right now it's just logged to the console I need to see how we can achieve this
17359
+ */
17500
17360
 
17501
- installImportMetaCss(import.meta);import.meta.css = /* css */`
17502
- @layer navi {
17503
- .navi_checkbox_list {
17504
- display: flex;
17505
- flex-direction: column;
17506
- }
17507
- }
17508
- `;
17509
- const CheckboxList = forwardRef((props, ref) => {
17510
- const uiStateController = useUIGroupStateController(props, "checkbox_list", {
17511
- childComponentType: "checkbox",
17361
+ const Form = props => {
17362
+ const uiStateController = useUIGroupStateController(props, "form", {
17363
+ childComponentType: "*",
17512
17364
  aggregateChildStates: childUIStateControllers => {
17513
- const values = [];
17365
+ const formValues = {};
17514
17366
  for (const childUIStateController of childUIStateControllers) {
17515
- if (childUIStateController.uiState) {
17516
- values.push(childUIStateController.uiState);
17367
+ const {
17368
+ name,
17369
+ uiState
17370
+ } = childUIStateController;
17371
+ if (!name) {
17372
+ console.warn("A form child component is missing a name property, its state won't be included in the form state", childUIStateController);
17373
+ continue;
17517
17374
  }
17375
+ formValues[name] = uiState;
17518
17376
  }
17519
- return values.length === 0 ? undefined : values;
17377
+ return formValues;
17520
17378
  }
17521
17379
  });
17522
17380
  const uiState = useUIState(uiStateController);
17523
- const checkboxList = renderActionableComponent(props, ref);
17381
+ const form = renderActionableComponent(props, {
17382
+ Basic: FormBasic,
17383
+ WithAction: FormWithAction
17384
+ });
17524
17385
  return jsx(UIStateControllerContext.Provider, {
17525
17386
  value: uiStateController,
17526
17387
  children: jsx(UIStateContext.Provider, {
17527
17388
  value: uiState,
17528
- children: checkboxList
17389
+ children: form
17529
17390
  })
17530
17391
  });
17531
- });
17532
- const Checkbox = InputCheckbox;
17533
- const CheckboxListBasic = forwardRef((props, ref) => {
17534
- const contextReadOnly = useContext(ReadOnlyContext);
17535
- const contextDisabled = useContext(DisabledContext);
17536
- const contextLoading = useContext(LoadingContext);
17392
+ };
17393
+ const FormBasic = props => {
17537
17394
  const uiStateController = useContext(UIStateControllerContext);
17538
17395
  const {
17539
- name,
17540
17396
  readOnly,
17541
- disabled,
17542
- required,
17543
17397
  loading,
17544
17398
  children,
17545
17399
  ...rest
17546
17400
  } = props;
17547
- const innerRef = useRef();
17548
- useImperativeHandle(ref, () => innerRef.current);
17549
- const innerLoading = loading || contextLoading;
17550
- const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
17551
- const innerDisabled = disabled || contextDisabled;
17552
- return jsx("div", {
17401
+ const defaultRef = useRef();
17402
+ const ref = props.ref || defaultRef;
17403
+
17404
+ // instantiation validation to:
17405
+ // - receive "requestsubmit" custom event ensure submit is prevented
17406
+ // (and also execute action without validation if form.submit() is ever called)
17407
+ useConstraints(ref, []);
17408
+ const innerReadOnly = readOnly || loading;
17409
+ const formContextValue = useMemo(() => {
17410
+ return {
17411
+ loading
17412
+ };
17413
+ }, [loading]);
17414
+ return jsx(Box, {
17553
17415
  ...rest,
17554
- ref: innerRef,
17555
- name: name,
17556
- className: "navi_checkbox_list",
17557
- "data-checkbox-list": true
17558
- // eslint-disable-next-line react/no-unknown-property
17559
- ,
17560
- onresetuistate: e => {
17416
+ as: "form",
17417
+ ref: ref,
17418
+ onReset: e => {
17419
+ // browser would empty all fields to their default values (likely empty/unchecked)
17420
+ // we want to reset to the last known external state instead
17421
+ e.preventDefault();
17561
17422
  uiStateController.resetUIState(e);
17562
17423
  },
17563
17424
  children: jsx(ParentUIStateControllerContext.Provider, {
17564
17425
  value: uiStateController,
17565
- children: jsx(FieldNameContext.Provider, {
17566
- value: name,
17567
- children: jsx(ReadOnlyContext.Provider, {
17568
- value: innerReadOnly,
17569
- children: jsx(DisabledContext.Provider, {
17570
- value: innerDisabled,
17571
- children: jsx(RequiredContext.Provider, {
17572
- value: required,
17573
- children: jsx(LoadingContext.Provider, {
17574
- value: innerLoading,
17575
- children: children
17576
- })
17577
- })
17426
+ children: jsx(ReadOnlyContext.Provider, {
17427
+ value: innerReadOnly,
17428
+ children: jsx(LoadingContext.Provider, {
17429
+ value: loading,
17430
+ children: jsx(FormContext.Provider, {
17431
+ value: formContextValue,
17432
+ children: children
17578
17433
  })
17579
17434
  })
17580
17435
  })
17581
17436
  })
17582
17437
  });
17583
- });
17584
- forwardRef((props, ref) => {
17438
+ };
17439
+ const FormWithAction = props => {
17585
17440
  const uiStateController = useContext(UIStateControllerContext);
17586
17441
  const uiState = useContext(UIStateContext);
17587
17442
  const {
17588
17443
  action,
17589
- actionErrorEffect,
17590
- onCancel,
17444
+ method,
17445
+ actionErrorEffect = "show_validation_message",
17446
+ // "show_validation_message" or "throw"
17447
+ errorMapping,
17591
17448
  onActionPrevented,
17592
17449
  onActionStart,
17593
17450
  onActionAbort,
@@ -17597,265 +17454,18 @@ forwardRef((props, ref) => {
17597
17454
  children,
17598
17455
  ...rest
17599
17456
  } = props;
17600
- const innerRef = useRef();
17601
- useImperativeHandle(ref, () => innerRef.current);
17602
- const [boundAction] = useActionBoundToOneArrayParam(action, uiState);
17603
- const {
17604
- loading: actionLoading
17605
- } = useActionStatus(boundAction);
17606
- const executeAction = useExecuteAction(innerRef, {
17607
- errorEffect: actionErrorEffect
17457
+ const defaultRef = useRef();
17458
+ const ref = props.ref || defaultRef;
17459
+ const [actionBoundToUIState] = useActionBoundToOneParam(action, uiState);
17460
+ const executeAction = useExecuteAction(ref, {
17461
+ errorEffect: actionErrorEffect,
17462
+ errorMapping
17608
17463
  });
17609
- const [actionRequester, setActionRequester] = useState(null);
17610
- useActionEvents(innerRef, {
17611
- onCancel: (e, reason) => {
17612
- uiStateController.resetUIState(e);
17613
- onCancel?.(e, reason);
17614
- },
17615
- onPrevented: onActionPrevented,
17616
- onAction: actionEvent => {
17617
- setActionRequester(actionEvent.detail.requester);
17618
- executeAction(actionEvent);
17619
- },
17620
- onStart: onActionStart,
17621
- onAbort: e => {
17622
- uiStateController.resetUIState(e);
17623
- onActionAbort?.(e);
17624
- },
17625
- onError: e => {
17626
- uiStateController.resetUIState(e);
17627
- onActionError?.(e);
17628
- },
17629
- onEnd: e => {
17630
- onActionEnd?.(e);
17631
- }
17632
- });
17633
- return jsx(CheckboxListBasic, {
17634
- "data-action": boundAction.name,
17635
- ...rest,
17636
- ref: innerRef,
17637
- onChange: event => {
17638
- const checkboxList = innerRef.current;
17639
- const checkbox = event.target;
17640
- requestAction(checkboxList, boundAction, {
17641
- event,
17642
- requester: checkbox
17643
- });
17644
- },
17645
- loading: loading || actionLoading,
17646
- children: jsx(LoadingElementContext.Provider, {
17647
- value: actionRequester,
17648
- children: children
17649
- })
17650
- });
17651
- });
17652
-
17653
- const collectFormElementValues = (element) => {
17654
- let formElements;
17655
- if (element.tagName === "FORM") {
17656
- formElements = element.elements;
17657
- } else {
17658
- // fieldset or anything else
17659
- formElements = element.querySelectorAll(
17660
- "input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button[name]:not([disabled])",
17661
- );
17662
- }
17663
-
17664
- const values = {};
17665
- const checkboxArrayNameSet = new Set();
17666
- for (const formElement of formElements) {
17667
- if (formElement.type === "checkbox" && formElement.name) {
17668
- const name = formElement.name;
17669
- const endsWithBrackets = name.endsWith("[]");
17670
- if (endsWithBrackets) {
17671
- checkboxArrayNameSet.add(name);
17672
- values[name] = [];
17673
- continue;
17674
- }
17675
- const closestDataCheckboxList = formElement.closest(
17676
- "[data-checkbox-list]",
17677
- );
17678
- if (closestDataCheckboxList) {
17679
- checkboxArrayNameSet.add(name);
17680
- values[name] = [];
17681
- }
17682
- }
17683
- }
17684
-
17685
- for (const formElement of formElements) {
17686
- const name = formElement.name;
17687
- if (!name) {
17688
- continue;
17689
- }
17690
- const value = getFormElementValue(formElement);
17691
- if (value === undefined) {
17692
- continue; // Skip unchecked checkboxes/radios
17693
- }
17694
- if (formElement.type === "checkbox" && checkboxArrayNameSet.has(name)) {
17695
- values[name].push(value);
17696
- } else {
17697
- values[name] = value;
17698
- }
17699
- }
17700
- return values;
17701
- };
17702
-
17703
- const getFormElementValue = (formElement) => {
17704
- const { type, tagName } = formElement;
17705
-
17706
- if (tagName === "SELECT") {
17707
- if (formElement.multiple) {
17708
- return Array.from(formElement.selectedOptions, (option) =>
17709
- getValue(option),
17710
- );
17711
- }
17712
- return formElement.value;
17713
- }
17714
-
17715
- if (type === "checkbox" || type === "radio") {
17716
- return formElement.checked ? getValue(formElement) : undefined;
17717
- }
17718
-
17719
- if (type === "file") {
17720
- return formElement.files; // Return FileList for special handling
17721
- }
17722
-
17723
- return getValue(formElement);
17724
- };
17725
-
17726
- const getValue = (formElement) => {
17727
- const hasDataValueAttribute = formElement.hasAttribute("data-value");
17728
- if (hasDataValueAttribute) {
17729
- // happens for "datetime-local" inputs to keep the timezone
17730
- // consistent when sending to the server
17731
- return formElement.getAttribute("data-value");
17732
- }
17733
- return formElement.value;
17734
- };
17735
-
17736
- /**
17737
- *
17738
- * Here we want the same behaviour as web standards:
17739
- *
17740
- * 1. When submitting the form URL does not change
17741
- * 2. When form submission id done user is redirected (by default the current one)
17742
- * (we can configure this using target)
17743
- * So for example user might be reidrect to a page with the resource he just created
17744
- * I could create an example where we would put a link on the page to let user see what he created
17745
- * but by default user stays on the form allowing to create multiple resources at once
17746
- * And an other where he is redirected to the resource he created
17747
- * 3. If form submission fails ideally we should display this somewhere on the UI
17748
- * right now it's just logged to the console I need to see how we can achieve this
17749
- */
17750
-
17751
- const Form = props => {
17752
- const uiStateController = useUIGroupStateController(props, "form", {
17753
- childComponentType: "*",
17754
- aggregateChildStates: childUIStateControllers => {
17755
- const formValues = {};
17756
- for (const childUIStateController of childUIStateControllers) {
17757
- const {
17758
- name,
17759
- uiState
17760
- } = childUIStateController;
17761
- if (!name) {
17762
- console.warn("A form child component is missing a name property, its state won't be included in the form state", childUIStateController);
17763
- continue;
17764
- }
17765
- formValues[name] = uiState;
17766
- }
17767
- return formValues;
17768
- }
17769
- });
17770
- const uiState = useUIState(uiStateController);
17771
- const form = renderActionableComponent(props, {
17772
- Basic: FormBasic,
17773
- WithAction: FormWithAction
17774
- });
17775
- return jsx(UIStateControllerContext.Provider, {
17776
- value: uiStateController,
17777
- children: jsx(UIStateContext.Provider, {
17778
- value: uiState,
17779
- children: form
17780
- })
17781
- });
17782
- };
17783
- const FormBasic = props => {
17784
- const uiStateController = useContext(UIStateControllerContext);
17785
- const {
17786
- readOnly,
17787
- loading,
17788
- children,
17789
- ...rest
17790
- } = props;
17791
- const defaultRef = useRef();
17792
- const ref = props.ref || defaultRef;
17793
-
17794
- // instantiation validation to:
17795
- // - receive "requestsubmit" custom event ensure submit is prevented
17796
- // (and also execute action without validation if form.submit() is ever called)
17797
- useConstraints(ref, []);
17798
- const innerReadOnly = readOnly || loading;
17799
- const formContextValue = useMemo(() => {
17800
- return {
17801
- loading
17802
- };
17803
- }, [loading]);
17804
- return jsx(Box, {
17805
- ...rest,
17806
- as: "form",
17807
- ref: ref,
17808
- onReset: e => {
17809
- // browser would empty all fields to their default values (likely empty/unchecked)
17810
- // we want to reset to the last known external state instead
17811
- e.preventDefault();
17812
- uiStateController.resetUIState(e);
17813
- },
17814
- children: jsx(ParentUIStateControllerContext.Provider, {
17815
- value: uiStateController,
17816
- children: jsx(ReadOnlyContext.Provider, {
17817
- value: innerReadOnly,
17818
- children: jsx(LoadingContext.Provider, {
17819
- value: loading,
17820
- children: jsx(FormContext.Provider, {
17821
- value: formContextValue,
17822
- children: children
17823
- })
17824
- })
17825
- })
17826
- })
17827
- });
17828
- };
17829
- const FormWithAction = props => {
17830
- const uiStateController = useContext(UIStateControllerContext);
17831
- const uiState = useContext(UIStateContext);
17832
- const {
17833
- action,
17834
- method,
17835
- actionErrorEffect = "show_validation_message",
17836
- // "show_validation_message" or "throw"
17837
- errorMapping,
17838
- onActionPrevented,
17839
- onActionStart,
17840
- onActionAbort,
17841
- onActionError,
17842
- onActionEnd,
17843
- loading,
17844
- children,
17845
- ...rest
17846
- } = props;
17847
- const defaultRef = useRef();
17848
- const ref = props.ref || defaultRef;
17849
- const [actionBoundToUIState] = useActionBoundToOneParam(action, uiState);
17850
- const executeAction = useExecuteAction(ref, {
17851
- errorEffect: actionErrorEffect,
17852
- errorMapping
17853
- });
17854
- const {
17855
- actionPending,
17856
- actionRequester: formActionRequester
17857
- } = useRequestedActionStatus(ref);
17858
- useActionEvents(ref, {
17464
+ const {
17465
+ actionPending,
17466
+ actionRequester: formActionRequester
17467
+ } = useRequestedActionStatus(ref);
17468
+ useActionEvents(ref, {
17859
17469
  onPrevented: onActionPrevented,
17860
17470
  onRequested: e => {
17861
17471
  forwardActionRequested(e, actionBoundToUIState);
@@ -18104,213 +17714,759 @@ const useRefArray = (items, keyFromItem) => {
18104
17714
  };
18105
17715
 
18106
17716
  installImportMetaCss(import.meta);import.meta.css = /* css */`
18107
- .navi_select[data-readonly] {
18108
- pointer-events: none;
17717
+ .navi_select[data-readonly] {
17718
+ pointer-events: none;
17719
+ }
17720
+ `;
17721
+ const Select = forwardRef((props, ref) => {
17722
+ const select = renderActionableComponent(props, ref);
17723
+ return select;
17724
+ });
17725
+ const SelectControlled = forwardRef((props, ref) => {
17726
+ const {
17727
+ name,
17728
+ value,
17729
+ loading,
17730
+ disabled,
17731
+ readOnly,
17732
+ children,
17733
+ ...rest
17734
+ } = props;
17735
+ const innerRef = useRef();
17736
+ useImperativeHandle(ref, () => innerRef.current);
17737
+ const selectElement = jsx("select", {
17738
+ className: "navi_select",
17739
+ ref: innerRef,
17740
+ "data-readonly": readOnly && !disabled ? "" : undefined,
17741
+ onKeyDown: e => {
17742
+ if (readOnly) {
17743
+ e.preventDefault();
17744
+ }
17745
+ },
17746
+ ...rest,
17747
+ children: children.map(child => {
17748
+ const {
17749
+ label,
17750
+ readOnly: childReadOnly,
17751
+ disabled: childDisabled,
17752
+ loading: childLoading,
17753
+ value: childValue,
17754
+ ...childRest
17755
+ } = child;
17756
+ return jsx("option", {
17757
+ name: name,
17758
+ value: childValue,
17759
+ selected: childValue === value,
17760
+ readOnly: readOnly || childReadOnly,
17761
+ disabled: disabled || childDisabled,
17762
+ loading: loading || childLoading,
17763
+ ...childRest,
17764
+ children: label
17765
+ }, childValue);
17766
+ })
17767
+ });
17768
+ return jsx(LoaderBackground, {
17769
+ loading: loading,
17770
+ color: "light-dark(#355fcc, #3b82f6)",
17771
+ inset: -1,
17772
+ children: selectElement
17773
+ });
17774
+ });
17775
+ forwardRef((props, ref) => {
17776
+ const {
17777
+ value: initialValue,
17778
+ id,
17779
+ children,
17780
+ ...rest
17781
+ } = props;
17782
+ const innerRef = useRef();
17783
+ useImperativeHandle(ref, () => innerRef.current);
17784
+ const [navState, setNavState] = useNavState(id);
17785
+ const valueAtStart = navState === undefined ? initialValue : navState;
17786
+ const [value, setValue] = useState(valueAtStart);
17787
+ useEffect(() => {
17788
+ setNavState(value);
17789
+ }, [value]);
17790
+ return jsx(SelectControlled, {
17791
+ ref: innerRef,
17792
+ value: value,
17793
+ onChange: event => {
17794
+ const select = event.target;
17795
+ const selectedValue = select.value;
17796
+ setValue(selectedValue);
17797
+ },
17798
+ ...rest,
17799
+ children: children
17800
+ });
17801
+ });
17802
+ forwardRef((props, ref) => {
17803
+ const {
17804
+ id,
17805
+ name,
17806
+ value: externalValue,
17807
+ valueSignal,
17808
+ action,
17809
+ children,
17810
+ onCancel,
17811
+ onActionPrevented,
17812
+ onActionStart,
17813
+ onActionAbort,
17814
+ onActionError,
17815
+ onActionEnd,
17816
+ actionErrorEffect,
17817
+ ...rest
17818
+ } = props;
17819
+ const innerRef = useRef();
17820
+ useImperativeHandle(ref, () => innerRef.current);
17821
+ const [navState, setNavState, resetNavState] = useNavState(id);
17822
+ const [boundAction, value, setValue, initialValue] = useActionBoundToOneParam(action, name);
17823
+ const {
17824
+ loading: actionLoading
17825
+ } = useActionStatus(boundAction);
17826
+ const executeAction = useExecuteAction(innerRef, {
17827
+ errorEffect: actionErrorEffect
17828
+ });
17829
+ useEffect(() => {
17830
+ setNavState(value);
17831
+ }, [value]);
17832
+ const actionRequesterRef = useRef(null);
17833
+ useActionEvents(innerRef, {
17834
+ onCancel: (e, reason) => {
17835
+ resetNavState();
17836
+ setValue(initialValue);
17837
+ onCancel?.(e, reason);
17838
+ },
17839
+ onPrevented: onActionPrevented,
17840
+ onAction: actionEvent => {
17841
+ actionRequesterRef.current = actionEvent.detail.requester;
17842
+ executeAction(actionEvent);
17843
+ },
17844
+ onStart: onActionStart,
17845
+ onAbort: e => {
17846
+ setValue(initialValue);
17847
+ onActionAbort?.(e);
17848
+ },
17849
+ onError: error => {
17850
+ setValue(initialValue);
17851
+ onActionError?.(error);
17852
+ },
17853
+ onEnd: () => {
17854
+ resetNavState();
17855
+ onActionEnd?.();
17856
+ }
17857
+ });
17858
+ const childRefArray = useRefArray(children, child => child.value);
17859
+ return jsx(SelectControlled, {
17860
+ ref: innerRef,
17861
+ name: name,
17862
+ value: value,
17863
+ "data-action": boundAction,
17864
+ onChange: event => {
17865
+ const select = event.target;
17866
+ const selectedValue = select.value;
17867
+ setValue(selectedValue);
17868
+ const radioListContainer = innerRef.current;
17869
+ const optionSelected = select.querySelector(`option[value="${selectedValue}"]`);
17870
+ requestAction(radioListContainer, boundAction, {
17871
+ event,
17872
+ requester: optionSelected
17873
+ });
17874
+ },
17875
+ ...rest,
17876
+ children: children.map((child, i) => {
17877
+ const childRef = childRefArray[i];
17878
+ return {
17879
+ ...child,
17880
+ ref: childRef,
17881
+ loading: child.loading || actionLoading && actionRequesterRef.current === childRef.current,
17882
+ readOnly: child.readOnly || actionLoading
17883
+ };
17884
+ })
17885
+ });
17886
+ });
17887
+ forwardRef((props, ref) => {
17888
+ const {
17889
+ id,
17890
+ name,
17891
+ value: externalValue,
17892
+ children,
17893
+ ...rest
17894
+ } = props;
17895
+ const innerRef = useRef();
17896
+ useImperativeHandle(ref, () => innerRef.current);
17897
+ const [navState, setNavState] = useNavState(id);
17898
+ const [value, setValue, initialValue] = [name, externalValue, navState];
17899
+ useEffect(() => {
17900
+ setNavState(value);
17901
+ }, [value]);
17902
+ useFormEvents(innerRef, {
17903
+ onFormReset: () => {
17904
+ setValue(undefined);
17905
+ },
17906
+ onFormActionAbort: () => {
17907
+ setValue(initialValue);
17908
+ },
17909
+ onFormActionError: () => {
17910
+ setValue(initialValue);
17911
+ }
17912
+ });
17913
+ return jsx(SelectControlled, {
17914
+ ref: innerRef,
17915
+ name: name,
17916
+ value: value,
17917
+ onChange: event => {
17918
+ const select = event.target;
17919
+ const selectedValue = select.checked;
17920
+ setValue(selectedValue);
17921
+ },
17922
+ ...rest,
17923
+ children: children
17924
+ });
17925
+ });
17926
+
17927
+ const createUniqueValueConstraint = (
17928
+ // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
17929
+ // but this is already nice to help user with what we know
17930
+ // it's also possible that front is unsync with backend, preventing user to choose a value
17931
+ // that is actually free.
17932
+ // But this is unlikely to happen and user could reload the page to be able to choose that name
17933
+ // that suddenly became available
17934
+ existingValueSet,
17935
+ message = `"{value}" already exists. Please choose another value.`,
17936
+ ) => {
17937
+ return {
17938
+ name: "unique_value",
17939
+ check: (input) => {
17940
+ const inputValue = input.value;
17941
+ const hasConflict = existingValueSet.has(inputValue);
17942
+ // console.log({
17943
+ // inputValue,
17944
+ // names: Array.from(otherNameSet.values()),
17945
+ // hasConflict,
17946
+ // });
17947
+ if (hasConflict) {
17948
+ return message.replace("{value}", inputValue);
17949
+ }
17950
+ return "";
17951
+ },
17952
+ };
17953
+ };
17954
+
17955
+ const SINGLE_SPACE_CONSTRAINT = {
17956
+ name: "single_space",
17957
+ check: (input) => {
17958
+ const inputValue = input.value;
17959
+ const hasLeadingSpace = inputValue.startsWith(" ");
17960
+ const hasTrailingSpace = inputValue.endsWith(" ");
17961
+ const hasDoubleSpace = inputValue.includes(" ");
17962
+ if (hasLeadingSpace || hasDoubleSpace || hasTrailingSpace) {
17963
+ return "Spaces at the beginning, end, or consecutive spaces are not allowed";
17964
+ }
17965
+ return "";
17966
+ },
17967
+ };
17968
+
17969
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
17970
+ .action_error {
17971
+ padding: 20px;
17972
+ background: #fdd;
17973
+ border: 1px solid red;
17974
+ margin-top: 0;
17975
+ margin-bottom: 20px;
17976
+ }
17977
+ `;
17978
+ const renderIdleDefault = () => null;
17979
+ const renderLoadingDefault = () => null;
17980
+ const renderAbortedDefault = () => null;
17981
+ const renderErrorDefault = error => {
17982
+ let routeErrorText = error && error.message ? error.message : error;
17983
+ return jsxs("p", {
17984
+ className: "action_error",
17985
+ children: ["An error occured: ", routeErrorText]
17986
+ });
17987
+ };
17988
+ const renderCompletedDefault = () => null;
17989
+ const ActionRenderer = ({
17990
+ action,
17991
+ children,
17992
+ disabled
17993
+ }) => {
17994
+ const {
17995
+ idle: renderIdle = renderIdleDefault,
17996
+ loading: renderLoading = renderLoadingDefault,
17997
+ aborted: renderAborted = renderAbortedDefault,
17998
+ error: renderError = renderErrorDefault,
17999
+ completed: renderCompleted,
18000
+ always: renderAlways
18001
+ } = typeof children === "function" ? {
18002
+ completed: children
18003
+ } : children || {};
18004
+ if (disabled) {
18005
+ return null;
18006
+ }
18007
+ if (action === undefined) {
18008
+ throw new Error("ActionRenderer requires an action to render, but none was provided.");
18009
+ }
18010
+ const {
18011
+ idle,
18012
+ loading,
18013
+ aborted,
18014
+ error,
18015
+ data
18016
+ } = useActionStatus(action);
18017
+ const UIRenderedPromise = useUIRenderedPromise(action);
18018
+ const [errorBoundary, resetErrorBoundary] = useErrorBoundary();
18019
+
18020
+ // Mark this action as bound to UI components (has renderers)
18021
+ // This tells the action system that errors should be caught and stored
18022
+ // in the action's error state rather than bubbling up
18023
+ useLayoutEffect(() => {
18024
+ if (action) {
18025
+ const {
18026
+ ui
18027
+ } = getActionPrivateProperties(action);
18028
+ ui.hasRenderers = true;
18029
+ }
18030
+ }, [action]);
18031
+ useLayoutEffect(() => {
18032
+ resetErrorBoundary();
18033
+ }, [action, loading, idle, resetErrorBoundary]);
18034
+ useLayoutEffect(() => {
18035
+ UIRenderedPromise.resolve();
18036
+ return () => {
18037
+ actionUIRenderedPromiseWeakMap.delete(action);
18038
+ };
18039
+ }, [action]);
18040
+
18041
+ // If renderAlways is provided, it wins and handles all rendering
18042
+ if (renderAlways) {
18043
+ return renderAlways({
18044
+ idle,
18045
+ loading,
18046
+ aborted,
18047
+ error,
18048
+ data
18049
+ });
18050
+ }
18051
+ if (idle) {
18052
+ return renderIdle(action);
18053
+ }
18054
+ if (errorBoundary) {
18055
+ return renderError(errorBoundary, "ui_error", action);
18056
+ }
18057
+ if (error) {
18058
+ return renderError(error, "action_error", action);
18059
+ }
18060
+ if (aborted) {
18061
+ return renderAborted(action);
18062
+ }
18063
+ let renderCompletedSafe;
18064
+ if (renderCompleted) {
18065
+ renderCompletedSafe = renderCompleted;
18066
+ } else {
18067
+ const {
18068
+ ui
18069
+ } = getActionPrivateProperties(action);
18070
+ if (ui.renderCompleted) {
18071
+ renderCompletedSafe = ui.renderCompleted;
18072
+ } else {
18073
+ renderCompletedSafe = renderCompletedDefault;
18074
+ }
18075
+ }
18076
+ if (loading) {
18077
+ if (action.canDisplayOldData && data !== undefined) {
18078
+ return renderCompletedSafe(data, action);
18079
+ }
18080
+ return renderLoading(action);
18081
+ }
18082
+ return renderCompletedSafe(data, action);
18083
+ };
18084
+ const defaultPromise = Promise.resolve();
18085
+ defaultPromise.resolve = () => {};
18086
+ const actionUIRenderedPromiseWeakMap = new WeakMap();
18087
+ const useUIRenderedPromise = action => {
18088
+ if (!action) {
18089
+ return defaultPromise;
18090
+ }
18091
+ const actionUIRenderedPromise = actionUIRenderedPromiseWeakMap.get(action);
18092
+ if (actionUIRenderedPromise) {
18093
+ return actionUIRenderedPromise;
18094
+ }
18095
+ let resolve;
18096
+ const promise = new Promise(res => {
18097
+ resolve = res;
18098
+ });
18099
+ promise.resolve = resolve;
18100
+ actionUIRenderedPromiseWeakMap.set(action, promise);
18101
+ return promise;
18102
+ };
18103
+
18104
+ const useFocusGroup = (
18105
+ elementRef,
18106
+ { enabled = true, direction, skipTab, loop, name } = {},
18107
+ ) => {
18108
+ useLayoutEffect(() => {
18109
+ if (!enabled) {
18110
+ return null;
18111
+ }
18112
+ const focusGroup = initFocusGroup(elementRef.current, {
18113
+ direction,
18114
+ skipTab,
18115
+ loop,
18116
+ name,
18117
+ });
18118
+ return focusGroup.cleanup;
18119
+ }, [direction, skipTab, loop, name]);
18120
+ };
18121
+
18122
+ installImportMetaCss(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
18123
+ const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
18124
+ import.meta.css = /* css */`
18125
+ .summary_marker {
18126
+ width: 1em;
18127
+ height: 1em;
18128
+ line-height: 1em;
18129
+ }
18130
+ .summary_marker_svg .arrow {
18131
+ animation-duration: 0.3s;
18132
+ animation-fill-mode: forwards;
18133
+ animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
18134
+ }
18135
+ .summary_marker_svg .arrow[data-animation-target="down"] {
18136
+ animation-name: morph-to-down;
18137
+ }
18138
+ @keyframes morph-to-down {
18139
+ from {
18140
+ d: path("${rightArrowPath}");
18141
+ }
18142
+ to {
18143
+ d: path("${downArrowPath}");
18144
+ }
18145
+ }
18146
+ .summary_marker_svg .arrow[data-animation-target="right"] {
18147
+ animation-name: morph-to-right;
18148
+ }
18149
+ @keyframes morph-to-right {
18150
+ from {
18151
+ d: path("${downArrowPath}");
18152
+ }
18153
+ to {
18154
+ d: path("${rightArrowPath}");
18155
+ }
18156
+ }
18157
+
18158
+ .summary_marker_svg .foreground_circle {
18159
+ stroke-dasharray: 503 1507; /* ~25% of circle perimeter */
18160
+ stroke-dashoffset: 0;
18161
+ animation: progress-around-circle 1.5s linear infinite;
18162
+ }
18163
+ @keyframes progress-around-circle {
18164
+ 0% {
18165
+ stroke-dashoffset: 0;
18166
+ }
18167
+ 100% {
18168
+ stroke-dashoffset: -2010;
18169
+ }
18170
+ }
18171
+
18172
+ /* fading and scaling */
18173
+ .summary_marker_svg .arrow {
18174
+ transition: opacity 0.3s ease-in-out;
18175
+ opacity: 1;
18176
+ }
18177
+ .summary_marker_svg .loading_container {
18178
+ transition: transform 0.3s linear;
18179
+ transform: scale(0.3);
18180
+ }
18181
+ .summary_marker_svg .background_circle,
18182
+ .summary_marker_svg .foreground_circle {
18183
+ transition: opacity 0.3s ease-in-out;
18184
+ opacity: 0;
18185
+ }
18186
+ .summary_marker_svg[data-loading] .arrow {
18187
+ opacity: 0;
18188
+ }
18189
+ .summary_marker_svg[data-loading] .loading_container {
18190
+ transform: scale(1);
18191
+ }
18192
+ .summary_marker_svg[data-loading] .background_circle {
18193
+ opacity: 0.2;
18194
+ }
18195
+ .summary_marker_svg[data-loading] .foreground_circle {
18196
+ opacity: 1;
18197
+ }
18198
+ `;
18199
+ const SummaryMarker = ({
18200
+ open,
18201
+ loading
18202
+ }) => {
18203
+ const showLoading = useDebounceTrue(loading, 300);
18204
+ const mountedRef = useRef(false);
18205
+ const prevOpenRef = useRef(open);
18206
+ useLayoutEffect(() => {
18207
+ mountedRef.current = true;
18208
+ return () => {
18209
+ mountedRef.current = false;
18210
+ };
18211
+ }, []);
18212
+ const shouldAnimate = mountedRef.current && prevOpenRef.current !== open;
18213
+ prevOpenRef.current = open;
18214
+ return jsx("span", {
18215
+ className: "summary_marker",
18216
+ children: jsxs("svg", {
18217
+ className: "summary_marker_svg",
18218
+ viewBox: "0 -960 960 960",
18219
+ xmlns: "http://www.w3.org/2000/svg",
18220
+ "data-loading": open ? showLoading || undefined : undefined,
18221
+ children: [jsxs("g", {
18222
+ className: "loading_container",
18223
+ "transform-origin": "480px -480px",
18224
+ children: [jsx("circle", {
18225
+ className: "background_circle",
18226
+ cx: "480",
18227
+ cy: "-480",
18228
+ r: "320",
18229
+ stroke: "currentColor",
18230
+ fill: "none",
18231
+ strokeWidth: "60",
18232
+ opacity: "0.2"
18233
+ }), jsx("circle", {
18234
+ className: "foreground_circle",
18235
+ cx: "480",
18236
+ cy: "-480",
18237
+ r: "320",
18238
+ stroke: "currentColor",
18239
+ fill: "none",
18240
+ strokeWidth: "60",
18241
+ strokeLinecap: "round",
18242
+ strokeDasharray: "503 1507"
18243
+ })]
18244
+ }), jsx("g", {
18245
+ className: "arrow_container",
18246
+ "transform-origin": "480px -480px",
18247
+ children: jsx("path", {
18248
+ className: "arrow",
18249
+ fill: "currentColor",
18250
+ "data-animation-target": shouldAnimate ? open ? "down" : "right" : undefined,
18251
+ d: open ? downArrowPath : rightArrowPath
18252
+ })
18253
+ })]
18254
+ })
18255
+ });
18256
+ };
18257
+
18258
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
18259
+ .navi_details {
18260
+ position: relative;
18261
+ z-index: 1;
18262
+ display: flex;
18263
+ flex-shrink: 0;
18264
+ flex-direction: column;
18265
+ }
18266
+
18267
+ .navi_details > summary {
18268
+ display: flex;
18269
+ flex-shrink: 0;
18270
+ flex-direction: column;
18271
+ cursor: pointer;
18272
+ user-select: none;
18273
+ }
18274
+ .summary_body {
18275
+ display: flex;
18276
+ width: 100%;
18277
+ flex-direction: row;
18278
+ align-items: center;
18279
+ gap: 0.2em;
18280
+ }
18281
+ .summary_label {
18282
+ display: flex;
18283
+ padding-right: 10px;
18284
+ flex: 1;
18285
+ align-items: center;
18286
+ gap: 0.2em;
18287
+ }
18288
+
18289
+ .navi_details > summary:focus {
18290
+ z-index: 1;
18109
18291
  }
18110
18292
  `;
18111
- const Select = forwardRef((props, ref) => {
18112
- const select = renderActionableComponent(props, ref);
18113
- return select;
18114
- });
18115
- const SelectControlled = forwardRef((props, ref) => {
18116
- const {
18117
- name,
18118
- value,
18119
- loading,
18120
- disabled,
18121
- readOnly,
18122
- children,
18123
- ...rest
18124
- } = props;
18125
- const innerRef = useRef();
18126
- useImperativeHandle(ref, () => innerRef.current);
18127
- const selectElement = jsx("select", {
18128
- className: "navi_select",
18129
- ref: innerRef,
18130
- "data-readonly": readOnly && !disabled ? "" : undefined,
18131
- onKeyDown: e => {
18132
- if (readOnly) {
18133
- e.preventDefault();
18134
- }
18135
- },
18136
- ...rest,
18137
- children: children.map(child => {
18138
- const {
18139
- label,
18140
- readOnly: childReadOnly,
18141
- disabled: childDisabled,
18142
- loading: childLoading,
18143
- value: childValue,
18144
- ...childRest
18145
- } = child;
18146
- return jsx("option", {
18147
- name: name,
18148
- value: childValue,
18149
- selected: childValue === value,
18150
- readOnly: readOnly || childReadOnly,
18151
- disabled: disabled || childDisabled,
18152
- loading: loading || childLoading,
18153
- ...childRest,
18154
- children: label
18155
- }, childValue);
18156
- })
18157
- });
18158
- return jsx(LoaderBackground, {
18159
- loading: loading,
18160
- color: "light-dark(#355fcc, #3b82f6)",
18161
- inset: -1,
18162
- children: selectElement
18163
- });
18293
+ const Details = forwardRef((props, ref) => {
18294
+ return renderActionableComponent(props, ref);
18164
18295
  });
18165
- forwardRef((props, ref) => {
18296
+ const DetailsBasic = forwardRef((props, ref) => {
18166
18297
  const {
18167
- value: initialValue,
18168
18298
  id,
18299
+ label = "Summary",
18300
+ open,
18301
+ loading,
18302
+ className,
18303
+ focusGroup,
18304
+ focusGroupDirection,
18305
+ arrowKeyShortcuts = true,
18306
+ openKeyShortcut = "ArrowRight",
18307
+ closeKeyShortcut = "ArrowLeft",
18308
+ onToggle,
18169
18309
  children,
18170
18310
  ...rest
18171
18311
  } = props;
18172
18312
  const innerRef = useRef();
18173
18313
  useImperativeHandle(ref, () => innerRef.current);
18174
18314
  const [navState, setNavState] = useNavState(id);
18175
- const valueAtStart = navState === undefined ? initialValue : navState;
18176
- const [value, setValue] = useState(valueAtStart);
18177
- useEffect(() => {
18178
- setNavState(value);
18179
- }, [value]);
18180
- return jsx(SelectControlled, {
18181
- ref: innerRef,
18182
- value: value,
18183
- onChange: event => {
18184
- const select = event.target;
18185
- const selectedValue = select.value;
18186
- setValue(selectedValue);
18187
- },
18188
- ...rest,
18189
- children: children
18190
- });
18191
- });
18192
- forwardRef((props, ref) => {
18193
- const {
18194
- id,
18195
- name,
18196
- value: externalValue,
18197
- valueSignal,
18198
- action,
18199
- children,
18200
- onCancel,
18201
- onActionPrevented,
18202
- onActionStart,
18203
- onActionAbort,
18204
- onActionError,
18205
- onActionEnd,
18206
- actionErrorEffect,
18207
- ...rest
18208
- } = props;
18209
- const innerRef = useRef();
18210
- useImperativeHandle(ref, () => innerRef.current);
18211
- const [navState, setNavState, resetNavState] = useNavState(id);
18212
- const [boundAction, value, setValue, initialValue] = useActionBoundToOneParam(action, name);
18213
- const {
18214
- loading: actionLoading
18215
- } = useActionStatus(boundAction);
18216
- const executeAction = useExecuteAction(innerRef, {
18217
- errorEffect: actionErrorEffect
18218
- });
18219
- useEffect(() => {
18220
- setNavState(value);
18221
- }, [value]);
18222
- const actionRequesterRef = useRef(null);
18223
- useActionEvents(innerRef, {
18224
- onCancel: (e, reason) => {
18225
- resetNavState();
18226
- setValue(initialValue);
18227
- onCancel?.(e, reason);
18228
- },
18229
- onPrevented: onActionPrevented,
18230
- onAction: actionEvent => {
18231
- actionRequesterRef.current = actionEvent.detail.requester;
18232
- executeAction(actionEvent);
18233
- },
18234
- onStart: onActionStart,
18235
- onAbort: e => {
18236
- setValue(initialValue);
18237
- onActionAbort?.(e);
18238
- },
18239
- onError: error => {
18240
- setValue(initialValue);
18241
- onActionError?.(error);
18242
- },
18243
- onEnd: () => {
18244
- resetNavState();
18245
- onActionEnd?.();
18246
- }
18315
+ const [innerOpen, innerOpenSetter] = useState(open || navState);
18316
+ useFocusGroup(innerRef, {
18317
+ enabled: focusGroup,
18318
+ name: typeof focusGroup === "string" ? focusGroup : undefined,
18319
+ direction: focusGroupDirection
18247
18320
  });
18248
- const childRefArray = useRefArray(children, child => child.value);
18249
- return jsx(SelectControlled, {
18250
- ref: innerRef,
18251
- name: name,
18252
- value: value,
18253
- "data-action": boundAction,
18254
- onChange: event => {
18255
- const select = event.target;
18256
- const selectedValue = select.value;
18257
- setValue(selectedValue);
18258
- const radioListContainer = innerRef.current;
18259
- const optionSelected = select.querySelector(`option[value="${selectedValue}"]`);
18260
- requestAction(radioListContainer, boundAction, {
18261
- event,
18262
- requester: optionSelected
18321
+
18322
+ /**
18323
+ * Browser will dispatch "toggle" event even if we set open={true}
18324
+ * When rendering the component for the first time
18325
+ * We have to ensure the initial "toggle" event is ignored.
18326
+ *
18327
+ * If we don't do that code will think the details has changed and run logic accordingly
18328
+ * For example it will try to navigate to the current url while we are already there
18329
+ *
18330
+ * See:
18331
+ * - https://techblog.thescore.com/2024/10/08/why-we-decided-to-change-how-the-details-element-works/
18332
+ * - https://github.com/whatwg/html/issues/4500
18333
+ * - https://stackoverflow.com/questions/58942600/react-html-details-toggles-uncontrollably-when-starts-open
18334
+ *
18335
+ */
18336
+
18337
+ const summaryRef = useRef(null);
18338
+ useKeyboardShortcuts(innerRef, [{
18339
+ key: openKeyShortcut,
18340
+ enabled: arrowKeyShortcuts,
18341
+ when: e => document.activeElement === summaryRef.current &&
18342
+ // avoid handling openKeyShortcut twice when keydown occurs inside nested details
18343
+ !e.defaultPrevented,
18344
+ action: e => {
18345
+ const details = innerRef.current;
18346
+ if (!details.open) {
18347
+ e.preventDefault();
18348
+ details.open = true;
18349
+ return;
18350
+ }
18351
+ const summary = summaryRef.current;
18352
+ const firstFocusableElementInDetails = findAfter(summary, elementIsFocusable, {
18353
+ root: details
18263
18354
  });
18355
+ if (!firstFocusableElementInDetails) {
18356
+ return;
18357
+ }
18358
+ e.preventDefault();
18359
+ firstFocusableElementInDetails.focus();
18360
+ }
18361
+ }, {
18362
+ key: closeKeyShortcut,
18363
+ enabled: arrowKeyShortcuts,
18364
+ when: () => {
18365
+ const details = innerRef.current;
18366
+ return details.open;
18264
18367
  },
18368
+ action: e => {
18369
+ const details = innerRef.current;
18370
+ const summary = summaryRef.current;
18371
+ if (document.activeElement === summary) {
18372
+ e.preventDefault();
18373
+ summary.focus();
18374
+ details.open = false;
18375
+ } else {
18376
+ e.preventDefault();
18377
+ summary.focus();
18378
+ }
18379
+ }
18380
+ }]);
18381
+ const mountedRef = useRef(false);
18382
+ useEffect(() => {
18383
+ mountedRef.current = true;
18384
+ }, []);
18385
+ return jsxs("details", {
18265
18386
  ...rest,
18266
- children: children.map((child, i) => {
18267
- const childRef = childRefArray[i];
18268
- return {
18269
- ...child,
18270
- ref: childRef,
18271
- loading: child.loading || actionLoading && actionRequesterRef.current === childRef.current,
18272
- readOnly: child.readOnly || actionLoading
18273
- };
18274
- })
18387
+ ref: innerRef,
18388
+ id: id,
18389
+ className: ["navi_details", ...(className ? className.split(" ") : [])].join(" "),
18390
+ onToggle: e => {
18391
+ const isOpen = e.newState === "open";
18392
+ if (mountedRef.current) {
18393
+ if (isOpen) {
18394
+ innerOpenSetter(true);
18395
+ setNavState(true);
18396
+ } else {
18397
+ innerOpenSetter(false);
18398
+ setNavState(undefined);
18399
+ }
18400
+ }
18401
+ onToggle?.(e);
18402
+ },
18403
+ open: innerOpen,
18404
+ children: [jsx("summary", {
18405
+ ref: summaryRef,
18406
+ children: jsxs("div", {
18407
+ className: "summary_body",
18408
+ children: [jsx(SummaryMarker, {
18409
+ open: innerOpen,
18410
+ loading: loading
18411
+ }), jsx("div", {
18412
+ className: "summary_label",
18413
+ children: label
18414
+ })]
18415
+ })
18416
+ }), children]
18275
18417
  });
18276
18418
  });
18277
18419
  forwardRef((props, ref) => {
18278
18420
  const {
18279
- id,
18280
- name,
18281
- value: externalValue,
18421
+ action,
18422
+ loading,
18423
+ onToggle,
18424
+ onActionPrevented,
18425
+ onActionStart,
18426
+ onActionError,
18427
+ onActionEnd,
18282
18428
  children,
18283
18429
  ...rest
18284
18430
  } = props;
18285
18431
  const innerRef = useRef();
18286
18432
  useImperativeHandle(ref, () => innerRef.current);
18287
- const [navState, setNavState] = useNavState(id);
18288
- const [value, setValue, initialValue] = [name, externalValue, navState];
18289
- useEffect(() => {
18290
- setNavState(value);
18291
- }, [value]);
18292
- useFormEvents(innerRef, {
18293
- onFormReset: () => {
18294
- setValue(undefined);
18295
- },
18296
- onFormActionAbort: () => {
18297
- setValue(initialValue);
18433
+ const effectiveAction = useAction(action);
18434
+ const {
18435
+ loading: actionLoading
18436
+ } = useActionStatus(effectiveAction);
18437
+ const executeAction = useExecuteAction(innerRef, {
18438
+ // the error will be displayed by actionRenderer inside <details>
18439
+ errorEffect: "none"
18440
+ });
18441
+ useActionEvents(innerRef, {
18442
+ onPrevented: onActionPrevented,
18443
+ onAction: e => {
18444
+ executeAction(e);
18298
18445
  },
18299
- onFormActionError: () => {
18300
- setValue(initialValue);
18301
- }
18446
+ onStart: onActionStart,
18447
+ onError: onActionError,
18448
+ onEnd: onActionEnd
18302
18449
  });
18303
- return jsx(SelectControlled, {
18450
+ return jsx(DetailsBasic, {
18451
+ ...rest,
18304
18452
  ref: innerRef,
18305
- name: name,
18306
- value: value,
18307
- onChange: event => {
18308
- const select = event.target;
18309
- const selectedValue = select.checked;
18310
- setValue(selectedValue);
18453
+ loading: loading || actionLoading,
18454
+ onToggle: toggleEvent => {
18455
+ const isOpen = toggleEvent.newState === "open";
18456
+ if (isOpen) {
18457
+ requestAction(toggleEvent.target, effectiveAction, {
18458
+ event: toggleEvent,
18459
+ method: "run"
18460
+ });
18461
+ } else {
18462
+ effectiveAction.abort();
18463
+ }
18464
+ onToggle?.(toggleEvent);
18311
18465
  },
18312
- ...rest,
18313
- children: children
18466
+ children: jsx(ActionRenderer, {
18467
+ action: effectiveAction,
18468
+ children: children
18469
+ })
18314
18470
  });
18315
18471
  });
18316
18472
 
@@ -21678,116 +21834,6 @@ const useCellsAndColumns = (cells, columns) => {
21678
21834
  };
21679
21835
  };
21680
21836
 
21681
- installImportMetaCss(import.meta);import.meta.css = /* css */`
21682
- .navi_tablist {
21683
- display: flex;
21684
- overflow-x: auto;
21685
- overflow-y: hidden;
21686
- justify-content: space-between;
21687
- }
21688
-
21689
- .navi_tablist > ul {
21690
- align-items: center;
21691
- display: flex;
21692
- gap: 0.5rem;
21693
- list-style: none;
21694
- margin: 0;
21695
- padding: 0;
21696
- }
21697
-
21698
- .navi_tablist > ul > li {
21699
- display: inline-flex;
21700
- position: relative;
21701
- }
21702
-
21703
- .navi_tab {
21704
- white-space: nowrap;
21705
- display: flex;
21706
- flex-direction: column;
21707
- }
21708
-
21709
- .navi_tab_content {
21710
- transition: background 0.12s ease-out;
21711
- border-radius: 6px;
21712
- text-decoration: none;
21713
- line-height: 30px;
21714
- display: flex;
21715
- padding: 0 0.5rem;
21716
- }
21717
-
21718
- .navi_tab:hover .navi_tab_content {
21719
- background: #dae0e7;
21720
- color: #010409;
21721
- }
21722
-
21723
- .navi_tab .active_marker {
21724
- display: flex;
21725
- background: transparent;
21726
- border-radius: 0.1px;
21727
- width: 100%;
21728
- z-index: 1;
21729
- height: 2px;
21730
- margin-top: 5px;
21731
- }
21732
-
21733
- /* Hidden bold clone to reserve space for bold width without affecting height */
21734
- .navi_tab_content_bold_clone {
21735
- font-weight: 600; /* force bold to compute max width */
21736
- visibility: hidden; /* not visible */
21737
- display: block; /* in-flow so it contributes to width */
21738
- height: 0; /* zero height so it doesn't change layout height */
21739
- overflow: hidden; /* avoid any accidental height */
21740
- pointer-events: none; /* inert */
21741
- }
21742
-
21743
- .navi_tab[aria-selected="true"] .active_marker {
21744
- background: rgb(205, 52, 37);
21745
- }
21746
-
21747
- .navi_tab[aria-selected="true"] .navi_tab_content {
21748
- font-weight: 600;
21749
- }
21750
- `;
21751
- const TabList = ({
21752
- children,
21753
- ...props
21754
- }) => {
21755
- return jsx("nav", {
21756
- className: "navi_tablist",
21757
- role: "tablist",
21758
- ...props,
21759
- children: jsx("ul", {
21760
- children: children.map(child => {
21761
- return jsx("li", {
21762
- children: child
21763
- }, child.props.key);
21764
- })
21765
- })
21766
- });
21767
- };
21768
- const Tab = ({
21769
- children,
21770
- selected,
21771
- ...props
21772
- }) => {
21773
- return jsxs("div", {
21774
- className: "navi_tab",
21775
- role: "tab",
21776
- "aria-selected": selected ? "true" : "false",
21777
- ...props,
21778
- children: [jsx("div", {
21779
- className: "navi_tab_content",
21780
- children: children
21781
- }), jsx("div", {
21782
- className: "navi_tab_content_bold_clone",
21783
- "aria-hidden": "true",
21784
- children: children
21785
- }), jsx("span", {
21786
- className: "active_marker"
21787
- })]
21788
- });
21789
- };
21790
-
21791
21837
  /**
21792
21838
  * Creates a signal that stays synchronized with an external value,
21793
21839
  * only updating the signal when the value actually changes.
@@ -22357,48 +22403,6 @@ const ViewportLayout = props => {
22357
22403
  });
22358
22404
  };
22359
22405
 
22360
- const createUniqueValueConstraint = (
22361
- // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
22362
- // but this is already nice to help user with what we know
22363
- // it's also possible that front is unsync with backend, preventing user to choose a value
22364
- // that is actually free.
22365
- // But this is unlikely to happen and user could reload the page to be able to choose that name
22366
- // that suddenly became available
22367
- existingValueSet,
22368
- message = `"{value}" already exists. Please choose another value.`,
22369
- ) => {
22370
- return {
22371
- name: "unique_value",
22372
- check: (input) => {
22373
- const inputValue = input.value;
22374
- const hasConflict = existingValueSet.has(inputValue);
22375
- // console.log({
22376
- // inputValue,
22377
- // names: Array.from(otherNameSet.values()),
22378
- // hasConflict,
22379
- // });
22380
- if (hasConflict) {
22381
- return message.replace("{value}", inputValue);
22382
- }
22383
- return "";
22384
- },
22385
- };
22386
- };
22387
-
22388
- const SINGLE_SPACE_CONSTRAINT = {
22389
- name: "single_space",
22390
- check: (input) => {
22391
- const inputValue = input.value;
22392
- const hasLeadingSpace = inputValue.startsWith(" ");
22393
- const hasTrailingSpace = inputValue.endsWith(" ");
22394
- const hasDoubleSpace = inputValue.includes(" ");
22395
- if (hasLeadingSpace || hasDoubleSpace || hasTrailingSpace) {
22396
- return "Spaces at the beginning, end, or consecutive spaces are not allowed";
22397
- }
22398
- return "";
22399
- },
22400
- };
22401
-
22402
22406
  /*
22403
22407
  * - Usage
22404
22408
  * useEffect(() => {