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