@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.mjs CHANGED
@@ -1,9 +1,10 @@
1
- import React2, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
2
- import { Platform, StyleSheet, TextInput, View, Text, Switch, Pressable, TouchableOpacity } from 'react-native';
1
+ import React2, { useState, useCallback, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
2
+ import { Platform, StyleSheet, TextInput, View, Text, Switch, Pressable, ScrollView, 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
6
  import { MIN_TOUCH_TARGET_PX } from '@dloizides/a11y';
7
+ import { createPortal } from 'react-dom';
7
8
 
8
9
  // src/Field/Field.tsx
9
10
  function attachControlId(child, candidateId, describedById) {
@@ -1001,13 +1002,76 @@ var FormCheckbox = ({
1001
1002
  }
1002
1003
  );
1003
1004
  };
1004
- var IS_WEB5 = typeof document !== "undefined";
1005
+ var IS_WEB5 = Platform.OS === "web";
1006
+ var MENU_PORTAL_Z_INDEX = 1e3;
1007
+ var MENU_TOP_GAP2 = 4;
1008
+ function toDomNode(ref) {
1009
+ const node = ref.current;
1010
+ if (node === null || typeof node.getBoundingClientRect !== "function") return null;
1011
+ return node;
1012
+ }
1013
+ function readAnchorRect(ref) {
1014
+ if (!IS_WEB5) return null;
1015
+ const node = toDomNode(ref);
1016
+ if (node === null) return null;
1017
+ const r = node.getBoundingClientRect();
1018
+ return { top: r.top, left: r.left, width: r.width, bottom: r.bottom };
1019
+ }
1020
+ function buildPortalPopoverStyle(rect) {
1021
+ return {
1022
+ // RN's ViewStyle union omits 'fixed', but react-native-web honours it at runtime.
1023
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- web-only position value
1024
+ position: "fixed",
1025
+ top: rect.bottom + MENU_TOP_GAP2,
1026
+ left: rect.left,
1027
+ width: rect.width,
1028
+ zIndex: MENU_PORTAL_Z_INDEX
1029
+ };
1030
+ }
1031
+ function useAnchorRect(ref, enabled) {
1032
+ const [rect, setRect] = useState(null);
1033
+ useLayoutEffect(() => {
1034
+ if (!IS_WEB5 || !enabled) {
1035
+ setRect(null);
1036
+ return void 0;
1037
+ }
1038
+ let frame = null;
1039
+ let disposed = false;
1040
+ const apply = () => {
1041
+ frame = null;
1042
+ if (disposed) return;
1043
+ setRect(readAnchorRect(ref));
1044
+ };
1045
+ const schedule = () => {
1046
+ if (frame !== null) return;
1047
+ frame = window.requestAnimationFrame(apply);
1048
+ };
1049
+ apply();
1050
+ document.addEventListener("scroll", schedule, true);
1051
+ window.addEventListener("resize", schedule);
1052
+ const node = toDomNode(ref);
1053
+ const observer = typeof ResizeObserver === "function" ? new ResizeObserver(schedule) : null;
1054
+ if (observer !== null) {
1055
+ if (node !== null) observer.observe(node);
1056
+ observer.observe(document.body);
1057
+ }
1058
+ return () => {
1059
+ disposed = true;
1060
+ if (frame !== null) window.cancelAnimationFrame(frame);
1061
+ document.removeEventListener("scroll", schedule, true);
1062
+ window.removeEventListener("resize", schedule);
1063
+ if (observer !== null) observer.disconnect();
1064
+ };
1065
+ }, [ref, enabled]);
1066
+ return IS_WEB5 && enabled ? rect : null;
1067
+ }
1068
+ var IS_WEB6 = typeof document !== "undefined";
1005
1069
  var ESCAPE_KEY = "Escape";
1006
1070
  var ACTIVE_WEIGHT = "700";
1007
1071
  var IDLE_WEIGHT = "400";
1008
1072
  function useDismiss(anchorRef, menuRef, onDismiss) {
1009
1073
  useEffect(() => {
1010
- if (!IS_WEB5) return void 0;
1074
+ if (!IS_WEB6) return void 0;
1011
1075
  const onMouseDown = (event) => {
1012
1076
  const anchorNode = anchorRef.current;
1013
1077
  const menuNode = menuRef.current;
@@ -1035,18 +1099,21 @@ function AnchoredMenu({
1035
1099
  colors,
1036
1100
  optionHint,
1037
1101
  testID,
1038
- anchorRef
1102
+ anchorRef,
1103
+ usePortal = false
1039
1104
  }) {
1040
1105
  const menuRef = React2.useRef(null);
1041
1106
  useDismiss(anchorRef, menuRef, onDismiss);
1042
- return /* @__PURE__ */ jsx(
1107
+ const rect = useAnchorRect(anchorRef, usePortal);
1108
+ const portalStyle = rect !== null ? buildPortalPopoverStyle(rect) : null;
1109
+ const menu = /* @__PURE__ */ jsx(
1043
1110
  View,
1044
1111
  {
1045
1112
  ref: menuRef,
1046
1113
  accessibilityRole: "menu",
1047
- style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }],
1114
+ style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }, portalStyle],
1048
1115
  testID: `${testID}-menu`,
1049
- children: options.map((opt) => {
1116
+ children: /* @__PURE__ */ jsx(ScrollView, { keyboardShouldPersistTaps: "handled", nestedScrollEnabled: true, children: options.map((opt) => {
1050
1117
  const active = opt.value === selectedValue;
1051
1118
  return /* @__PURE__ */ jsx(
1052
1119
  Pressable,
@@ -1063,9 +1130,11 @@ function AnchoredMenu({
1063
1130
  },
1064
1131
  opt.value
1065
1132
  );
1066
- })
1133
+ }) })
1067
1134
  }
1068
1135
  );
1136
+ if (usePortal && IS_WEB6) return createPortal(menu, document.body);
1137
+ return menu;
1069
1138
  }
1070
1139
  var CHEVRON_DOWN = "\u25BE";
1071
1140
  var CHEVRON_UP = "\u25B4";
@@ -1249,7 +1318,8 @@ function TypeaheadControl({
1249
1318
  colors: { text: colors.text, border: colors.border, surface: colors.surface, brand: palette.primary["500"] },
1250
1319
  optionHint,
1251
1320
  testID,
1252
- anchorRef
1321
+ anchorRef,
1322
+ usePortal: true
1253
1323
  }
1254
1324
  ) : null,
1255
1325
  hasError ? /* @__PURE__ */ jsx(Text, { role: "alert", style: [controlStyles.error, { color: semantic.error["500"] }], testID: `${testID}-error`, children: error }) : null