@dloizides/ui-forms 1.12.0 → 1.13.2
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/dist/index.d.mts +20 -9
- package/dist/index.d.ts +20 -9
- package/dist/index.js +78 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -733,17 +733,20 @@ declare function DateRangeControl({ value, onChange, onSubmit, fromLabel, toLabe
|
|
|
733
733
|
/**
|
|
734
734
|
* `AnchoredMenu` — the floating option list shared by `SelectControl` and `TypeaheadControl`.
|
|
735
735
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
736
|
+
* By default it renders an in-tree, absolutely-positioned popover under its anchor
|
|
737
|
+
* (`position:absolute; top:100%; zIndex; elevation`). Set {@link AnchoredMenuProps.usePortal} to
|
|
738
|
+
* render it — on web only — in a PORTAL to `document.body` with `position: fixed` at the trigger's
|
|
739
|
+
* measured rect instead: that escapes react-native-web's trapped `z-index: 0` stacking contexts and
|
|
740
|
+
* any ancestor `overflow: hidden`, so the menu is never painted under / clipped by later siblings
|
|
741
|
+
* (the fix for the Country typeahead menu that hid behind the content below it). See {@link menuPortal}.
|
|
742
|
+
* On web it dismisses on outside-click / Escape; on native the caller closes it on blur/select.
|
|
743
|
+
* Purely presentational + theme-flat, so it stays reusable.
|
|
740
744
|
*
|
|
741
745
|
* Promoted verbatim out of `@dloizides/ui-tables`' private `Filters/components/AnchoredMenu`.
|
|
742
746
|
*
|
|
743
|
-
* Deliberately
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
* every consumer. Apps needing the responsive modal/bottom-sheet variant inject it instead.
|
|
747
|
+
* Deliberately no `ui-layout` dependency: `ui-forms` is a low-level primitive and must not take a
|
|
748
|
+
* runtime dependency on the higher-level `ui-layout` `ModalDropdown` — that would invert the graph
|
|
749
|
+
* for every consumer. The portal mechanism therefore lives locally in {@link menuPortal}.
|
|
747
750
|
*/
|
|
748
751
|
|
|
749
752
|
/** The four theme colours the menu paints with — passed in, so the menu stays theme-flat. */
|
|
@@ -766,8 +769,16 @@ interface AnchoredMenuProps {
|
|
|
766
769
|
testID: string;
|
|
767
770
|
/** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
|
|
768
771
|
anchorRef: React.RefObject<View | null>;
|
|
772
|
+
/**
|
|
773
|
+
* On web, render the popover in a portal to `document.body` with `position: fixed` at the anchor's
|
|
774
|
+
* measured rect, so it escapes react-native-web's trapped stacking contexts and any ancestor
|
|
775
|
+
* `overflow: hidden` (the fix for a menu that paints under / is clipped by the content below it).
|
|
776
|
+
* No effect on native. Default `false` — the existing in-tree behaviour, so `SelectControl`
|
|
777
|
+
* usages are unchanged; `TypeaheadControl` opts in.
|
|
778
|
+
*/
|
|
779
|
+
usePortal?: boolean;
|
|
769
780
|
}
|
|
770
|
-
declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, }: AnchoredMenuProps): React.ReactElement;
|
|
781
|
+
declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, usePortal, }: AnchoredMenuProps): React.ReactElement;
|
|
771
782
|
|
|
772
783
|
declare const controlStyles: {
|
|
773
784
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -733,17 +733,20 @@ declare function DateRangeControl({ value, onChange, onSubmit, fromLabel, toLabe
|
|
|
733
733
|
/**
|
|
734
734
|
* `AnchoredMenu` — the floating option list shared by `SelectControl` and `TypeaheadControl`.
|
|
735
735
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
736
|
+
* By default it renders an in-tree, absolutely-positioned popover under its anchor
|
|
737
|
+
* (`position:absolute; top:100%; zIndex; elevation`). Set {@link AnchoredMenuProps.usePortal} to
|
|
738
|
+
* render it — on web only — in a PORTAL to `document.body` with `position: fixed` at the trigger's
|
|
739
|
+
* measured rect instead: that escapes react-native-web's trapped `z-index: 0` stacking contexts and
|
|
740
|
+
* any ancestor `overflow: hidden`, so the menu is never painted under / clipped by later siblings
|
|
741
|
+
* (the fix for the Country typeahead menu that hid behind the content below it). See {@link menuPortal}.
|
|
742
|
+
* On web it dismisses on outside-click / Escape; on native the caller closes it on blur/select.
|
|
743
|
+
* Purely presentational + theme-flat, so it stays reusable.
|
|
740
744
|
*
|
|
741
745
|
* Promoted verbatim out of `@dloizides/ui-tables`' private `Filters/components/AnchoredMenu`.
|
|
742
746
|
*
|
|
743
|
-
* Deliberately
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
* every consumer. Apps needing the responsive modal/bottom-sheet variant inject it instead.
|
|
747
|
+
* Deliberately no `ui-layout` dependency: `ui-forms` is a low-level primitive and must not take a
|
|
748
|
+
* runtime dependency on the higher-level `ui-layout` `ModalDropdown` — that would invert the graph
|
|
749
|
+
* for every consumer. The portal mechanism therefore lives locally in {@link menuPortal}.
|
|
747
750
|
*/
|
|
748
751
|
|
|
749
752
|
/** The four theme colours the menu paints with — passed in, so the menu stays theme-flat. */
|
|
@@ -766,8 +769,16 @@ interface AnchoredMenuProps {
|
|
|
766
769
|
testID: string;
|
|
767
770
|
/** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
|
|
768
771
|
anchorRef: React.RefObject<View | null>;
|
|
772
|
+
/**
|
|
773
|
+
* On web, render the popover in a portal to `document.body` with `position: fixed` at the anchor's
|
|
774
|
+
* measured rect, so it escapes react-native-web's trapped stacking contexts and any ancestor
|
|
775
|
+
* `overflow: hidden` (the fix for a menu that paints under / is clipped by the content below it).
|
|
776
|
+
* No effect on native. Default `false` — the existing in-tree behaviour, so `SelectControl`
|
|
777
|
+
* usages are unchanged; `TypeaheadControl` opts in.
|
|
778
|
+
*/
|
|
779
|
+
usePortal?: boolean;
|
|
769
780
|
}
|
|
770
|
-
declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, }: AnchoredMenuProps): React.ReactElement;
|
|
781
|
+
declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, usePortal, }: AnchoredMenuProps): React.ReactElement;
|
|
771
782
|
|
|
772
783
|
declare const controlStyles: {
|
|
773
784
|
/**
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ var uiFeedback = require('@dloizides/ui-feedback');
|
|
|
6
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
7
|
var uiButtons = require('@dloizides/ui-buttons');
|
|
8
8
|
var a11y = require('@dloizides/a11y');
|
|
9
|
+
var reactDom = require('react-dom');
|
|
9
10
|
|
|
10
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
|
|
@@ -1007,13 +1008,76 @@ var FormCheckbox = ({
|
|
|
1007
1008
|
}
|
|
1008
1009
|
);
|
|
1009
1010
|
};
|
|
1010
|
-
var IS_WEB5 =
|
|
1011
|
+
var IS_WEB5 = reactNative.Platform.OS === "web";
|
|
1012
|
+
var MENU_PORTAL_Z_INDEX = 1e3;
|
|
1013
|
+
var MENU_TOP_GAP2 = 4;
|
|
1014
|
+
function toDomNode(ref) {
|
|
1015
|
+
const node = ref.current;
|
|
1016
|
+
if (node === null || typeof node.getBoundingClientRect !== "function") return null;
|
|
1017
|
+
return node;
|
|
1018
|
+
}
|
|
1019
|
+
function readAnchorRect(ref) {
|
|
1020
|
+
if (!IS_WEB5) return null;
|
|
1021
|
+
const node = toDomNode(ref);
|
|
1022
|
+
if (node === null) return null;
|
|
1023
|
+
const r = node.getBoundingClientRect();
|
|
1024
|
+
return { top: r.top, left: r.left, width: r.width, bottom: r.bottom };
|
|
1025
|
+
}
|
|
1026
|
+
function buildPortalPopoverStyle(rect) {
|
|
1027
|
+
return {
|
|
1028
|
+
// RN's ViewStyle union omits 'fixed', but react-native-web honours it at runtime.
|
|
1029
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- web-only position value
|
|
1030
|
+
position: "fixed",
|
|
1031
|
+
top: rect.bottom + MENU_TOP_GAP2,
|
|
1032
|
+
left: rect.left,
|
|
1033
|
+
width: rect.width,
|
|
1034
|
+
zIndex: MENU_PORTAL_Z_INDEX
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function useAnchorRect(ref, enabled) {
|
|
1038
|
+
const [rect, setRect] = React2.useState(null);
|
|
1039
|
+
React2.useLayoutEffect(() => {
|
|
1040
|
+
if (!IS_WEB5 || !enabled) {
|
|
1041
|
+
setRect(null);
|
|
1042
|
+
return void 0;
|
|
1043
|
+
}
|
|
1044
|
+
let frame = null;
|
|
1045
|
+
let disposed = false;
|
|
1046
|
+
const apply = () => {
|
|
1047
|
+
frame = null;
|
|
1048
|
+
if (disposed) return;
|
|
1049
|
+
setRect(readAnchorRect(ref));
|
|
1050
|
+
};
|
|
1051
|
+
const schedule = () => {
|
|
1052
|
+
if (frame !== null) return;
|
|
1053
|
+
frame = window.requestAnimationFrame(apply);
|
|
1054
|
+
};
|
|
1055
|
+
apply();
|
|
1056
|
+
document.addEventListener("scroll", schedule, true);
|
|
1057
|
+
window.addEventListener("resize", schedule);
|
|
1058
|
+
const node = toDomNode(ref);
|
|
1059
|
+
const observer = typeof ResizeObserver === "function" ? new ResizeObserver(schedule) : null;
|
|
1060
|
+
if (observer !== null) {
|
|
1061
|
+
if (node !== null) observer.observe(node);
|
|
1062
|
+
observer.observe(document.body);
|
|
1063
|
+
}
|
|
1064
|
+
return () => {
|
|
1065
|
+
disposed = true;
|
|
1066
|
+
if (frame !== null) window.cancelAnimationFrame(frame);
|
|
1067
|
+
document.removeEventListener("scroll", schedule, true);
|
|
1068
|
+
window.removeEventListener("resize", schedule);
|
|
1069
|
+
if (observer !== null) observer.disconnect();
|
|
1070
|
+
};
|
|
1071
|
+
}, [ref, enabled]);
|
|
1072
|
+
return IS_WEB5 && enabled ? rect : null;
|
|
1073
|
+
}
|
|
1074
|
+
var IS_WEB6 = typeof document !== "undefined";
|
|
1011
1075
|
var ESCAPE_KEY = "Escape";
|
|
1012
1076
|
var ACTIVE_WEIGHT = "700";
|
|
1013
1077
|
var IDLE_WEIGHT = "400";
|
|
1014
1078
|
function useDismiss(anchorRef, menuRef, onDismiss) {
|
|
1015
1079
|
React2.useEffect(() => {
|
|
1016
|
-
if (!
|
|
1080
|
+
if (!IS_WEB6) return void 0;
|
|
1017
1081
|
const onMouseDown = (event) => {
|
|
1018
1082
|
const anchorNode = anchorRef.current;
|
|
1019
1083
|
const menuNode = menuRef.current;
|
|
@@ -1041,18 +1105,21 @@ function AnchoredMenu({
|
|
|
1041
1105
|
colors,
|
|
1042
1106
|
optionHint,
|
|
1043
1107
|
testID,
|
|
1044
|
-
anchorRef
|
|
1108
|
+
anchorRef,
|
|
1109
|
+
usePortal = false
|
|
1045
1110
|
}) {
|
|
1046
1111
|
const menuRef = React2__default.default.useRef(null);
|
|
1047
1112
|
useDismiss(anchorRef, menuRef, onDismiss);
|
|
1048
|
-
|
|
1113
|
+
const rect = useAnchorRect(anchorRef, usePortal);
|
|
1114
|
+
const portalStyle = rect !== null ? buildPortalPopoverStyle(rect) : null;
|
|
1115
|
+
const menu = /* @__PURE__ */ jsxRuntime.jsx(
|
|
1049
1116
|
reactNative.View,
|
|
1050
1117
|
{
|
|
1051
1118
|
ref: menuRef,
|
|
1052
1119
|
accessibilityRole: "menu",
|
|
1053
|
-
style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }],
|
|
1120
|
+
style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }, portalStyle],
|
|
1054
1121
|
testID: `${testID}-menu`,
|
|
1055
|
-
children: options.map((opt) => {
|
|
1122
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { keyboardShouldPersistTaps: "handled", nestedScrollEnabled: true, children: options.map((opt) => {
|
|
1056
1123
|
const active = opt.value === selectedValue;
|
|
1057
1124
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1058
1125
|
reactNative.Pressable,
|
|
@@ -1069,9 +1136,11 @@ function AnchoredMenu({
|
|
|
1069
1136
|
},
|
|
1070
1137
|
opt.value
|
|
1071
1138
|
);
|
|
1072
|
-
})
|
|
1139
|
+
}) })
|
|
1073
1140
|
}
|
|
1074
1141
|
);
|
|
1142
|
+
if (usePortal && IS_WEB6) return reactDom.createPortal(menu, document.body);
|
|
1143
|
+
return menu;
|
|
1075
1144
|
}
|
|
1076
1145
|
var CHEVRON_DOWN = "\u25BE";
|
|
1077
1146
|
var CHEVRON_UP = "\u25B4";
|
|
@@ -1255,7 +1324,8 @@ function TypeaheadControl({
|
|
|
1255
1324
|
colors: { text: colors.text, border: colors.border, surface: colors.surface, brand: palette.primary["500"] },
|
|
1256
1325
|
optionHint,
|
|
1257
1326
|
testID,
|
|
1258
|
-
anchorRef
|
|
1327
|
+
anchorRef,
|
|
1328
|
+
usePortal: true
|
|
1259
1329
|
}
|
|
1260
1330
|
) : null,
|
|
1261
1331
|
hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { role: "alert", style: [controlStyles.error, { color: semantic.error["500"] }], testID: `${testID}-error`, children: error }) : null
|