@dloizides/ui-forms 1.13.4 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/index.d.mts +95 -18
- package/dist/index.d.ts +95 -18
- package/dist/index.js +519 -243
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +520 -246
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ var reactNative = require('react-native');
|
|
|
5
5
|
var uiFeedback = require('@dloizides/ui-feedback');
|
|
6
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
7
|
var uiButtons = require('@dloizides/ui-buttons');
|
|
8
|
-
var a11y = require('@dloizides/a11y');
|
|
9
8
|
var reactDom = require('react-dom');
|
|
9
|
+
var a11y = require('@dloizides/a11y');
|
|
10
10
|
|
|
11
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
|
|
@@ -465,6 +465,142 @@ var controlStyles = reactNative.StyleSheet.create({
|
|
|
465
465
|
error: { fontSize: ERROR_FONT, marginTop: ERROR_GAP }
|
|
466
466
|
});
|
|
467
467
|
var IS_WEB3 = reactNative.Platform.OS === "web";
|
|
468
|
+
var MENU_PORTAL_Z_INDEX = 1e3;
|
|
469
|
+
var MENU_TOP_GAP2 = 4;
|
|
470
|
+
function toDomNode(ref) {
|
|
471
|
+
const node = ref.current;
|
|
472
|
+
if (node === null || typeof node.getBoundingClientRect !== "function") return null;
|
|
473
|
+
return node;
|
|
474
|
+
}
|
|
475
|
+
function readAnchorRect(ref) {
|
|
476
|
+
if (!IS_WEB3) return null;
|
|
477
|
+
const node = toDomNode(ref);
|
|
478
|
+
if (node === null) return null;
|
|
479
|
+
const r = node.getBoundingClientRect();
|
|
480
|
+
return { top: r.top, left: r.left, width: r.width, bottom: r.bottom };
|
|
481
|
+
}
|
|
482
|
+
function buildPortalPopoverStyle(rect) {
|
|
483
|
+
return {
|
|
484
|
+
// RN's ViewStyle union omits 'fixed', but react-native-web honours it at runtime.
|
|
485
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- web-only position value
|
|
486
|
+
position: "fixed",
|
|
487
|
+
top: rect.bottom + MENU_TOP_GAP2,
|
|
488
|
+
left: rect.left,
|
|
489
|
+
width: rect.width,
|
|
490
|
+
zIndex: MENU_PORTAL_Z_INDEX
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function useAnchorRect(ref, enabled) {
|
|
494
|
+
const [rect, setRect] = React2.useState(null);
|
|
495
|
+
React2.useLayoutEffect(() => {
|
|
496
|
+
if (!IS_WEB3 || !enabled) {
|
|
497
|
+
setRect(null);
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
let frame = null;
|
|
501
|
+
let disposed = false;
|
|
502
|
+
const apply = () => {
|
|
503
|
+
frame = null;
|
|
504
|
+
if (disposed) return;
|
|
505
|
+
setRect(readAnchorRect(ref));
|
|
506
|
+
};
|
|
507
|
+
const schedule = () => {
|
|
508
|
+
if (frame !== null) return;
|
|
509
|
+
frame = window.requestAnimationFrame(apply);
|
|
510
|
+
};
|
|
511
|
+
apply();
|
|
512
|
+
document.addEventListener("scroll", schedule, true);
|
|
513
|
+
window.addEventListener("resize", schedule);
|
|
514
|
+
const node = toDomNode(ref);
|
|
515
|
+
const observer = typeof ResizeObserver === "function" ? new ResizeObserver(schedule) : null;
|
|
516
|
+
if (observer !== null) {
|
|
517
|
+
if (node !== null) observer.observe(node);
|
|
518
|
+
observer.observe(document.body);
|
|
519
|
+
}
|
|
520
|
+
return () => {
|
|
521
|
+
disposed = true;
|
|
522
|
+
if (frame !== null) window.cancelAnimationFrame(frame);
|
|
523
|
+
document.removeEventListener("scroll", schedule, true);
|
|
524
|
+
window.removeEventListener("resize", schedule);
|
|
525
|
+
if (observer !== null) observer.disconnect();
|
|
526
|
+
};
|
|
527
|
+
}, [ref, enabled]);
|
|
528
|
+
return IS_WEB3 && enabled ? rect : null;
|
|
529
|
+
}
|
|
530
|
+
var IS_WEB4 = typeof document !== "undefined";
|
|
531
|
+
var ESCAPE_KEY = "Escape";
|
|
532
|
+
var ACTIVE_WEIGHT = "700";
|
|
533
|
+
var IDLE_WEIGHT = "400";
|
|
534
|
+
function useDismiss(anchorRef, menuRef, onDismiss) {
|
|
535
|
+
React2.useEffect(() => {
|
|
536
|
+
if (!IS_WEB4) return void 0;
|
|
537
|
+
const onMouseDown = (event) => {
|
|
538
|
+
const anchorNode = anchorRef.current;
|
|
539
|
+
const menuNode = menuRef.current;
|
|
540
|
+
const target = event.target instanceof Node ? event.target : null;
|
|
541
|
+
const insideAnchor = anchorNode !== null && target !== null && anchorNode.contains(target);
|
|
542
|
+
const insideMenu = menuNode !== null && target !== null && menuNode.contains(target);
|
|
543
|
+
if (!insideAnchor && !insideMenu) onDismiss();
|
|
544
|
+
};
|
|
545
|
+
const onKeyDown = (event) => {
|
|
546
|
+
if (event.key === ESCAPE_KEY) onDismiss();
|
|
547
|
+
};
|
|
548
|
+
document.addEventListener("mousedown", onMouseDown);
|
|
549
|
+
document.addEventListener("keydown", onKeyDown);
|
|
550
|
+
return () => {
|
|
551
|
+
document.removeEventListener("mousedown", onMouseDown);
|
|
552
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
553
|
+
};
|
|
554
|
+
}, [anchorRef, menuRef, onDismiss]);
|
|
555
|
+
}
|
|
556
|
+
function AnchoredMenu({
|
|
557
|
+
options,
|
|
558
|
+
selectedValue,
|
|
559
|
+
onSelect,
|
|
560
|
+
onDismiss,
|
|
561
|
+
colors,
|
|
562
|
+
optionHint,
|
|
563
|
+
accessibilityLabel,
|
|
564
|
+
testID,
|
|
565
|
+
anchorRef,
|
|
566
|
+
usePortal = true
|
|
567
|
+
}) {
|
|
568
|
+
const menuRef = React2__default.default.useRef(null);
|
|
569
|
+
useDismiss(anchorRef, menuRef, onDismiss);
|
|
570
|
+
const rect = useAnchorRect(anchorRef, usePortal);
|
|
571
|
+
const portalStyle = rect !== null ? buildPortalPopoverStyle(rect) : null;
|
|
572
|
+
const menu = /* @__PURE__ */ jsxRuntime.jsx(
|
|
573
|
+
reactNative.View,
|
|
574
|
+
{
|
|
575
|
+
ref: menuRef,
|
|
576
|
+
accessibilityLabel,
|
|
577
|
+
accessibilityRole: "menu",
|
|
578
|
+
style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }, portalStyle],
|
|
579
|
+
testID: `${testID}-menu`,
|
|
580
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { keyboardShouldPersistTaps: "handled", nestedScrollEnabled: true, children: options.map((opt) => {
|
|
581
|
+
const active = opt.value === selectedValue;
|
|
582
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
583
|
+
reactNative.Pressable,
|
|
584
|
+
{
|
|
585
|
+
accessibilityRole: "menuitem",
|
|
586
|
+
accessibilityLabel: opt.label,
|
|
587
|
+
accessibilityHint: optionHint,
|
|
588
|
+
accessibilityState: { selected: active },
|
|
589
|
+
"aria-selected": active,
|
|
590
|
+
onPress: () => onSelect(opt.value),
|
|
591
|
+
style: controlStyles.option,
|
|
592
|
+
testID: `${testID}-option-${opt.value}`,
|
|
593
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [controlStyles.optionText, { color: active ? colors.brand : colors.text, fontWeight: active ? ACTIVE_WEIGHT : IDLE_WEIGHT }], children: opt.label })
|
|
594
|
+
},
|
|
595
|
+
opt.value
|
|
596
|
+
);
|
|
597
|
+
}) })
|
|
598
|
+
}
|
|
599
|
+
);
|
|
600
|
+
if (usePortal && IS_WEB4) return reactDom.createPortal(menu, document.body);
|
|
601
|
+
return menu;
|
|
602
|
+
}
|
|
603
|
+
var IS_WEB5 = reactNative.Platform.OS === "web";
|
|
468
604
|
var TRANSITION_PROPERTY = "border-color, background-color, box-shadow";
|
|
469
605
|
var TRANSITION_DURATION = "0.14s";
|
|
470
606
|
var TRANSITION_TIMING = "ease";
|
|
@@ -480,7 +616,7 @@ var GREEN_START = 3;
|
|
|
480
616
|
var BLUE_START = 5;
|
|
481
617
|
var CHANNEL_END_OFFSET = 2;
|
|
482
618
|
function prefersReducedMotion() {
|
|
483
|
-
if (!
|
|
619
|
+
if (!IS_WEB5 || typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
484
620
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
485
621
|
}
|
|
486
622
|
function withAlpha(color, alpha) {
|
|
@@ -523,7 +659,7 @@ function useThemedInput(options = {}) {
|
|
|
523
659
|
const borderColor = isFocused ? focusBorder : isHovered ? withAlpha(colors.text, HOVER_BORDER_ALPHA) : restBorder;
|
|
524
660
|
const backgroundColor = isFocused ? colors.surface : colors.surfaceElevated;
|
|
525
661
|
const base = { color: colors.text, borderColor, backgroundColor };
|
|
526
|
-
if (!
|
|
662
|
+
if (!IS_WEB5) return base;
|
|
527
663
|
const ringColor = hasError ? errorColor : primary;
|
|
528
664
|
const ring = isFocused ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(ringColor, FOCUS_RING_ALPHA)}` : "none";
|
|
529
665
|
const webStyle = {
|
|
@@ -548,7 +684,7 @@ function useThemedInput(options = {}) {
|
|
|
548
684
|
]);
|
|
549
685
|
const focusBind = React2.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
|
|
550
686
|
const hoverBind = React2.useMemo(
|
|
551
|
-
() =>
|
|
687
|
+
() => IS_WEB5 ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
|
|
552
688
|
[handleHoverIn, handleHoverOut]
|
|
553
689
|
);
|
|
554
690
|
return {
|
|
@@ -560,9 +696,9 @@ function useThemedInput(options = {}) {
|
|
|
560
696
|
hoverBind
|
|
561
697
|
};
|
|
562
698
|
}
|
|
563
|
-
var
|
|
699
|
+
var IS_WEB6 = reactNative.Platform.OS === "web";
|
|
564
700
|
function webFieldA11y({ describedById, hasError = false, required = false }) {
|
|
565
|
-
if (!
|
|
701
|
+
if (!IS_WEB6) {
|
|
566
702
|
return {};
|
|
567
703
|
}
|
|
568
704
|
return {
|
|
@@ -612,12 +748,187 @@ var ThemedTextInput = React2__default.default.forwardRef(function ThemedTextInpu
|
|
|
612
748
|
}
|
|
613
749
|
);
|
|
614
750
|
});
|
|
751
|
+
|
|
752
|
+
// src/useRecentValues/recentValuesCore.ts
|
|
753
|
+
var STORAGE_PREFIX = "@dloizides/ui-forms:recent:";
|
|
754
|
+
var DEFAULT_MAX_RECENTS = 10;
|
|
755
|
+
function recentStorageKey(key) {
|
|
756
|
+
return `${STORAGE_PREFIX}${key}`;
|
|
757
|
+
}
|
|
758
|
+
function getStorage() {
|
|
759
|
+
try {
|
|
760
|
+
if (typeof window === "undefined") return null;
|
|
761
|
+
const storage = window.localStorage;
|
|
762
|
+
return storage !== null && storage !== void 0 ? storage : null;
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function readRecents(key) {
|
|
768
|
+
const storage = getStorage();
|
|
769
|
+
if (storage === null) return [];
|
|
770
|
+
try {
|
|
771
|
+
const raw = storage.getItem(recentStorageKey(key));
|
|
772
|
+
if (raw === null) return [];
|
|
773
|
+
const parsed = JSON.parse(raw);
|
|
774
|
+
if (!Array.isArray(parsed)) return [];
|
|
775
|
+
return parsed.filter((entry) => typeof entry === "string");
|
|
776
|
+
} catch {
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function writeRecents(key, values) {
|
|
781
|
+
const storage = getStorage();
|
|
782
|
+
if (storage === null) return;
|
|
783
|
+
try {
|
|
784
|
+
storage.setItem(recentStorageKey(key), JSON.stringify(values));
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function clearRecents(key) {
|
|
789
|
+
const storage = getStorage();
|
|
790
|
+
if (storage === null) return;
|
|
791
|
+
try {
|
|
792
|
+
storage.removeItem(recentStorageKey(key));
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function computeNextRecents(current, value, max) {
|
|
797
|
+
const trimmed = value.trim();
|
|
798
|
+
if (trimmed === "") return [...current];
|
|
799
|
+
const lower = trimmed.toLowerCase();
|
|
800
|
+
const withoutMatch = current.filter((entry) => entry.toLowerCase() !== lower);
|
|
801
|
+
const promoted = [trimmed, ...withoutMatch];
|
|
802
|
+
const cap = max > 0 ? max : 0;
|
|
803
|
+
return promoted.slice(0, cap);
|
|
804
|
+
}
|
|
805
|
+
function recentsEqual(a, b) {
|
|
806
|
+
if (a.length !== b.length) return false;
|
|
807
|
+
return a.every((entry, index) => entry === b[index]);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/useRecentValues/useRecentValues.ts
|
|
811
|
+
function useRecentValues(key, max = DEFAULT_MAX_RECENTS) {
|
|
812
|
+
const [recents, setRecents] = React2.useState(() => readRecents(key).slice(0, max));
|
|
813
|
+
React2.useEffect(() => {
|
|
814
|
+
setRecents(readRecents(key).slice(0, max));
|
|
815
|
+
}, [key, max]);
|
|
816
|
+
const remember = React2.useCallback(
|
|
817
|
+
(value) => {
|
|
818
|
+
setRecents((current) => {
|
|
819
|
+
const next = computeNextRecents(current, value, max);
|
|
820
|
+
if (recentsEqual(next, current)) return current;
|
|
821
|
+
writeRecents(key, next);
|
|
822
|
+
return next;
|
|
823
|
+
});
|
|
824
|
+
},
|
|
825
|
+
[key, max]
|
|
826
|
+
);
|
|
827
|
+
const clear = React2.useCallback(() => {
|
|
828
|
+
clearRecents(key);
|
|
829
|
+
setRecents([]);
|
|
830
|
+
}, [key]);
|
|
831
|
+
return { recents, remember, clear };
|
|
832
|
+
}
|
|
833
|
+
var CLOSE_DELAY_MS = 140;
|
|
834
|
+
function RecentInputField({
|
|
835
|
+
recentKey,
|
|
836
|
+
recentMax,
|
|
837
|
+
label,
|
|
838
|
+
value,
|
|
839
|
+
testID,
|
|
840
|
+
onChangeText,
|
|
841
|
+
onSubmitEditing,
|
|
842
|
+
onFocus,
|
|
843
|
+
onBlur,
|
|
844
|
+
...rest
|
|
845
|
+
}) {
|
|
846
|
+
const { theme } = uiFeedback.useUi();
|
|
847
|
+
const { colors, palette } = theme;
|
|
848
|
+
const { recents, remember } = useRecentValues(recentKey, recentMax);
|
|
849
|
+
const anchorRef = React2.useRef(null);
|
|
850
|
+
const latestText = React2.useRef(typeof value === "string" ? value : "");
|
|
851
|
+
const [isOpen, setIsOpen] = React2.useState(false);
|
|
852
|
+
const handleChange = React2.useCallback(
|
|
853
|
+
(text) => {
|
|
854
|
+
latestText.current = text;
|
|
855
|
+
onChangeText?.(text);
|
|
856
|
+
},
|
|
857
|
+
[onChangeText]
|
|
858
|
+
);
|
|
859
|
+
const handleFocus = React2.useCallback(
|
|
860
|
+
(event) => {
|
|
861
|
+
setIsOpen(true);
|
|
862
|
+
onFocus?.(event);
|
|
863
|
+
},
|
|
864
|
+
[onFocus]
|
|
865
|
+
);
|
|
866
|
+
const handleBlur = React2.useCallback(
|
|
867
|
+
(event) => {
|
|
868
|
+
if (latestText.current.trim() !== "") remember(latestText.current);
|
|
869
|
+
setTimeout(() => setIsOpen(false), CLOSE_DELAY_MS);
|
|
870
|
+
onBlur?.(event);
|
|
871
|
+
},
|
|
872
|
+
[remember, onBlur]
|
|
873
|
+
);
|
|
874
|
+
const handleSubmit = React2.useCallback(
|
|
875
|
+
(event) => {
|
|
876
|
+
remember(latestText.current);
|
|
877
|
+
setIsOpen(false);
|
|
878
|
+
onSubmitEditing?.(event);
|
|
879
|
+
},
|
|
880
|
+
[remember, onSubmitEditing]
|
|
881
|
+
);
|
|
882
|
+
const pick = React2.useCallback(
|
|
883
|
+
(picked) => {
|
|
884
|
+
latestText.current = picked;
|
|
885
|
+
onChangeText?.(picked);
|
|
886
|
+
remember(picked);
|
|
887
|
+
setIsOpen(false);
|
|
888
|
+
},
|
|
889
|
+
[onChangeText, remember]
|
|
890
|
+
);
|
|
891
|
+
const options = recents.map((entry) => ({ label: entry, value: entry }));
|
|
892
|
+
const showMenu = isOpen && options.length > 0;
|
|
893
|
+
const recentTestID = testID !== void 0 ? `${testID}-recent` : "form-field-recent";
|
|
894
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ref: anchorRef, style: controlStyles.anchor, children: [
|
|
895
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
896
|
+
ThemedTextInput,
|
|
897
|
+
{
|
|
898
|
+
...rest,
|
|
899
|
+
testID,
|
|
900
|
+
value,
|
|
901
|
+
onChangeText: handleChange,
|
|
902
|
+
onFocus: handleFocus,
|
|
903
|
+
onBlur: handleBlur,
|
|
904
|
+
onSubmitEditing: handleSubmit
|
|
905
|
+
}
|
|
906
|
+
),
|
|
907
|
+
showMenu ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
908
|
+
AnchoredMenu,
|
|
909
|
+
{
|
|
910
|
+
accessibilityLabel: `Recent ${label} values`,
|
|
911
|
+
anchorRef,
|
|
912
|
+
colors: { text: colors.text, border: colors.border, surface: colors.surface, brand: palette.primary["500"] },
|
|
913
|
+
onDismiss: () => setIsOpen(false),
|
|
914
|
+
onSelect: pick,
|
|
915
|
+
optionHint: `Fill ${label} with this recent value`,
|
|
916
|
+
options,
|
|
917
|
+
selectedValue: typeof value === "string" ? value : "",
|
|
918
|
+
testID: recentTestID,
|
|
919
|
+
usePortal: true
|
|
920
|
+
}
|
|
921
|
+
) : null
|
|
922
|
+
] });
|
|
923
|
+
}
|
|
615
924
|
var FormField = ({
|
|
616
925
|
label,
|
|
617
926
|
labelHidden = false,
|
|
618
927
|
required = false,
|
|
619
928
|
error,
|
|
620
929
|
containerStyle,
|
|
930
|
+
recentKey,
|
|
931
|
+
recentMax,
|
|
621
932
|
...textInputProps
|
|
622
933
|
}) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
623
934
|
Field,
|
|
@@ -626,9 +937,8 @@ var FormField = ({
|
|
|
626
937
|
error,
|
|
627
938
|
label: labelHidden ? void 0 : label,
|
|
628
939
|
required,
|
|
629
|
-
children: ({ describedById, hasError, controlId }) =>
|
|
630
|
-
|
|
631
|
-
{
|
|
940
|
+
children: ({ describedById, hasError, controlId }) => {
|
|
941
|
+
const inputProps = {
|
|
632
942
|
accessibilityHint: `Enter ${label}`,
|
|
633
943
|
accessibilityLabel: label,
|
|
634
944
|
describedById,
|
|
@@ -637,60 +947,24 @@ var FormField = ({
|
|
|
637
947
|
requiredField: required,
|
|
638
948
|
testID: "form-field-input",
|
|
639
949
|
...textInputProps
|
|
640
|
-
}
|
|
641
|
-
|
|
950
|
+
};
|
|
951
|
+
return recentKey !== void 0 && recentKey !== "" ? /* @__PURE__ */ jsxRuntime.jsx(RecentInputField, { label, recentKey, recentMax, ...inputProps }) : /* @__PURE__ */ jsxRuntime.jsx(ThemedTextInput, { ...inputProps });
|
|
952
|
+
}
|
|
642
953
|
}
|
|
643
954
|
);
|
|
955
|
+
|
|
956
|
+
// src/ChipSelector/constants.ts
|
|
644
957
|
var TRANSPARENT_COLOR = "transparent";
|
|
645
|
-
var
|
|
958
|
+
var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
|
|
959
|
+
var CHIP_GUTTER = 8;
|
|
960
|
+
var DEFAULT_CHIP_TESTID_PREFIX = "chip-selector-chip";
|
|
646
961
|
var SOLID_RADIUS = 20;
|
|
647
962
|
var OUTLINE_RADIUS = 999;
|
|
648
963
|
var OUTLINE_PAD_H = 12;
|
|
649
964
|
var OUTLINE_PAD_V = 5;
|
|
650
965
|
var OUTLINE_FONT_SIZE = 13;
|
|
651
|
-
var
|
|
652
|
-
var
|
|
653
|
-
var HEX_RADIX2 = 16;
|
|
654
|
-
var RGB_R_START = 0;
|
|
655
|
-
var RGB_R_END = 2;
|
|
656
|
-
var RGB_G_END = 4;
|
|
657
|
-
var RGB_B_END = 6;
|
|
658
|
-
var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
|
|
659
|
-
var CHIP_GUTTER = 8;
|
|
660
|
-
var DEFAULT_CHIP_TESTID_PREFIX = "chip-selector-chip";
|
|
661
|
-
var ON_BRAND_SCALE = "onBrand";
|
|
662
|
-
var MAIN_STEP = "500";
|
|
663
|
-
function onBrandInk(palette) {
|
|
664
|
-
const scales = palette;
|
|
665
|
-
return scales[ON_BRAND_SCALE]?.[MAIN_STEP] ?? WHITE_COLOR;
|
|
666
|
-
}
|
|
667
|
-
function withAlpha2(hex, alpha) {
|
|
668
|
-
const normalized = hex.replace("#", "");
|
|
669
|
-
const isShort = normalized.length === HEX_SHORT_LENGTH2;
|
|
670
|
-
const full = isShort ? normalized.split("").map((c) => c + c).join("") : normalized;
|
|
671
|
-
const r = parseInt(full.slice(RGB_R_START, RGB_R_END), HEX_RADIX2);
|
|
672
|
-
const g = parseInt(full.slice(RGB_R_END, RGB_G_END), HEX_RADIX2);
|
|
673
|
-
const b = parseInt(full.slice(RGB_G_END, RGB_B_END), HEX_RADIX2);
|
|
674
|
-
const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
|
|
675
|
-
return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
|
|
676
|
-
}
|
|
677
|
-
var styles5 = reactNative.StyleSheet.create({
|
|
678
|
-
chipContainer: {
|
|
679
|
-
flexDirection: "row",
|
|
680
|
-
flexWrap: "wrap",
|
|
681
|
-
// `gap`, NOT per-chip margins plus a compensating negative container margin.
|
|
682
|
-
//
|
|
683
|
-
// The old form was `chipWrapper { marginRight: 8, marginBottom: 8 }` with
|
|
684
|
-
// `chipContainer { marginBottom: -8 }`. Its comment blamed `Field`'s 16px container margin —
|
|
685
|
-
// that was WRONG, and worth recording: F3 was briefed on the belief that the -8 compensated for
|
|
686
|
-
// `Field` and had to move in lockstep with it. It did not. -8 cancelled the CHIP's own gutter
|
|
687
|
-
// hanging off the last row, identically whether `Field` contributed 16, 0, or anything else.
|
|
688
|
-
//
|
|
689
|
-
// The pair is still the wrong tool: it holds only while the two constants agree, and it leaves
|
|
690
|
-
// a real negative margin any measuring parent inherits. `gap` produces the byte-identical box
|
|
691
|
-
// and is correct under BOTH of `Field`'s spacing models, not just the one it was tuned against.
|
|
692
|
-
gap: CHIP_GUTTER
|
|
693
|
-
},
|
|
966
|
+
var CHIP_ICON_GAP = 6;
|
|
967
|
+
var chipStyles = reactNative.StyleSheet.create({
|
|
694
968
|
chip: {
|
|
695
969
|
paddingHorizontal: 12,
|
|
696
970
|
paddingVertical: 6,
|
|
@@ -704,28 +978,21 @@ var styles5 = reactNative.StyleSheet.create({
|
|
|
704
978
|
borderRadius: OUTLINE_RADIUS,
|
|
705
979
|
borderWidth: 1
|
|
706
980
|
},
|
|
981
|
+
chipContent: { flexDirection: "row", alignItems: "center", gap: CHIP_ICON_GAP },
|
|
707
982
|
chipText: {},
|
|
708
983
|
chipTextOutline: { fontSize: OUTLINE_FONT_SIZE }
|
|
709
984
|
});
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
};
|
|
985
|
+
function ChipContent({ icon, label, textStyle }) {
|
|
986
|
+
const text = /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: textStyle, children: label });
|
|
987
|
+
if (icon === void 0 || icon === null)
|
|
988
|
+
return text;
|
|
989
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: chipStyles.chipContent, children: [
|
|
990
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { "aria-hidden": true, children: icon }),
|
|
991
|
+
text
|
|
992
|
+
] });
|
|
718
993
|
}
|
|
719
|
-
function OutlineChip({
|
|
720
|
-
option,
|
|
721
|
-
selected,
|
|
722
|
-
disabled,
|
|
723
|
-
colors,
|
|
724
|
-
testID,
|
|
725
|
-
accessibilityHint,
|
|
726
|
-
ariaPressed,
|
|
727
|
-
onPress
|
|
728
|
-
}) {
|
|
994
|
+
function OutlineChip(props) {
|
|
995
|
+
const { option, selected, disabled, colors, testID, accessibilityHint, onPress } = props;
|
|
729
996
|
const [hovered, setHovered] = React2__default.default.useState(false);
|
|
730
997
|
const active = selected || hovered;
|
|
731
998
|
const borderColor = active ? colors.primary : colors.border;
|
|
@@ -734,49 +1001,179 @@ function OutlineChip({
|
|
|
734
1001
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
735
1002
|
reactNative.Pressable,
|
|
736
1003
|
{
|
|
1004
|
+
ref: props.hostRef,
|
|
737
1005
|
accessibilityHint,
|
|
738
1006
|
accessibilityLabel: option.label,
|
|
739
|
-
accessibilityRole:
|
|
1007
|
+
accessibilityRole: props.accessibilityRole,
|
|
740
1008
|
accessibilityState: { selected, disabled },
|
|
741
|
-
"aria-
|
|
1009
|
+
"aria-checked": props.ariaChecked,
|
|
1010
|
+
"aria-pressed": props.ariaPressed,
|
|
742
1011
|
disabled,
|
|
743
1012
|
hitSlop: CHIP_HIT_SLOP,
|
|
1013
|
+
tabIndex: props.tabIndex,
|
|
744
1014
|
testID,
|
|
745
1015
|
onHoverIn: () => setHovered(true),
|
|
746
1016
|
onHoverOut: () => setHovered(false),
|
|
747
1017
|
onPress,
|
|
748
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [
|
|
1018
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [chipStyles.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1019
|
+
ChipContent,
|
|
1020
|
+
{
|
|
1021
|
+
icon: option.icon,
|
|
1022
|
+
label: option.label,
|
|
1023
|
+
textStyle: [chipStyles.chipText, chipStyles.chipTextOutline, { color: textColor }]
|
|
1024
|
+
}
|
|
1025
|
+
) })
|
|
749
1026
|
}
|
|
750
1027
|
);
|
|
751
1028
|
}
|
|
752
|
-
function SolidChip({
|
|
753
|
-
option,
|
|
754
|
-
selected,
|
|
755
|
-
disabled,
|
|
756
|
-
colors,
|
|
757
|
-
testID,
|
|
758
|
-
accessibilityHint,
|
|
759
|
-
ariaPressed,
|
|
760
|
-
onPress
|
|
761
|
-
}) {
|
|
1029
|
+
function SolidChip(props) {
|
|
1030
|
+
const { option, selected, disabled, colors, testID, accessibilityHint, onPress } = props;
|
|
762
1031
|
const backgroundColor = selected ? colors.primary : TRANSPARENT_COLOR;
|
|
763
1032
|
const textColor = selected ? colors.onPrimary : colors.textStrong;
|
|
1033
|
+
const rovingTabStop = { tabIndex: props.tabIndex };
|
|
764
1034
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
765
1035
|
reactNative.TouchableOpacity,
|
|
766
1036
|
{
|
|
1037
|
+
ref: props.hostRef,
|
|
767
1038
|
accessibilityHint,
|
|
768
1039
|
accessibilityLabel: option.label,
|
|
769
|
-
accessibilityRole:
|
|
1040
|
+
accessibilityRole: props.accessibilityRole,
|
|
770
1041
|
accessibilityState: { selected, disabled },
|
|
771
|
-
"aria-
|
|
1042
|
+
"aria-checked": props.ariaChecked,
|
|
1043
|
+
"aria-pressed": props.ariaPressed,
|
|
772
1044
|
disabled,
|
|
773
1045
|
hitSlop: CHIP_HIT_SLOP,
|
|
774
1046
|
testID,
|
|
775
1047
|
onPress,
|
|
776
|
-
|
|
1048
|
+
...rovingTabStop,
|
|
1049
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [chipStyles.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(ChipContent, { icon: option.icon, label: option.label, textStyle: [chipStyles.chipText, { color: textColor }] }) })
|
|
777
1050
|
}
|
|
778
1051
|
);
|
|
779
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
// src/ChipSelector/utils/radioKeyboard.ts
|
|
1055
|
+
var SPACE_KEYS = [" ", "Spacebar"];
|
|
1056
|
+
var NEXT_KEYS = ["ArrowRight", "ArrowDown"];
|
|
1057
|
+
var PREVIOUS_KEYS = ["ArrowLeft", "ArrowUp"];
|
|
1058
|
+
var ROVING_TAB_STOP = 0;
|
|
1059
|
+
var ROVING_SKIPPED = -1;
|
|
1060
|
+
function radioKeyTarget(key, index, count) {
|
|
1061
|
+
const isOutOfRange = index < 0 || index >= count;
|
|
1062
|
+
if (isOutOfRange)
|
|
1063
|
+
return null;
|
|
1064
|
+
if (SPACE_KEYS.includes(key))
|
|
1065
|
+
return index;
|
|
1066
|
+
const isArrow = NEXT_KEYS.includes(key) || PREVIOUS_KEYS.includes(key);
|
|
1067
|
+
if (!isArrow || count === 1)
|
|
1068
|
+
return null;
|
|
1069
|
+
const step = NEXT_KEYS.includes(key) ? 1 : -1;
|
|
1070
|
+
return (index + step + count) % count;
|
|
1071
|
+
}
|
|
1072
|
+
function radioTabIndex(index, selectedIndex, disabled) {
|
|
1073
|
+
if (disabled)
|
|
1074
|
+
return ROVING_SKIPPED;
|
|
1075
|
+
const tabStop = selectedIndex >= 0 ? selectedIndex : 0;
|
|
1076
|
+
return index === tabStop ? ROVING_TAB_STOP : ROVING_SKIPPED;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/ChipSelector/hooks/useRadioKeyboard.ts
|
|
1080
|
+
var NO_GROUP_PROPS = {};
|
|
1081
|
+
function useRadioKeyboard(config) {
|
|
1082
|
+
const { enabled, disabled, values, selectedIndex, onChange } = config;
|
|
1083
|
+
const nodes = React2__default.default.useRef([]);
|
|
1084
|
+
const chipRef = React2__default.default.useCallback((index) => (node) => {
|
|
1085
|
+
nodes.current[index] = node;
|
|
1086
|
+
}, []);
|
|
1087
|
+
const onKeyDown = (event) => {
|
|
1088
|
+
if (disabled)
|
|
1089
|
+
return;
|
|
1090
|
+
const index = nodes.current.indexOf(event.target);
|
|
1091
|
+
const target = radioKeyTarget(event.key, index, values.length);
|
|
1092
|
+
const nextValue = target === null ? void 0 : values[target];
|
|
1093
|
+
if (target === null || nextValue === void 0)
|
|
1094
|
+
return;
|
|
1095
|
+
event.preventDefault();
|
|
1096
|
+
if (target !== index)
|
|
1097
|
+
nodes.current[target]?.focus?.();
|
|
1098
|
+
onChange(nextValue);
|
|
1099
|
+
};
|
|
1100
|
+
return {
|
|
1101
|
+
chipRef,
|
|
1102
|
+
tabIndexFor: (index) => enabled ? radioTabIndex(index, selectedIndex, disabled) : void 0,
|
|
1103
|
+
// `onKeyDown` is a web-only View prop RN's types do not declare; RNW forwards it to the DOM.
|
|
1104
|
+
groupProps: enabled ? { onKeyDown } : NO_GROUP_PROPS
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/ChipSelector/utils/chipColors.ts
|
|
1109
|
+
var WHITE_COLOR = "#fff";
|
|
1110
|
+
var OUTLINE_TINT_ALPHA = 0.1;
|
|
1111
|
+
var HEX_SHORT_LENGTH2 = 3;
|
|
1112
|
+
var HEX_RADIX2 = 16;
|
|
1113
|
+
var RGB_R_START = 0;
|
|
1114
|
+
var RGB_R_END = 2;
|
|
1115
|
+
var RGB_G_END = 4;
|
|
1116
|
+
var RGB_B_END = 6;
|
|
1117
|
+
var ON_BRAND_SCALE = "onBrand";
|
|
1118
|
+
var MAIN_STEP = "500";
|
|
1119
|
+
function onBrandInk(palette) {
|
|
1120
|
+
const scales = palette;
|
|
1121
|
+
return scales[ON_BRAND_SCALE]?.[MAIN_STEP] ?? WHITE_COLOR;
|
|
1122
|
+
}
|
|
1123
|
+
function withAlpha2(hex, alpha) {
|
|
1124
|
+
const normalized = hex.replace("#", "");
|
|
1125
|
+
const isShort = normalized.length === HEX_SHORT_LENGTH2;
|
|
1126
|
+
const full = isShort ? normalized.split("").map((c) => c + c).join("") : normalized;
|
|
1127
|
+
const r = parseInt(full.slice(RGB_R_START, RGB_R_END), HEX_RADIX2);
|
|
1128
|
+
const g = parseInt(full.slice(RGB_R_END, RGB_G_END), HEX_RADIX2);
|
|
1129
|
+
const b = parseInt(full.slice(RGB_G_END, RGB_B_END), HEX_RADIX2);
|
|
1130
|
+
const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
|
|
1131
|
+
return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
|
|
1132
|
+
}
|
|
1133
|
+
function buildChipColors(colors, palette) {
|
|
1134
|
+
const primary = palette.primary[500];
|
|
1135
|
+
return {
|
|
1136
|
+
border: colors.border,
|
|
1137
|
+
textStrong: colors.text,
|
|
1138
|
+
textMuted: colors.textSecondary,
|
|
1139
|
+
primary,
|
|
1140
|
+
onPrimary: onBrandInk(palette),
|
|
1141
|
+
muted: colors.background,
|
|
1142
|
+
tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/ChipSelector/utils/chipIdentity.ts
|
|
1147
|
+
function chipIdentity(option, selected, config) {
|
|
1148
|
+
const isRadio = config.radio === true && !config.multiple;
|
|
1149
|
+
return {
|
|
1150
|
+
testID: `${config.prefix}-${String(option.value)}`,
|
|
1151
|
+
// The English default is retained verbatim: erevna/katalogos assert the exact string
|
|
1152
|
+
// `Selects Red`. Consumers that localize pass `optionAccessibilityHint`.
|
|
1153
|
+
accessibilityHint: config.hint ?? `Selects ${option.label}`,
|
|
1154
|
+
ariaPressed: config.multiple ? selected : void 0,
|
|
1155
|
+
accessibilityRole: isRadio ? "radio" : "button",
|
|
1156
|
+
ariaChecked: isRadio ? selected : void 0
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
var styles5 = reactNative.StyleSheet.create({
|
|
1160
|
+
chipContainer: {
|
|
1161
|
+
flexDirection: "row",
|
|
1162
|
+
flexWrap: "wrap",
|
|
1163
|
+
// `gap`, NOT per-chip margins plus a compensating negative container margin.
|
|
1164
|
+
//
|
|
1165
|
+
// The old form was `chipWrapper { marginRight: 8, marginBottom: 8 }` with
|
|
1166
|
+
// `chipContainer { marginBottom: -8 }`. Its comment blamed `Field`'s 16px container margin —
|
|
1167
|
+
// that was WRONG, and worth recording: F3 was briefed on the belief that the -8 compensated for
|
|
1168
|
+
// `Field` and had to move in lockstep with it. It did not. -8 cancelled the CHIP's own gutter
|
|
1169
|
+
// hanging off the last row, identically whether `Field` contributed 16, 0, or anything else.
|
|
1170
|
+
//
|
|
1171
|
+
// The pair is still the wrong tool: it holds only while the two constants agree, and it leaves
|
|
1172
|
+
// a real negative margin any measuring parent inherits. `gap` produces the byte-identical box
|
|
1173
|
+
// and is correct under BOTH of `Field`'s spacing models, not just the one it was tuned against.
|
|
1174
|
+
gap: CHIP_GUTTER
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
780
1177
|
var ChipSelector = ({
|
|
781
1178
|
label,
|
|
782
1179
|
options,
|
|
@@ -791,28 +1188,26 @@ var ChipSelector = ({
|
|
|
791
1188
|
error,
|
|
792
1189
|
testID,
|
|
793
1190
|
chipTestIDPrefix = DEFAULT_CHIP_TESTID_PREFIX,
|
|
794
|
-
optionAccessibilityHint
|
|
1191
|
+
optionAccessibilityHint,
|
|
1192
|
+
singleSelectRole = "button"
|
|
795
1193
|
}) => {
|
|
1194
|
+
const isRadioGroup = !multiple && singleSelectRole === "radio";
|
|
796
1195
|
const { theme } = uiFeedback.useUi();
|
|
797
1196
|
const { colors, palette } = theme;
|
|
798
|
-
const chipColors = React2__default.default.useMemo(() =>
|
|
799
|
-
const primary = palette.primary["500"];
|
|
800
|
-
return {
|
|
801
|
-
border: colors.border,
|
|
802
|
-
textStrong: colors.text,
|
|
803
|
-
textMuted: colors.textSecondary,
|
|
804
|
-
primary,
|
|
805
|
-
onPrimary: onBrandInk(palette),
|
|
806
|
-
muted: colors.background,
|
|
807
|
-
tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
|
|
808
|
-
};
|
|
809
|
-
}, [colors.border, colors.text, colors.textSecondary, colors.background, palette]);
|
|
1197
|
+
const chipColors = React2__default.default.useMemo(() => buildChipColors(colors, palette), [colors, palette]);
|
|
810
1198
|
function isSelected(optionValue) {
|
|
811
1199
|
if (multiple && Array.isArray(value))
|
|
812
1200
|
return value.includes(optionValue);
|
|
813
1201
|
return value === optionValue;
|
|
814
1202
|
}
|
|
815
|
-
|
|
1203
|
+
const radio = useRadioKeyboard({
|
|
1204
|
+
enabled: isRadioGroup,
|
|
1205
|
+
disabled,
|
|
1206
|
+
values: options.map((option) => option.value),
|
|
1207
|
+
selectedIndex: options.findIndex((option) => isSelected(option.value)),
|
|
1208
|
+
onChange
|
|
1209
|
+
});
|
|
1210
|
+
function renderChip(option, index) {
|
|
816
1211
|
const selected = isSelected(option.value);
|
|
817
1212
|
const chipProps = {
|
|
818
1213
|
colors: chipColors,
|
|
@@ -822,8 +1217,13 @@ var ChipSelector = ({
|
|
|
822
1217
|
...chipIdentity(option, selected, {
|
|
823
1218
|
prefix: chipTestIDPrefix,
|
|
824
1219
|
hint: optionAccessibilityHint,
|
|
825
|
-
multiple
|
|
1220
|
+
multiple,
|
|
1221
|
+
radio: isRadioGroup
|
|
826
1222
|
}),
|
|
1223
|
+
// Radio mode only: roving tab stop + host node for arrow-key focus moves. Both stay
|
|
1224
|
+
// `undefined` otherwise, so button / multi chips keep their existing tab order.
|
|
1225
|
+
tabIndex: radio.tabIndexFor(index),
|
|
1226
|
+
hostRef: isRadioGroup ? radio.chipRef(index) : void 0,
|
|
827
1227
|
onPress: () => onChange(option.value)
|
|
828
1228
|
};
|
|
829
1229
|
const key = String(option.value);
|
|
@@ -838,7 +1238,16 @@ var ChipSelector = ({
|
|
|
838
1238
|
required,
|
|
839
1239
|
spacing,
|
|
840
1240
|
testID,
|
|
841
|
-
children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1241
|
+
children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1242
|
+
reactNative.View,
|
|
1243
|
+
{
|
|
1244
|
+
accessibilityRole: isRadioGroup ? "radiogroup" : void 0,
|
|
1245
|
+
style: styles5.chipContainer,
|
|
1246
|
+
...webFieldA11y({ describedById, hasError }),
|
|
1247
|
+
...radio.groupProps,
|
|
1248
|
+
children: options.map(renderChip)
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
842
1251
|
}
|
|
843
1252
|
);
|
|
844
1253
|
};
|
|
@@ -896,8 +1305,7 @@ var FormSwitch = ({
|
|
|
896
1305
|
{
|
|
897
1306
|
accessibilityHint: accessibilityHint ?? label,
|
|
898
1307
|
accessibilityLabel: label,
|
|
899
|
-
|
|
900
|
-
accessibilityState: { checked: value },
|
|
1308
|
+
accessibilityState: { checked: value, disabled },
|
|
901
1309
|
disabled,
|
|
902
1310
|
testID,
|
|
903
1311
|
value,
|
|
@@ -1009,140 +1417,6 @@ var FormCheckbox = ({
|
|
|
1009
1417
|
}
|
|
1010
1418
|
);
|
|
1011
1419
|
};
|
|
1012
|
-
var IS_WEB5 = reactNative.Platform.OS === "web";
|
|
1013
|
-
var MENU_PORTAL_Z_INDEX = 1e3;
|
|
1014
|
-
var MENU_TOP_GAP2 = 4;
|
|
1015
|
-
function toDomNode(ref) {
|
|
1016
|
-
const node = ref.current;
|
|
1017
|
-
if (node === null || typeof node.getBoundingClientRect !== "function") return null;
|
|
1018
|
-
return node;
|
|
1019
|
-
}
|
|
1020
|
-
function readAnchorRect(ref) {
|
|
1021
|
-
if (!IS_WEB5) return null;
|
|
1022
|
-
const node = toDomNode(ref);
|
|
1023
|
-
if (node === null) return null;
|
|
1024
|
-
const r = node.getBoundingClientRect();
|
|
1025
|
-
return { top: r.top, left: r.left, width: r.width, bottom: r.bottom };
|
|
1026
|
-
}
|
|
1027
|
-
function buildPortalPopoverStyle(rect) {
|
|
1028
|
-
return {
|
|
1029
|
-
// RN's ViewStyle union omits 'fixed', but react-native-web honours it at runtime.
|
|
1030
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- web-only position value
|
|
1031
|
-
position: "fixed",
|
|
1032
|
-
top: rect.bottom + MENU_TOP_GAP2,
|
|
1033
|
-
left: rect.left,
|
|
1034
|
-
width: rect.width,
|
|
1035
|
-
zIndex: MENU_PORTAL_Z_INDEX
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
function useAnchorRect(ref, enabled) {
|
|
1039
|
-
const [rect, setRect] = React2.useState(null);
|
|
1040
|
-
React2.useLayoutEffect(() => {
|
|
1041
|
-
if (!IS_WEB5 || !enabled) {
|
|
1042
|
-
setRect(null);
|
|
1043
|
-
return void 0;
|
|
1044
|
-
}
|
|
1045
|
-
let frame = null;
|
|
1046
|
-
let disposed = false;
|
|
1047
|
-
const apply = () => {
|
|
1048
|
-
frame = null;
|
|
1049
|
-
if (disposed) return;
|
|
1050
|
-
setRect(readAnchorRect(ref));
|
|
1051
|
-
};
|
|
1052
|
-
const schedule = () => {
|
|
1053
|
-
if (frame !== null) return;
|
|
1054
|
-
frame = window.requestAnimationFrame(apply);
|
|
1055
|
-
};
|
|
1056
|
-
apply();
|
|
1057
|
-
document.addEventListener("scroll", schedule, true);
|
|
1058
|
-
window.addEventListener("resize", schedule);
|
|
1059
|
-
const node = toDomNode(ref);
|
|
1060
|
-
const observer = typeof ResizeObserver === "function" ? new ResizeObserver(schedule) : null;
|
|
1061
|
-
if (observer !== null) {
|
|
1062
|
-
if (node !== null) observer.observe(node);
|
|
1063
|
-
observer.observe(document.body);
|
|
1064
|
-
}
|
|
1065
|
-
return () => {
|
|
1066
|
-
disposed = true;
|
|
1067
|
-
if (frame !== null) window.cancelAnimationFrame(frame);
|
|
1068
|
-
document.removeEventListener("scroll", schedule, true);
|
|
1069
|
-
window.removeEventListener("resize", schedule);
|
|
1070
|
-
if (observer !== null) observer.disconnect();
|
|
1071
|
-
};
|
|
1072
|
-
}, [ref, enabled]);
|
|
1073
|
-
return IS_WEB5 && enabled ? rect : null;
|
|
1074
|
-
}
|
|
1075
|
-
var IS_WEB6 = typeof document !== "undefined";
|
|
1076
|
-
var ESCAPE_KEY = "Escape";
|
|
1077
|
-
var ACTIVE_WEIGHT = "700";
|
|
1078
|
-
var IDLE_WEIGHT = "400";
|
|
1079
|
-
function useDismiss(anchorRef, menuRef, onDismiss) {
|
|
1080
|
-
React2.useEffect(() => {
|
|
1081
|
-
if (!IS_WEB6) return void 0;
|
|
1082
|
-
const onMouseDown = (event) => {
|
|
1083
|
-
const anchorNode = anchorRef.current;
|
|
1084
|
-
const menuNode = menuRef.current;
|
|
1085
|
-
const target = event.target instanceof Node ? event.target : null;
|
|
1086
|
-
const insideAnchor = anchorNode !== null && target !== null && anchorNode.contains(target);
|
|
1087
|
-
const insideMenu = menuNode !== null && target !== null && menuNode.contains(target);
|
|
1088
|
-
if (!insideAnchor && !insideMenu) onDismiss();
|
|
1089
|
-
};
|
|
1090
|
-
const onKeyDown = (event) => {
|
|
1091
|
-
if (event.key === ESCAPE_KEY) onDismiss();
|
|
1092
|
-
};
|
|
1093
|
-
document.addEventListener("mousedown", onMouseDown);
|
|
1094
|
-
document.addEventListener("keydown", onKeyDown);
|
|
1095
|
-
return () => {
|
|
1096
|
-
document.removeEventListener("mousedown", onMouseDown);
|
|
1097
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
1098
|
-
};
|
|
1099
|
-
}, [anchorRef, menuRef, onDismiss]);
|
|
1100
|
-
}
|
|
1101
|
-
function AnchoredMenu({
|
|
1102
|
-
options,
|
|
1103
|
-
selectedValue,
|
|
1104
|
-
onSelect,
|
|
1105
|
-
onDismiss,
|
|
1106
|
-
colors,
|
|
1107
|
-
optionHint,
|
|
1108
|
-
testID,
|
|
1109
|
-
anchorRef,
|
|
1110
|
-
usePortal = true
|
|
1111
|
-
}) {
|
|
1112
|
-
const menuRef = React2__default.default.useRef(null);
|
|
1113
|
-
useDismiss(anchorRef, menuRef, onDismiss);
|
|
1114
|
-
const rect = useAnchorRect(anchorRef, usePortal);
|
|
1115
|
-
const portalStyle = rect !== null ? buildPortalPopoverStyle(rect) : null;
|
|
1116
|
-
const menu = /* @__PURE__ */ jsxRuntime.jsx(
|
|
1117
|
-
reactNative.View,
|
|
1118
|
-
{
|
|
1119
|
-
ref: menuRef,
|
|
1120
|
-
accessibilityRole: "menu",
|
|
1121
|
-
style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }, portalStyle],
|
|
1122
|
-
testID: `${testID}-menu`,
|
|
1123
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { keyboardShouldPersistTaps: "handled", nestedScrollEnabled: true, children: options.map((opt) => {
|
|
1124
|
-
const active = opt.value === selectedValue;
|
|
1125
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1126
|
-
reactNative.Pressable,
|
|
1127
|
-
{
|
|
1128
|
-
accessibilityRole: "menuitem",
|
|
1129
|
-
accessibilityLabel: opt.label,
|
|
1130
|
-
accessibilityHint: optionHint,
|
|
1131
|
-
accessibilityState: { selected: active },
|
|
1132
|
-
"aria-selected": active,
|
|
1133
|
-
onPress: () => onSelect(opt.value),
|
|
1134
|
-
style: controlStyles.option,
|
|
1135
|
-
testID: `${testID}-option-${opt.value}`,
|
|
1136
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [controlStyles.optionText, { color: active ? colors.brand : colors.text, fontWeight: active ? ACTIVE_WEIGHT : IDLE_WEIGHT }], children: opt.label })
|
|
1137
|
-
},
|
|
1138
|
-
opt.value
|
|
1139
|
-
);
|
|
1140
|
-
}) })
|
|
1141
|
-
}
|
|
1142
|
-
);
|
|
1143
|
-
if (usePortal && IS_WEB6) return reactDom.createPortal(menu, document.body);
|
|
1144
|
-
return menu;
|
|
1145
|
-
}
|
|
1146
1420
|
var CHEVRON_DOWN = "\u25BE";
|
|
1147
1421
|
var CHEVRON_UP = "\u25B4";
|
|
1148
1422
|
function SelectControl({
|
|
@@ -1244,7 +1518,7 @@ function isUnmatched(options, text) {
|
|
|
1244
1518
|
if (needle === "") return false;
|
|
1245
1519
|
return !options.some((o) => o.value.toLowerCase() === needle || o.label.toLowerCase() === needle);
|
|
1246
1520
|
}
|
|
1247
|
-
var
|
|
1521
|
+
var CLOSE_DELAY_MS2 = 140;
|
|
1248
1522
|
var DEFAULT_TYPEAHEAD_MIN_CHARS = 1;
|
|
1249
1523
|
var DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS = 8;
|
|
1250
1524
|
function TypeaheadControl({
|
|
@@ -1288,7 +1562,7 @@ function TypeaheadControl({
|
|
|
1288
1562
|
[options, onChange]
|
|
1289
1563
|
);
|
|
1290
1564
|
const handleBlur = React2.useCallback(() => {
|
|
1291
|
-
setTimeout(() => setIsOpen(false),
|
|
1565
|
+
setTimeout(() => setIsOpen(false), CLOSE_DELAY_MS2);
|
|
1292
1566
|
}, []);
|
|
1293
1567
|
const handleSubmit = React2.useCallback(() => {
|
|
1294
1568
|
setIsOpen(false);
|
|
@@ -1407,6 +1681,7 @@ function DateRangeControl({
|
|
|
1407
1681
|
|
|
1408
1682
|
exports.AnchoredMenu = AnchoredMenu;
|
|
1409
1683
|
exports.ChipSelector = ChipSelector;
|
|
1684
|
+
exports.DEFAULT_MAX_RECENTS = DEFAULT_MAX_RECENTS;
|
|
1410
1685
|
exports.DEFAULT_OPTIONAL_LABEL = DEFAULT_OPTIONAL_LABEL;
|
|
1411
1686
|
exports.DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS = DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS;
|
|
1412
1687
|
exports.DEFAULT_TYPEAHEAD_MIN_CHARS = DEFAULT_TYPEAHEAD_MIN_CHARS;
|
|
@@ -1425,6 +1700,7 @@ exports.TypeaheadControl = TypeaheadControl;
|
|
|
1425
1700
|
exports.controlStyles = controlStyles;
|
|
1426
1701
|
exports.isUnmatched = isUnmatched;
|
|
1427
1702
|
exports.suggestOptions = suggestOptions;
|
|
1703
|
+
exports.useRecentValues = useRecentValues;
|
|
1428
1704
|
exports.useThemedInput = useThemedInput;
|
|
1429
1705
|
//# sourceMappingURL=index.js.map
|
|
1430
1706
|
//# sourceMappingURL=index.js.map
|