@dloizides/ui-forms 1.11.1 → 1.13.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/CHANGELOG.md +16 -1
- package/dist/index.d.mts +32 -10
- package/dist/index.d.ts +32 -10
- package/dist/index.js +97 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +98 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.12.0
|
|
4
|
+
|
|
5
|
+
**`FormField` gained `labelHidden` — hide the visible label, keep the accessible name.**
|
|
6
|
+
|
|
7
|
+
A field whose placeholder already names it (a toolbar search box, an inline filter) does
|
|
8
|
+
not want a stacked visible label: the label wastes a row and, worse, pushes the input a
|
|
9
|
+
label-row lower than a bare button beside it, so the two misalign. Until now the only way
|
|
10
|
+
to drop the visible label was to blank `label` — which ALSO blanked the input's
|
|
11
|
+
`accessibilityLabel`, silently stripping the accessible name.
|
|
12
|
+
|
|
13
|
+
`labelHidden` removes ONLY the visible `<label>` row: `label` stays required and the input
|
|
14
|
+
keeps `accessibilityLabel={label}`, so a screen reader reads the same name and the field
|
|
15
|
+
still resolves by role + name. Additive and backward-compatible — default `false`, every
|
|
16
|
+
existing call renders unchanged.
|
|
17
|
+
|
|
18
|
+
## 1.11.1
|
|
4
19
|
|
|
5
20
|
**`ThemedTextInput` shipped no metrics, so every direct caller got a raw 19px input.**
|
|
6
21
|
|
package/dist/index.d.mts
CHANGED
|
@@ -380,11 +380,22 @@ declare const FormActions: ({ onSave, onCancel, saveLabel, cancelLabel, saveHint
|
|
|
380
380
|
*/
|
|
381
381
|
interface FormFieldProps extends Omit<TextInputProps, 'style'> {
|
|
382
382
|
label: string;
|
|
383
|
+
/**
|
|
384
|
+
* Hide the VISIBLE label row while keeping the accessible name.
|
|
385
|
+
*
|
|
386
|
+
* For a field whose placeholder already names it (a toolbar search box, an inline
|
|
387
|
+
* filter) the stacked visible label both wastes a row and misaligns the input
|
|
388
|
+
* against a bare button beside it. Setting this drops ONLY the visible `<label>`:
|
|
389
|
+
* the input still carries `accessibilityLabel={label}`, so a screen reader reads
|
|
390
|
+
* the same name and `label` stays REQUIRED — you never lose the accessible name by
|
|
391
|
+
* blanking it, which is the trap this option exists to remove. Default `false`.
|
|
392
|
+
*/
|
|
393
|
+
labelHidden?: boolean;
|
|
383
394
|
required?: boolean;
|
|
384
395
|
error?: string;
|
|
385
396
|
containerStyle?: ViewStyle;
|
|
386
397
|
}
|
|
387
|
-
declare const FormField: ({ label, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
|
|
398
|
+
declare const FormField: ({ label, labelHidden, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
|
|
388
399
|
|
|
389
400
|
/**
|
|
390
401
|
* `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
|
|
@@ -722,17 +733,20 @@ declare function DateRangeControl({ value, onChange, onSubmit, fromLabel, toLabe
|
|
|
722
733
|
/**
|
|
723
734
|
* `AnchoredMenu` — the floating option list shared by `SelectControl` and `TypeaheadControl`.
|
|
724
735
|
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
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.
|
|
729
744
|
*
|
|
730
745
|
* Promoted verbatim out of `@dloizides/ui-tables`' private `Filters/components/AnchoredMenu`.
|
|
731
746
|
*
|
|
732
|
-
* Deliberately
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
* 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}.
|
|
736
750
|
*/
|
|
737
751
|
|
|
738
752
|
/** The four theme colours the menu paints with — passed in, so the menu stays theme-flat. */
|
|
@@ -755,8 +769,16 @@ interface AnchoredMenuProps {
|
|
|
755
769
|
testID: string;
|
|
756
770
|
/** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
|
|
757
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;
|
|
758
780
|
}
|
|
759
|
-
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;
|
|
760
782
|
|
|
761
783
|
declare const controlStyles: {
|
|
762
784
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -380,11 +380,22 @@ declare const FormActions: ({ onSave, onCancel, saveLabel, cancelLabel, saveHint
|
|
|
380
380
|
*/
|
|
381
381
|
interface FormFieldProps extends Omit<TextInputProps, 'style'> {
|
|
382
382
|
label: string;
|
|
383
|
+
/**
|
|
384
|
+
* Hide the VISIBLE label row while keeping the accessible name.
|
|
385
|
+
*
|
|
386
|
+
* For a field whose placeholder already names it (a toolbar search box, an inline
|
|
387
|
+
* filter) the stacked visible label both wastes a row and misaligns the input
|
|
388
|
+
* against a bare button beside it. Setting this drops ONLY the visible `<label>`:
|
|
389
|
+
* the input still carries `accessibilityLabel={label}`, so a screen reader reads
|
|
390
|
+
* the same name and `label` stays REQUIRED — you never lose the accessible name by
|
|
391
|
+
* blanking it, which is the trap this option exists to remove. Default `false`.
|
|
392
|
+
*/
|
|
393
|
+
labelHidden?: boolean;
|
|
383
394
|
required?: boolean;
|
|
384
395
|
error?: string;
|
|
385
396
|
containerStyle?: ViewStyle;
|
|
386
397
|
}
|
|
387
|
-
declare const FormField: ({ label, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
|
|
398
|
+
declare const FormField: ({ label, labelHidden, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
|
|
388
399
|
|
|
389
400
|
/**
|
|
390
401
|
* `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
|
|
@@ -722,17 +733,20 @@ declare function DateRangeControl({ value, onChange, onSubmit, fromLabel, toLabe
|
|
|
722
733
|
/**
|
|
723
734
|
* `AnchoredMenu` — the floating option list shared by `SelectControl` and `TypeaheadControl`.
|
|
724
735
|
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
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.
|
|
729
744
|
*
|
|
730
745
|
* Promoted verbatim out of `@dloizides/ui-tables`' private `Filters/components/AnchoredMenu`.
|
|
731
746
|
*
|
|
732
|
-
* Deliberately
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
* 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}.
|
|
736
750
|
*/
|
|
737
751
|
|
|
738
752
|
/** The four theme colours the menu paints with — passed in, so the menu stays theme-flat. */
|
|
@@ -755,8 +769,16 @@ interface AnchoredMenuProps {
|
|
|
755
769
|
testID: string;
|
|
756
770
|
/** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
|
|
757
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;
|
|
758
780
|
}
|
|
759
|
-
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;
|
|
760
782
|
|
|
761
783
|
declare const controlStyles: {
|
|
762
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
|
|
|
@@ -612,23 +613,33 @@ var ThemedTextInput = React2__default.default.forwardRef(function ThemedTextInpu
|
|
|
612
613
|
});
|
|
613
614
|
var FormField = ({
|
|
614
615
|
label,
|
|
616
|
+
labelHidden = false,
|
|
615
617
|
required = false,
|
|
616
618
|
error,
|
|
617
619
|
containerStyle,
|
|
618
620
|
...textInputProps
|
|
619
|
-
}) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
620
|
-
|
|
621
|
+
}) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
622
|
+
Field,
|
|
621
623
|
{
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
624
|
+
containerStyle,
|
|
625
|
+
error,
|
|
626
|
+
label: labelHidden ? void 0 : label,
|
|
627
|
+
required,
|
|
628
|
+
children: ({ describedById, hasError, controlId }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
629
|
+
ThemedTextInput,
|
|
630
|
+
{
|
|
631
|
+
accessibilityHint: `Enter ${label}`,
|
|
632
|
+
accessibilityLabel: label,
|
|
633
|
+
describedById,
|
|
634
|
+
hasError,
|
|
635
|
+
id: controlId,
|
|
636
|
+
requiredField: required,
|
|
637
|
+
testID: "form-field-input",
|
|
638
|
+
...textInputProps
|
|
639
|
+
}
|
|
640
|
+
)
|
|
630
641
|
}
|
|
631
|
-
)
|
|
642
|
+
);
|
|
632
643
|
var TRANSPARENT_COLOR = "transparent";
|
|
633
644
|
var WHITE_COLOR = "#fff";
|
|
634
645
|
var SOLID_RADIUS = 20;
|
|
@@ -997,13 +1008,76 @@ var FormCheckbox = ({
|
|
|
997
1008
|
}
|
|
998
1009
|
);
|
|
999
1010
|
};
|
|
1000
|
-
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";
|
|
1001
1075
|
var ESCAPE_KEY = "Escape";
|
|
1002
1076
|
var ACTIVE_WEIGHT = "700";
|
|
1003
1077
|
var IDLE_WEIGHT = "400";
|
|
1004
1078
|
function useDismiss(anchorRef, menuRef, onDismiss) {
|
|
1005
1079
|
React2.useEffect(() => {
|
|
1006
|
-
if (!
|
|
1080
|
+
if (!IS_WEB6) return void 0;
|
|
1007
1081
|
const onMouseDown = (event) => {
|
|
1008
1082
|
const anchorNode = anchorRef.current;
|
|
1009
1083
|
const menuNode = menuRef.current;
|
|
@@ -1031,16 +1105,19 @@ function AnchoredMenu({
|
|
|
1031
1105
|
colors,
|
|
1032
1106
|
optionHint,
|
|
1033
1107
|
testID,
|
|
1034
|
-
anchorRef
|
|
1108
|
+
anchorRef,
|
|
1109
|
+
usePortal = false
|
|
1035
1110
|
}) {
|
|
1036
1111
|
const menuRef = React2__default.default.useRef(null);
|
|
1037
1112
|
useDismiss(anchorRef, menuRef, onDismiss);
|
|
1038
|
-
|
|
1113
|
+
const rect = useAnchorRect(anchorRef, usePortal);
|
|
1114
|
+
const portalStyle = rect !== null ? buildPortalPopoverStyle(rect) : null;
|
|
1115
|
+
const menu = /* @__PURE__ */ jsxRuntime.jsx(
|
|
1039
1116
|
reactNative.View,
|
|
1040
1117
|
{
|
|
1041
1118
|
ref: menuRef,
|
|
1042
1119
|
accessibilityRole: "menu",
|
|
1043
|
-
style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }],
|
|
1120
|
+
style: [controlStyles.menu, { borderColor: colors.border, backgroundColor: colors.surface }, portalStyle],
|
|
1044
1121
|
testID: `${testID}-menu`,
|
|
1045
1122
|
children: options.map((opt) => {
|
|
1046
1123
|
const active = opt.value === selectedValue;
|
|
@@ -1062,6 +1139,8 @@ function AnchoredMenu({
|
|
|
1062
1139
|
})
|
|
1063
1140
|
}
|
|
1064
1141
|
);
|
|
1142
|
+
if (usePortal && IS_WEB6) return reactDom.createPortal(menu, document.body);
|
|
1143
|
+
return menu;
|
|
1065
1144
|
}
|
|
1066
1145
|
var CHEVRON_DOWN = "\u25BE";
|
|
1067
1146
|
var CHEVRON_UP = "\u25B4";
|
|
@@ -1245,7 +1324,8 @@ function TypeaheadControl({
|
|
|
1245
1324
|
colors: { text: colors.text, border: colors.border, surface: colors.surface, brand: palette.primary["500"] },
|
|
1246
1325
|
optionHint,
|
|
1247
1326
|
testID,
|
|
1248
|
-
anchorRef
|
|
1327
|
+
anchorRef,
|
|
1328
|
+
usePortal: true
|
|
1249
1329
|
}
|
|
1250
1330
|
) : null,
|
|
1251
1331
|
hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { role: "alert", style: [controlStyles.error, { color: semantic.error["500"] }], testID: `${testID}-error`, children: error }) : null
|