@dloizides/ui-forms 1.9.0 → 1.11.1

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.js CHANGED
@@ -1,25 +1,63 @@
1
1
  'use strict';
2
2
 
3
- var React = require('react');
3
+ var React2 = require('react');
4
4
  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');
8
9
 
9
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
11
 
11
- var React__default = /*#__PURE__*/_interopDefault(React);
12
+ var React2__default = /*#__PURE__*/_interopDefault(React2);
12
13
 
13
14
  // src/Field/Field.tsx
15
+ function attachControlId(child, candidateId, describedById) {
16
+ if (!React2__default.default.isValidElement(child) || child.type === React2__default.default.Fragment) {
17
+ return { node: child };
18
+ }
19
+ const props = child.props;
20
+ const existing = props.id ?? props.nativeID;
21
+ const element = child;
22
+ const controlId = existing !== void 0 && existing !== "" ? existing : candidateId;
23
+ const injected = { id: controlId };
24
+ if (describedById !== void 0 && props.describedById === void 0) {
25
+ injected.describedById = describedById;
26
+ }
27
+ return { node: React2__default.default.cloneElement(element, injected), controlId };
28
+ }
29
+ var IS_WEB = reactNative.Platform.OS === "web";
30
+ function isAttributeHost(node) {
31
+ return typeof node === "object" && node !== null && typeof node.setAttribute === "function";
32
+ }
33
+ function useHtmlFor(controlId) {
34
+ const ref = React2.useRef(null);
35
+ React2.useEffect(() => {
36
+ const node = ref.current;
37
+ if (!IS_WEB || !isAttributeHost(node)) return;
38
+ const target = controlId !== void 0 && controlId !== "" ? node.ownerDocument?.getElementById(controlId) : null;
39
+ if (target === null || target === void 0) {
40
+ node.removeAttribute("for");
41
+ return;
42
+ }
43
+ node.setAttribute("for", controlId);
44
+ }, [controlId]);
45
+ return ref;
46
+ }
47
+ var IS_WEB2 = reactNative.Platform.OS === "web";
14
48
  function FieldLabel({
15
49
  label,
16
50
  marker,
17
51
  optionalLabel,
52
+ controlId,
53
+ labelId,
18
54
  style,
19
55
  requiredMarkStyle,
20
56
  optionalMarkStyle
21
57
  }) {
22
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style, children: [
58
+ const webProps = IS_WEB2 ? { role: "label" } : {};
59
+ const labelRef = useHtmlFor(controlId);
60
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { id: labelId, ref: labelRef, style, ...webProps, children: [
23
61
  label,
24
62
  " ",
25
63
  marker === "required" ? (
@@ -96,15 +134,15 @@ var labelVariantStyles = reactNative.StyleSheet.create({
96
134
  function resolveLabelVariantStyle(variant) {
97
135
  return variant === "control" ? labelVariantStyles.control : labelVariantStyles.field;
98
136
  }
99
- var FormGridContext = React__default.default.createContext(null);
100
- var GapOwnedContext = React__default.default.createContext(false);
137
+ var FormGridContext = React2__default.default.createContext(null);
138
+ var GapOwnedContext = React2__default.default.createContext(false);
101
139
  var FormGridProvider = FormGridContext.Provider;
102
140
  var GapOwnedProvider = GapOwnedContext.Provider;
103
141
  function useFormGridMetrics() {
104
- return React__default.default.useContext(FormGridContext);
142
+ return React2__default.default.useContext(FormGridContext);
105
143
  }
106
144
  function useIsGapOwned() {
107
- return React__default.default.useContext(GapOwnedContext);
145
+ return React2__default.default.useContext(GapOwnedContext);
108
146
  }
109
147
  var ERROR_FONT_SIZE = 12;
110
148
  var HINT_FONT_SIZE = 12;
@@ -122,10 +160,15 @@ var styles = reactNative.StyleSheet.create({
122
160
  });
123
161
  var fieldSeq = 0;
124
162
  function useFieldIds() {
125
- const [ids] = React__default.default.useState(() => {
163
+ const [ids] = React2__default.default.useState(() => {
126
164
  fieldSeq += 1;
127
165
  const seq = String(fieldSeq);
128
- return { hintId: `field-hint-${seq}`, errorId: `field-error-${seq}` };
166
+ return {
167
+ hintId: `field-hint-${seq}`,
168
+ errorId: `field-error-${seq}`,
169
+ controlId: `field-control-${seq}`,
170
+ labelId: `field-label-${seq}`
171
+ };
129
172
  });
130
173
  return ids;
131
174
  }
@@ -163,8 +206,8 @@ var Field = ({
163
206
  const labelText = label ?? "";
164
207
  const hasLabel = labelText !== "";
165
208
  const marker = resolveFieldMarker(required, optional);
166
- const { hintId, errorId } = useFieldIds();
167
- const themeStyles = React__default.default.useMemo(
209
+ const { hintId, errorId, controlId, labelId } = useFieldIds();
210
+ const themeStyles = React2__default.default.useMemo(
168
211
  () => ({
169
212
  label: { color: colors.textSecondary },
170
213
  requiredMark: { color: errorColor },
@@ -176,12 +219,16 @@ var Field = ({
176
219
  [colors.textSecondary, errorColor]
177
220
  );
178
221
  const describedById = joinDescribedBy(hasHint ? hintId : void 0, hasError ? errorId : void 0);
179
- const control = typeof children === "function" ? children({ describedById, hasError }) : children;
222
+ const isRenderChild = typeof children === "function";
223
+ const attached = isRenderChild ? { node: children({ describedById, hasError, controlId, labelId }), controlId } : attachControlId(children, controlId, describedById);
224
+ const control = attached.node;
180
225
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [spacingStyles.container, containerStyle], testID, children: [
181
226
  hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(
182
227
  FieldLabel,
183
228
  {
229
+ controlId: attached.controlId,
184
230
  label: labelText,
231
+ labelId,
185
232
  marker,
186
233
  optionalLabel,
187
234
  optionalMarkStyle: [styles.optionalMark, themeStyles.optionalMark],
@@ -211,7 +258,7 @@ var FormGrid = ({
211
258
  style,
212
259
  testID
213
260
  }) => {
214
- const metrics = React__default.default.useMemo(() => ({ minWidth, basis }), [minWidth, basis]);
261
+ const metrics = React2__default.default.useMemo(() => ({ minWidth, basis }), [minWidth, basis]);
215
262
  return /* @__PURE__ */ jsxRuntime.jsx(FormGridProvider, { value: metrics, children: /* @__PURE__ */ jsxRuntime.jsx(GapOwnedProvider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.grid, { gap }, style], testID, children }) }) });
216
263
  };
217
264
  var GROW = 1;
@@ -246,7 +293,7 @@ var styles3 = reactNative.StyleSheet.create({
246
293
  });
247
294
  var sectionSeq = 0;
248
295
  function useSectionLegendId() {
249
- const [id] = React__default.default.useState(() => {
296
+ const [id] = React2__default.default.useState(() => {
250
297
  sectionSeq += 1;
251
298
  return `form-section-legend-${String(sectionSeq)}`;
252
299
  });
@@ -333,7 +380,90 @@ var FormActions = ({
333
380
  }
334
381
  )
335
382
  ] });
336
- var IS_WEB = reactNative.Platform.OS === "web";
383
+ var FIELD_GAP = 4;
384
+ var LABEL_FONT = 11;
385
+ var LABEL_LETTER_SPACING = 0.4;
386
+ var INPUT_RADIUS = 8;
387
+ var INPUT_PAD_H = 12;
388
+ var INPUT_PAD_V = 10;
389
+ var INPUT_FONT = 14;
390
+ var INPUT_BORDER = 1;
391
+ var DATE_ROW_GAP = 8;
392
+ var CHEVRON_FONT = 10;
393
+ var MENU_TOP_GAP = 4;
394
+ var MENU_RADIUS = 8;
395
+ var MENU_PAD_V = 4;
396
+ var MENU_OPT_PAD_H = 12;
397
+ var MENU_OPT_PAD_V = 8;
398
+ var MENU_MIN_WIDTH = 160;
399
+ var MENU_MAX_HEIGHT = 260;
400
+ var MENU_Z = 1e3;
401
+ var MENU_ELEVATION = 8;
402
+ var MENU_BOX_SHADOW = "0px 2px 8px rgba(0, 0, 0, 0.15)";
403
+ var OPTION_FONT = 14;
404
+ var ERROR_FONT = 12;
405
+ var ERROR_GAP = 3;
406
+ var controlStyles = reactNative.StyleSheet.create({
407
+ /**
408
+ * The shared bordered box every dense text control renders in.
409
+ *
410
+ * `minHeight` is the kit floor: padding alone produced a ~38px box, and a bare input with no
411
+ * box style at all measured **19px** in production. Height is a hit-target guarantee, so it
412
+ * is stated rather than left to emerge from font size + padding.
413
+ */
414
+ input: {
415
+ borderWidth: INPUT_BORDER,
416
+ borderRadius: INPUT_RADIUS,
417
+ paddingHorizontal: INPUT_PAD_H,
418
+ paddingVertical: INPUT_PAD_V,
419
+ fontSize: INPUT_FONT,
420
+ minHeight: a11y.MIN_TOUCH_TARGET_PX
421
+ },
422
+ /** The select trigger: a bordered field box matching the text inputs + a chevron. */
423
+ selectTrigger: {
424
+ minHeight: a11y.MIN_TOUCH_TARGET_PX,
425
+ flexDirection: "row",
426
+ alignItems: "center",
427
+ justifyContent: "space-between",
428
+ gap: DATE_ROW_GAP,
429
+ borderWidth: INPUT_BORDER,
430
+ borderRadius: INPUT_RADIUS,
431
+ paddingHorizontal: INPUT_PAD_H,
432
+ paddingVertical: INPUT_PAD_V
433
+ },
434
+ selectTriggerText: { fontSize: INPUT_FONT, flexShrink: 1 },
435
+ chevron: { fontSize: CHEVRON_FONT },
436
+ dateRow: { flexDirection: "row", gap: DATE_ROW_GAP },
437
+ dateCol: { gap: FIELD_GAP, flex: 1 },
438
+ /**
439
+ * The micro-caption above a sub-control (a date range's From/To) and beside a switch. Note it
440
+ * is deliberately NOT the `control` label voice: it carries no `700`/uppercase, because it
441
+ * names a HALF of a control that already has a `control`-voice label above it. Uppercasing it
442
+ * too would make one field shout twice.
443
+ */
444
+ subLabel: { fontSize: LABEL_FONT, letterSpacing: LABEL_LETTER_SPACING },
445
+ /** Positioning context for a floating `AnchoredMenu` (`position:absolute; top:100%`). */
446
+ anchor: { position: "relative" },
447
+ menu: {
448
+ position: "absolute",
449
+ top: "100%",
450
+ left: 0,
451
+ marginTop: MENU_TOP_GAP,
452
+ minWidth: MENU_MIN_WIDTH,
453
+ maxHeight: MENU_MAX_HEIGHT,
454
+ borderWidth: INPUT_BORDER,
455
+ borderRadius: MENU_RADIUS,
456
+ paddingVertical: MENU_PAD_V,
457
+ zIndex: MENU_Z,
458
+ boxShadow: MENU_BOX_SHADOW,
459
+ elevation: MENU_ELEVATION,
460
+ overflow: "hidden"
461
+ },
462
+ option: { paddingHorizontal: MENU_OPT_PAD_H, paddingVertical: MENU_OPT_PAD_V },
463
+ optionText: { fontSize: OPTION_FONT },
464
+ error: { fontSize: ERROR_FONT, marginTop: ERROR_GAP }
465
+ });
466
+ var IS_WEB3 = reactNative.Platform.OS === "web";
337
467
  var TRANSITION_PROPERTY = "border-color, background-color, box-shadow";
338
468
  var TRANSITION_DURATION = "0.14s";
339
469
  var TRANSITION_TIMING = "ease";
@@ -348,7 +478,7 @@ var GREEN_START = 3;
348
478
  var BLUE_START = 5;
349
479
  var CHANNEL_END_OFFSET = 2;
350
480
  function prefersReducedMotion() {
351
- if (!IS_WEB || typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
481
+ if (!IS_WEB3 || typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
352
482
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
353
483
  }
354
484
  function withAlpha(color, alpha) {
@@ -365,33 +495,33 @@ function useThemedInput(options = {}) {
365
495
  const { hasError = false, onFocus, onBlur } = options;
366
496
  const { theme } = uiFeedback.useUi();
367
497
  const { colors, palette, semantic } = theme;
368
- const [isFocused, setIsFocused] = React.useState(false);
369
- const [isHovered, setIsHovered] = React.useState(false);
370
- const handleFocus = React.useCallback(
498
+ const [isFocused, setIsFocused] = React2.useState(false);
499
+ const [isHovered, setIsHovered] = React2.useState(false);
500
+ const handleFocus = React2.useCallback(
371
501
  (e) => {
372
502
  setIsFocused(true);
373
503
  onFocus?.(e);
374
504
  },
375
505
  [onFocus]
376
506
  );
377
- const handleBlur = React.useCallback(
507
+ const handleBlur = React2.useCallback(
378
508
  (e) => {
379
509
  setIsFocused(false);
380
510
  onBlur?.(e);
381
511
  },
382
512
  [onBlur]
383
513
  );
384
- const handleHoverIn = React.useCallback(() => setIsHovered(true), []);
385
- const handleHoverOut = React.useCallback(() => setIsHovered(false), []);
514
+ const handleHoverIn = React2.useCallback(() => setIsHovered(true), []);
515
+ const handleHoverOut = React2.useCallback(() => setIsHovered(false), []);
386
516
  const primary = palette.primary["500"];
387
517
  const errorColor = semantic.error["500"];
388
- const style = React.useMemo(() => {
518
+ const style = React2.useMemo(() => {
389
519
  const restBorder = hasError ? errorColor : colors.border;
390
520
  const focusBorder = hasError ? errorColor : primary;
391
521
  const borderColor = isFocused ? focusBorder : isHovered ? withAlpha(colors.text, HOVER_BORDER_ALPHA) : restBorder;
392
522
  const backgroundColor = isFocused ? colors.surface : colors.surfaceElevated;
393
523
  const base = { color: colors.text, borderColor, backgroundColor };
394
- if (!IS_WEB) return base;
524
+ if (!IS_WEB3) return base;
395
525
  const ringColor = hasError ? errorColor : primary;
396
526
  const ring = isFocused ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(ringColor, FOCUS_RING_ALPHA)}` : "none";
397
527
  const webStyle = {
@@ -414,9 +544,9 @@ function useThemedInput(options = {}) {
414
544
  primary,
415
545
  errorColor
416
546
  ]);
417
- const focusBind = React.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
418
- const hoverBind = React.useMemo(
419
- () => IS_WEB ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
547
+ const focusBind = React2.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
548
+ const hoverBind = React2.useMemo(
549
+ () => IS_WEB3 ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
420
550
  [handleHoverIn, handleHoverOut]
421
551
  );
422
552
  return {
@@ -428,19 +558,44 @@ function useThemedInput(options = {}) {
428
558
  hoverBind
429
559
  };
430
560
  }
431
- var IS_WEB2 = reactNative.Platform.OS === "web";
432
- var ThemedTextInput = React__default.default.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
561
+ var IS_WEB4 = reactNative.Platform.OS === "web";
562
+ function webFieldA11y({ describedById, hasError = false, required = false }) {
563
+ if (!IS_WEB4) {
564
+ return {};
565
+ }
566
+ return {
567
+ "aria-invalid": hasError ? true : void 0,
568
+ "aria-describedby": describedById,
569
+ "aria-required": required ? true : void 0
570
+ };
571
+ }
572
+ var MULTILINE_MIN_HEIGHT = 96;
573
+ var boxStyles = reactNative.StyleSheet.create({
574
+ multiline: {
575
+ minHeight: MULTILINE_MIN_HEIGHT,
576
+ textAlignVertical: "top"
577
+ }
578
+ });
579
+ var ThemedTextInput = React2__default.default.forwardRef(function ThemedTextInput2({
580
+ hasError = false,
581
+ describedById,
582
+ requiredField = false,
583
+ unstyledBox = false,
584
+ onFocus,
585
+ onBlur,
586
+ style,
587
+ placeholderTextColor,
588
+ ...rest
589
+ }, ref) {
433
590
  const {
434
591
  style: themedStyle,
435
592
  placeholderTextColor: themedPlaceholder,
436
593
  focusBind,
437
594
  hoverBind
438
595
  } = useThemedInput({ hasError, onFocus, onBlur });
439
- const webA11y = IS_WEB2 ? {
440
- "aria-invalid": hasError ? true : void 0,
441
- "aria-describedby": describedById,
442
- "aria-required": requiredField ? true : void 0
443
- } : {};
596
+ const webA11y = webFieldA11y({ describedById, hasError, required: requiredField });
597
+ const boxStyle = unstyledBox ? null : controlStyles.input;
598
+ const multilineStyle = rest.multiline === true && !unstyledBox ? boxStyles.multiline : null;
444
599
  return /* @__PURE__ */ jsxRuntime.jsx(
445
600
  reactNative.TextInput,
446
601
  {
@@ -451,37 +606,25 @@ var ThemedTextInput = React__default.default.forwardRef(function ThemedTextInput
451
606
  ...hoverBind,
452
607
  onFocus: focusBind.onFocus,
453
608
  onBlur: focusBind.onBlur,
454
- style: [style, themedStyle]
609
+ style: [boxStyle, multilineStyle, style, themedStyle]
455
610
  }
456
611
  );
457
612
  });
458
- var INPUT_PADDING_V = 10;
459
- var INPUT_PADDING_H = 12;
460
- var INPUT_BORDER_RADIUS = 8;
461
- var INPUT_BORDER_WIDTH = 1;
462
- var styles5 = reactNative.StyleSheet.create({
463
- input: {
464
- paddingVertical: INPUT_PADDING_V,
465
- paddingHorizontal: INPUT_PADDING_H,
466
- borderRadius: INPUT_BORDER_RADIUS,
467
- borderWidth: INPUT_BORDER_WIDTH
468
- }
469
- });
470
613
  var FormField = ({
471
614
  label,
472
615
  required = false,
473
616
  error,
474
617
  containerStyle,
475
618
  ...textInputProps
476
- }) => /* @__PURE__ */ jsxRuntime.jsx(Field, { containerStyle, error, label, required, children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(
619
+ }) => /* @__PURE__ */ jsxRuntime.jsx(Field, { containerStyle, error, label, required, children: ({ describedById, hasError, controlId }) => /* @__PURE__ */ jsxRuntime.jsx(
477
620
  ThemedTextInput,
478
621
  {
479
622
  accessibilityHint: `Enter ${label}`,
480
623
  accessibilityLabel: label,
481
624
  describedById,
482
625
  hasError,
626
+ id: controlId,
483
627
  requiredField: required,
484
- style: styles5.input,
485
628
  testID: "form-field-input",
486
629
  ...textInputProps
487
630
  }
@@ -519,7 +662,7 @@ function withAlpha2(hex, alpha) {
519
662
  const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
520
663
  return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
521
664
  }
522
- var styles6 = reactNative.StyleSheet.create({
665
+ var styles5 = reactNative.StyleSheet.create({
523
666
  chipContainer: {
524
667
  flexDirection: "row",
525
668
  flexWrap: "wrap",
@@ -571,7 +714,7 @@ function OutlineChip({
571
714
  ariaPressed,
572
715
  onPress
573
716
  }) {
574
- const [hovered, setHovered] = React__default.default.useState(false);
717
+ const [hovered, setHovered] = React2__default.default.useState(false);
575
718
  const active = selected || hovered;
576
719
  const borderColor = active ? colors.primary : colors.border;
577
720
  const backgroundColor = selected ? colors.tint : colors.muted;
@@ -590,7 +733,7 @@ function OutlineChip({
590
733
  onHoverIn: () => setHovered(true),
591
734
  onHoverOut: () => setHovered(false),
592
735
  onPress,
593
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles6.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.chipText, styles6.chipTextOutline, { color: textColor }], children: option.label }) })
736
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.chipText, styles5.chipTextOutline, { color: textColor }], children: option.label }) })
594
737
  }
595
738
  );
596
739
  }
@@ -618,15 +761,10 @@ function SolidChip({
618
761
  hitSlop: CHIP_HIT_SLOP,
619
762
  testID,
620
763
  onPress,
621
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles6.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.chipText, { color: textColor }], children: option.label }) })
764
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.chipText, { color: textColor }], children: option.label }) })
622
765
  }
623
766
  );
624
767
  }
625
- var IS_WEB3 = reactNative.Platform.OS === "web";
626
- function groupA11yProps(describedById, hasError) {
627
- if (!IS_WEB3) return {};
628
- return { "aria-invalid": hasError ? true : void 0, "aria-describedby": describedById };
629
- }
630
768
  var ChipSelector = ({
631
769
  label,
632
770
  options,
@@ -645,7 +783,7 @@ var ChipSelector = ({
645
783
  }) => {
646
784
  const { theme } = uiFeedback.useUi();
647
785
  const { colors, palette } = theme;
648
- const chipColors = React__default.default.useMemo(() => {
786
+ const chipColors = React2__default.default.useMemo(() => {
649
787
  const primary = palette.primary["500"];
650
788
  return {
651
789
  border: colors.border,
@@ -688,12 +826,12 @@ var ChipSelector = ({
688
826
  required,
689
827
  spacing,
690
828
  testID,
691
- children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles6.chipContainer, ...groupA11yProps(describedById, hasError), children: options.map(renderChip) })
829
+ children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles5.chipContainer, ...webFieldA11y({ describedById, hasError }), children: options.map(renderChip) })
692
830
  }
693
831
  );
694
832
  };
695
833
  var DESCRIPTION_FONT_SIZE2 = 12;
696
- var styles7 = reactNative.StyleSheet.create({
834
+ var styles6 = reactNative.StyleSheet.create({
697
835
  container: {
698
836
  flexDirection: "row",
699
837
  justifyContent: "space-between",
@@ -724,7 +862,7 @@ var FormSwitch = ({
724
862
  const { theme } = uiFeedback.useUi();
725
863
  const { colors } = theme;
726
864
  const hasDescription = typeof description === "string" && description !== "";
727
- const themeStyles = React__default.default.useMemo(() => {
865
+ const themeStyles = React2__default.default.useMemo(() => {
728
866
  const labelStyle = { color: colors.text };
729
867
  const descriptionStyle = { color: colors.textSecondary };
730
868
  return { label: labelStyle, description: descriptionStyle };
@@ -733,13 +871,13 @@ var FormSwitch = ({
733
871
  reactNative.View,
734
872
  {
735
873
  style: [
736
- styles7.container,
874
+ styles6.container,
737
875
  containerStyle
738
876
  ],
739
877
  children: [
740
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.textColumn, children: [
741
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.label, themeStyles.label], children: label }),
742
- hasDescription ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.description, themeStyles.description], children: description }) : null
878
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles6.textColumn, children: [
879
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.label, themeStyles.label], children: label }),
880
+ hasDescription ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.description, themeStyles.description], children: description }) : null
743
881
  ] }),
744
882
  /* @__PURE__ */ jsxRuntime.jsx(
745
883
  reactNative.Switch,
@@ -764,7 +902,7 @@ var BOX_SIZE = 20;
764
902
  var BOX_RADIUS = 5;
765
903
  var CHECK_FONT_SIZE = 14;
766
904
  var DISABLED_OPACITY = 0.5;
767
- var styles8 = reactNative.StyleSheet.create({
905
+ var styles7 = reactNative.StyleSheet.create({
768
906
  container: {
769
907
  flexDirection: "row",
770
908
  alignItems: "flex-start",
@@ -815,7 +953,7 @@ var FormCheckbox = ({
815
953
  const brand = palette.primary["500"];
816
954
  const hintText = typeof hint === "string" && hint !== "" ? hint : description;
817
955
  const hasHint = typeof hintText === "string" && hintText !== "";
818
- const themeStyles = React__default.default.useMemo(() => {
956
+ const themeStyles = React2__default.default.useMemo(() => {
819
957
  const boxOn = { backgroundColor: brand, borderColor: brand };
820
958
  const boxOff = { backgroundColor: "transparent", borderColor: colors.border };
821
959
  const labelStyle = { color: colors.text };
@@ -846,101 +984,26 @@ var FormCheckbox = ({
846
984
  disabled,
847
985
  onPress: toggle,
848
986
  role: "checkbox",
849
- style: [styles8.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
987
+ style: [styles7.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
850
988
  testID,
851
989
  ...keyboardProps,
852
990
  children: [
853
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles8.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
854
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.textColumn, children: [
855
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.label, themeStyles.label], children: label }),
856
- hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.hint, themeStyles.hint], children: hintText }) : null
991
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
992
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.textColumn, children: [
993
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.label, themeStyles.label], children: label }),
994
+ hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.hint, themeStyles.hint], children: hintText }) : null
857
995
  ] })
858
996
  ]
859
997
  }
860
998
  );
861
999
  };
862
- var FIELD_GAP = 4;
863
- var LABEL_FONT = 11;
864
- var LABEL_LETTER_SPACING = 0.4;
865
- var INPUT_RADIUS = 8;
866
- var INPUT_PAD_H = 12;
867
- var INPUT_PAD_V = 10;
868
- var INPUT_FONT = 14;
869
- var INPUT_BORDER = 1;
870
- var DATE_ROW_GAP = 8;
871
- var CHEVRON_FONT = 10;
872
- var MENU_TOP_GAP = 4;
873
- var MENU_RADIUS = 8;
874
- var MENU_PAD_V = 4;
875
- var MENU_OPT_PAD_H = 12;
876
- var MENU_OPT_PAD_V = 8;
877
- var MENU_MIN_WIDTH = 160;
878
- var MENU_MAX_HEIGHT = 260;
879
- var MENU_Z = 1e3;
880
- var MENU_ELEVATION = 8;
881
- var MENU_BOX_SHADOW = "0px 2px 8px rgba(0, 0, 0, 0.15)";
882
- var OPTION_FONT = 14;
883
- var ERROR_FONT = 12;
884
- var ERROR_GAP = 3;
885
- var controlStyles = reactNative.StyleSheet.create({
886
- /** The shared bordered box every dense text control renders in. */
887
- input: {
888
- borderWidth: INPUT_BORDER,
889
- borderRadius: INPUT_RADIUS,
890
- paddingHorizontal: INPUT_PAD_H,
891
- paddingVertical: INPUT_PAD_V,
892
- fontSize: INPUT_FONT
893
- },
894
- /** The select trigger: a bordered field box matching the text inputs + a chevron. */
895
- selectTrigger: {
896
- flexDirection: "row",
897
- alignItems: "center",
898
- justifyContent: "space-between",
899
- gap: DATE_ROW_GAP,
900
- borderWidth: INPUT_BORDER,
901
- borderRadius: INPUT_RADIUS,
902
- paddingHorizontal: INPUT_PAD_H,
903
- paddingVertical: INPUT_PAD_V
904
- },
905
- selectTriggerText: { fontSize: INPUT_FONT, flexShrink: 1 },
906
- chevron: { fontSize: CHEVRON_FONT },
907
- dateRow: { flexDirection: "row", gap: DATE_ROW_GAP },
908
- dateCol: { gap: FIELD_GAP, flex: 1 },
909
- /**
910
- * The micro-caption above a sub-control (a date range's From/To) and beside a switch. Note it
911
- * is deliberately NOT the `control` label voice: it carries no `700`/uppercase, because it
912
- * names a HALF of a control that already has a `control`-voice label above it. Uppercasing it
913
- * too would make one field shout twice.
914
- */
915
- subLabel: { fontSize: LABEL_FONT, letterSpacing: LABEL_LETTER_SPACING },
916
- /** Positioning context for a floating `AnchoredMenu` (`position:absolute; top:100%`). */
917
- anchor: { position: "relative" },
918
- menu: {
919
- position: "absolute",
920
- top: "100%",
921
- left: 0,
922
- marginTop: MENU_TOP_GAP,
923
- minWidth: MENU_MIN_WIDTH,
924
- maxHeight: MENU_MAX_HEIGHT,
925
- borderWidth: INPUT_BORDER,
926
- borderRadius: MENU_RADIUS,
927
- paddingVertical: MENU_PAD_V,
928
- zIndex: MENU_Z,
929
- boxShadow: MENU_BOX_SHADOW,
930
- elevation: MENU_ELEVATION,
931
- overflow: "hidden"
932
- },
933
- option: { paddingHorizontal: MENU_OPT_PAD_H, paddingVertical: MENU_OPT_PAD_V },
934
- optionText: { fontSize: OPTION_FONT },
935
- error: { fontSize: ERROR_FONT, marginTop: ERROR_GAP }
936
- });
937
- var IS_WEB4 = typeof document !== "undefined";
1000
+ var IS_WEB5 = typeof document !== "undefined";
938
1001
  var ESCAPE_KEY = "Escape";
939
1002
  var ACTIVE_WEIGHT = "700";
940
1003
  var IDLE_WEIGHT = "400";
941
1004
  function useDismiss(anchorRef, menuRef, onDismiss) {
942
- React.useEffect(() => {
943
- if (!IS_WEB4) return void 0;
1005
+ React2.useEffect(() => {
1006
+ if (!IS_WEB5) return void 0;
944
1007
  const onMouseDown = (event) => {
945
1008
  const anchorNode = anchorRef.current;
946
1009
  const menuNode = menuRef.current;
@@ -970,7 +1033,7 @@ function AnchoredMenu({
970
1033
  testID,
971
1034
  anchorRef
972
1035
  }) {
973
- const menuRef = React__default.default.useRef(null);
1036
+ const menuRef = React2__default.default.useRef(null);
974
1037
  useDismiss(anchorRef, menuRef, onDismiss);
975
1038
  return /* @__PURE__ */ jsxRuntime.jsx(
976
1039
  reactNative.View,
@@ -1009,25 +1072,26 @@ function SelectControl({
1009
1072
  placeholder = "",
1010
1073
  accessibilityLabel,
1011
1074
  accessibilityHint,
1075
+ labelledById,
1012
1076
  optionHint = "",
1013
1077
  style,
1014
1078
  testID
1015
1079
  }) {
1016
1080
  const { theme } = uiFeedback.useUi();
1017
1081
  const { colors, palette } = theme;
1018
- const anchorRef = React.useRef(null);
1019
- const [isOpen, setIsOpen] = React.useState(false);
1020
- const selectedLabel = React.useMemo(() => {
1082
+ const anchorRef = React2.useRef(null);
1083
+ const [isOpen, setIsOpen] = React2.useState(false);
1084
+ const selectedLabel = React2.useMemo(() => {
1021
1085
  const found = options.find((o) => o.value === value);
1022
1086
  return found?.label ?? placeholder;
1023
1087
  }, [options, placeholder, value]);
1024
- const close = React.useCallback(() => {
1088
+ const close = React2.useCallback(() => {
1025
1089
  setIsOpen(false);
1026
1090
  }, []);
1027
- const toggle = React.useCallback(() => {
1091
+ const toggle = React2.useCallback(() => {
1028
1092
  setIsOpen((prev) => !prev);
1029
1093
  }, []);
1030
- const select = React.useCallback(
1094
+ const select = React2.useCallback(
1031
1095
  (next) => {
1032
1096
  onChange(next);
1033
1097
  setIsOpen(false);
@@ -1043,6 +1107,7 @@ function SelectControl({
1043
1107
  accessibilityHint,
1044
1108
  accessibilityState: { expanded: isOpen },
1045
1109
  "aria-expanded": isOpen,
1110
+ "aria-labelledby": labelledById,
1046
1111
  onPress: toggle,
1047
1112
  style: [controlStyles.selectTrigger, { borderColor: colors.border, backgroundColor: colors.surface }],
1048
1113
  testID: `${testID}-trigger`,
@@ -1119,22 +1184,22 @@ function TypeaheadControl({
1119
1184
  }) {
1120
1185
  const { theme } = uiFeedback.useUi();
1121
1186
  const { colors, palette, semantic } = theme;
1122
- const anchorRef = React.useRef(null);
1123
- const [isOpen, setIsOpen] = React.useState(false);
1124
- const exactPick = React.useMemo(() => options.some((o) => o.label === value), [options, value]);
1125
- const suggestions = React.useMemo(
1187
+ const anchorRef = React2.useRef(null);
1188
+ const [isOpen, setIsOpen] = React2.useState(false);
1189
+ const exactPick = React2.useMemo(() => options.some((o) => o.label === value), [options, value]);
1190
+ const suggestions = React2.useMemo(
1126
1191
  () => exactPick ? [] : suggestOptions(options, value, minChars, maxSuggestions),
1127
1192
  [exactPick, options, value, minChars, maxSuggestions]
1128
1193
  );
1129
1194
  const showMenu = isOpen && suggestions.length > 0;
1130
- const handleChange = React.useCallback(
1195
+ const handleChange = React2.useCallback(
1131
1196
  (next) => {
1132
1197
  onChange(next);
1133
1198
  setIsOpen(true);
1134
1199
  },
1135
1200
  [onChange]
1136
1201
  );
1137
- const pick = React.useCallback(
1202
+ const pick = React2.useCallback(
1138
1203
  (optionValue) => {
1139
1204
  const opt = options.find((o) => o.value === optionValue);
1140
1205
  onChange(opt?.label ?? optionValue);
@@ -1142,10 +1207,10 @@ function TypeaheadControl({
1142
1207
  },
1143
1208
  [options, onChange]
1144
1209
  );
1145
- const handleBlur = React.useCallback(() => {
1210
+ const handleBlur = React2.useCallback(() => {
1146
1211
  setTimeout(() => setIsOpen(false), CLOSE_DELAY_MS);
1147
1212
  }, []);
1148
- const handleSubmit = React.useCallback(() => {
1213
+ const handleSubmit = React2.useCallback(() => {
1149
1214
  setIsOpen(false);
1150
1215
  onSubmit?.();
1151
1216
  }, [onSubmit]);