@seed-design/lynx-react 0.7.0 → 0.8.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/lib/components/Accordion/Accordion.jsx +2 -2
- package/lib/components/ActionButton/ActionButton.jsx +1 -1
- package/lib/components/AlertDialog/AlertDialog.d.ts +64 -0
- package/lib/components/AlertDialog/AlertDialog.d.ts.map +1 -0
- package/lib/components/AlertDialog/AlertDialog.jsx +138 -0
- package/lib/components/AlertDialog/AlertDialog.namespace.d.ts +2 -0
- package/lib/components/AlertDialog/AlertDialog.namespace.d.ts.map +1 -0
- package/lib/components/AlertDialog/AlertDialog.namespace.js +17 -0
- package/lib/components/AlertDialog/index.d.ts +3 -0
- package/lib/components/AlertDialog/index.d.ts.map +1 -0
- package/lib/components/AppBar/AppBar.d.ts +14 -1
- package/lib/components/AppBar/AppBar.d.ts.map +1 -1
- package/lib/components/AppBar/AppBar.jsx +46 -10
- package/lib/components/AppBar/AppBar.namespace.d.ts +1 -1
- package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -1
- package/lib/components/AppBar/index.d.ts +1 -1
- package/lib/components/AppBar/index.d.ts.map +1 -1
- package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts +182 -0
- package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts.map +1 -0
- package/lib/components/AttachmentDisplay/AttachmentDisplay.jsx +444 -0
- package/lib/components/AttachmentDisplay/index.d.ts +2 -0
- package/lib/components/AttachmentDisplay/index.d.ts.map +1 -0
- package/lib/components/AttachmentInput/AttachmentInput.d.ts +179 -0
- package/lib/components/AttachmentInput/AttachmentInput.d.ts.map +1 -0
- package/lib/components/AttachmentInput/AttachmentInput.jsx +520 -0
- package/lib/components/AttachmentInput/index.d.ts +2 -0
- package/lib/components/AttachmentInput/index.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.d.ts +1 -1
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.jsx +1 -7
- package/lib/components/Callout/Callout.jsx +2 -2
- package/lib/components/Checkbox/Checkbox.jsx +2 -2
- package/lib/components/Chip/Chip.jsx +2 -2
- package/lib/components/ChipTabs/ChipTabs.d.ts +41 -0
- package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -0
- package/lib/components/ChipTabs/ChipTabs.jsx +39 -0
- package/lib/components/ChipTabs/ChipTabs.namespace.d.ts +2 -0
- package/lib/components/ChipTabs/ChipTabs.namespace.d.ts.map +1 -0
- package/lib/components/ChipTabs/ChipTabs.namespace.js +13 -0
- package/lib/components/ChipTabs/index.d.ts +3 -0
- package/lib/components/ChipTabs/index.d.ts.map +1 -0
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts +18 -0
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -0
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.jsx +113 -0
- package/lib/components/ContextualFloatingButton/index.d.ts +2 -0
- package/lib/components/ContextualFloatingButton/index.d.ts.map +1 -0
- package/lib/components/Dialog/Dialog.d.ts +87 -0
- package/lib/components/Dialog/Dialog.d.ts.map +1 -0
- package/lib/components/Dialog/Dialog.jsx +147 -0
- package/lib/components/Dialog/Dialog.namespace.d.ts +2 -0
- package/lib/components/Dialog/Dialog.namespace.d.ts.map +1 -0
- package/lib/components/Dialog/Dialog.namespace.js +18 -0
- package/lib/components/Dialog/index.d.ts +3 -0
- package/lib/components/Dialog/index.d.ts.map +1 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.d.ts +23 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.jsx +76 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts +2 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts.map +1 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.namespace.js +10 -0
- package/lib/components/FloatingActionButton/index.d.ts +3 -0
- package/lib/components/FloatingActionButton/index.d.ts.map +1 -0
- package/lib/components/HelpBubble/HelpBubble.d.ts +67 -0
- package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -0
- package/lib/components/HelpBubble/HelpBubble.jsx +600 -0
- package/lib/components/HelpBubble/HelpBubble.namespace.d.ts +2 -0
- package/lib/components/HelpBubble/HelpBubble.namespace.d.ts.map +1 -0
- package/lib/components/HelpBubble/HelpBubble.namespace.js +18 -0
- package/lib/components/HelpBubble/index.d.ts +3 -0
- package/lib/components/HelpBubble/index.d.ts.map +1 -0
- package/lib/components/Icon/Icon.d.ts +1 -0
- package/lib/components/Icon/Icon.d.ts.map +1 -1
- package/lib/components/Icon/Icon.jsx +3 -2
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts +28 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts.map +1 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.jsx +37 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts +2 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts.map +1 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.js +9 -0
- package/lib/components/IdentityPlaceholder/identity-placeholder-business.js +4 -0
- package/lib/components/IdentityPlaceholder/identity-placeholder-person.js +4 -0
- package/lib/components/IdentityPlaceholder/index.d.ts +3 -0
- package/lib/components/IdentityPlaceholder/index.d.ts.map +1 -0
- package/lib/components/InputButton/InputButton.d.ts +2 -0
- package/lib/components/InputButton/InputButton.d.ts.map +1 -1
- package/lib/components/InputButton/InputButton.jsx +40 -8
- package/lib/components/List/List.d.ts +1 -1
- package/lib/components/List/List.d.ts.map +1 -1
- package/lib/components/List/List.jsx +26 -12
- package/lib/components/Menu/Menu.d.ts.map +1 -1
- package/lib/components/Menu/Menu.jsx +15 -5
- package/lib/components/PageBanner/PageBanner.d.ts +3 -0
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
- package/lib/components/PageBanner/PageBanner.jsx +49 -11
- package/lib/components/QuantityPicker/QuantityPicker.d.ts +53 -0
- package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
- package/lib/components/QuantityPicker/QuantityPicker.jsx +282 -0
- package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
- package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
- package/lib/components/QuantityPicker/QuantityPicker.namespace.js +11 -0
- package/lib/components/QuantityPicker/index.d.ts +3 -0
- package/lib/components/QuantityPicker/index.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
- package/lib/components/ReactionButton/ReactionButton.d.ts +1 -1
- package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
- package/lib/components/ReactionButton/ReactionButton.jsx +22 -13
- package/lib/components/ScrollFog/ScrollFog.d.ts +41 -0
- package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -0
- package/lib/components/ScrollFog/ScrollFog.jsx +65 -0
- package/lib/components/ScrollFog/index.d.ts +2 -0
- package/lib/components/ScrollFog/index.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.jsx +2 -2
- package/lib/components/Select/Select.d.ts +112 -0
- package/lib/components/Select/Select.d.ts.map +1 -0
- package/lib/components/Select/Select.jsx +885 -0
- package/lib/components/Select/Select.namespace.d.ts +2 -0
- package/lib/components/Select/Select.namespace.d.ts.map +1 -0
- package/lib/components/Select/Select.namespace.js +23 -0
- package/lib/components/Select/index.d.ts +3 -0
- package/lib/components/Select/index.d.ts.map +1 -0
- package/lib/components/Skeleton/Skeleton.d.ts +15 -0
- package/lib/components/Skeleton/Skeleton.d.ts.map +1 -0
- package/lib/components/Skeleton/Skeleton.jsx +18 -0
- package/lib/components/Skeleton/index.d.ts +2 -0
- package/lib/components/Skeleton/index.d.ts.map +1 -0
- package/lib/components/Slider/Slider.d.ts +101 -0
- package/lib/components/Slider/Slider.d.ts.map +1 -0
- package/lib/components/Slider/Slider.jsx +573 -0
- package/lib/components/Slider/Slider.namespace.d.ts +2 -0
- package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
- package/lib/components/Slider/Slider.namespace.js +19 -0
- package/lib/components/Slider/Slider.utils.d.ts +19 -0
- package/lib/components/Slider/Slider.utils.d.ts.map +1 -0
- package/lib/components/Slider/Slider.utils.js +106 -0
- package/lib/components/Slider/index.d.ts +3 -0
- package/lib/components/Slider/index.d.ts.map +1 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +22 -8
- package/lib/components/Switch/Switch.d.ts.map +1 -1
- package/lib/components/Switch/Switch.jsx +5 -3
- package/lib/components/Tabs/Tabs.d.ts +34 -2
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.jsx +186 -56
- package/lib/components/Tabs/Tabs.utils.d.ts +14 -1
- package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.utils.js +24 -3
- package/lib/components/Tabs/index.d.ts +1 -1
- package/lib/components/Tabs/index.d.ts.map +1 -1
- package/lib/components/index.d.ts +14 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/hooks/useControllableState.d.ts +1 -14
- package/lib/hooks/useControllableState.d.ts.map +1 -1
- package/lib/hooks/usePressTap.d.ts +1 -27
- package/lib/hooks/usePressTap.d.ts.map +1 -1
- package/lib/hooks/useScaleFeedback.d.ts +1 -1
- package/lib/hooks/useScaleFeedback.js +1 -1
- package/lib/index.js +29 -6
- package/lib/{hooks/useControllableState.js → packages/lynx-react-headless/use-controllable-state/lib/index.js} +2 -3
- package/lib/{hooks/usePressTap.js → packages/lynx-react-headless/use-press-tap/lib/index.js} +13 -10
- package/lib/types.d.ts +1 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/components/ActionButton/ActionButton.test.tsx +7 -7
- package/src/components/AlertDialog/AlertDialog.namespace.ts +22 -0
- package/src/components/AlertDialog/AlertDialog.test.tsx +226 -0
- package/src/components/AlertDialog/AlertDialog.tsx +374 -0
- package/src/components/AlertDialog/index.ts +24 -0
- package/src/components/AppBar/AppBar.namespace.ts +1 -0
- package/src/components/AppBar/AppBar.test.tsx +257 -0
- package/src/components/AppBar/AppBar.tsx +109 -14
- package/src/components/AppBar/index.ts +1 -0
- package/src/components/AttachmentDisplay/AttachmentDisplay.tsx +886 -0
- package/src/components/AttachmentDisplay/index.ts +48 -0
- package/src/components/AttachmentInput/AttachmentInput.tsx +1001 -0
- package/src/components/AttachmentInput/index.ts +49 -0
- package/src/components/BottomSheet/BottomSheet.test.tsx +22 -7
- package/src/components/BottomSheet/BottomSheet.tsx +1 -19
- package/src/components/ChipTabs/ChipTabs.namespace.ts +14 -0
- package/src/components/ChipTabs/ChipTabs.tsx +119 -0
- package/src/components/ChipTabs/index.ts +16 -0
- package/src/components/ContextualFloatingButton/ContextualFloatingButton.test.tsx +63 -0
- package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +232 -0
- package/src/components/ContextualFloatingButton/index.ts +4 -0
- package/src/components/Dialog/Dialog.namespace.ts +24 -0
- package/src/components/Dialog/Dialog.test.tsx +172 -0
- package/src/components/Dialog/Dialog.tsx +373 -0
- package/src/components/Dialog/index.ts +26 -0
- package/src/components/FloatingActionButton/FloatingActionButton.namespace.ts +8 -0
- package/src/components/FloatingActionButton/FloatingActionButton.test.tsx +59 -0
- package/src/components/FloatingActionButton/FloatingActionButton.tsx +207 -0
- package/src/components/FloatingActionButton/index.ts +10 -0
- package/src/components/HelpBubble/HelpBubble.namespace.ts +24 -0
- package/src/components/HelpBubble/HelpBubble.tsx +998 -0
- package/src/components/HelpBubble/index.ts +26 -0
- package/src/components/Icon/Icon.test.tsx +57 -5
- package/src/components/Icon/Icon.tsx +16 -2
- package/src/components/IdentityPlaceholder/IdentityPlaceholder.namespace.ts +6 -0
- package/src/components/IdentityPlaceholder/IdentityPlaceholder.tsx +103 -0
- package/src/components/IdentityPlaceholder/assets.d.ts +4 -0
- package/src/components/IdentityPlaceholder/identity-placeholder-business.webp +0 -0
- package/src/components/IdentityPlaceholder/identity-placeholder-person.webp +0 -0
- package/src/components/IdentityPlaceholder/index.ts +8 -0
- package/src/components/InputButton/InputButton.test.tsx +128 -1
- package/src/components/InputButton/InputButton.tsx +60 -9
- package/src/components/List/List.test.tsx +54 -2
- package/src/components/List/List.tsx +43 -10
- package/src/components/Menu/Menu.tsx +14 -4
- package/src/components/Menu/__tests__/Menu.test.tsx +34 -0
- package/src/components/PageBanner/PageBanner.test.tsx +110 -3
- package/src/components/PageBanner/PageBanner.tsx +83 -19
- package/src/components/QuantityPicker/QuantityPicker.namespace.ts +10 -0
- package/src/components/QuantityPicker/QuantityPicker.test.tsx +235 -0
- package/src/components/QuantityPicker/QuantityPicker.tsx +605 -0
- package/src/components/QuantityPicker/index.ts +14 -0
- package/src/components/ReactionButton/ReactionButton.feedback.test.tsx +50 -0
- package/src/components/ReactionButton/ReactionButton.test.tsx +6 -4
- package/src/components/ReactionButton/ReactionButton.tsx +20 -12
- package/src/components/ScrollFog/ScrollFog.test.tsx +222 -0
- package/src/components/ScrollFog/ScrollFog.tsx +172 -0
- package/src/components/ScrollFog/index.ts +1 -0
- package/src/components/Select/Select.namespace.ts +34 -0
- package/src/components/Select/Select.test.tsx +297 -0
- package/src/components/Select/Select.tsx +1483 -0
- package/src/components/Select/index.ts +39 -0
- package/src/components/Skeleton/Skeleton.test.tsx +64 -0
- package/src/components/Skeleton/Skeleton.tsx +45 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Slider/Slider.namespace.ts +26 -0
- package/src/components/Slider/Slider.test.tsx +260 -0
- package/src/components/Slider/Slider.tsx +897 -0
- package/src/components/Slider/Slider.utils.ts +167 -0
- package/src/components/Slider/index.ts +30 -0
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +21 -1
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +30 -7
- package/src/components/Switch/Switch.tsx +14 -13
- package/src/components/Tabs/Tabs.test.tsx +218 -1
- package/src/components/Tabs/Tabs.tsx +346 -75
- package/src/components/Tabs/Tabs.utils.ts +51 -1
- package/src/components/Tabs/index.ts +17 -1
- package/src/components/index.ts +14 -0
- package/src/hooks/useControllableState.ts +1 -43
- package/src/hooks/usePressTap.test.ts +23 -11
- package/src/hooks/usePressTap.ts +1 -60
- package/src/hooks/useScaleFeedback.ts +1 -1
- package/src/types.ts +1 -0
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import { isValidElement } from "@lynx-js/react";
|
|
3
|
+
import { getRectByRef } from "@lynx-js/lynx-ui-common";
|
|
4
|
+
import type { BaseEvent, NodesRef } from "@lynx-js/types";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
|
|
7
|
+
import { select, type SelectVariantProps } from "@seed-design/lynx-css/recipes/select";
|
|
8
|
+
import {
|
|
9
|
+
selectTrigger,
|
|
10
|
+
type SelectTriggerVariantProps,
|
|
11
|
+
} from "@seed-design/lynx-css/recipes/select-trigger";
|
|
12
|
+
import { selectItem, type SelectItemVariantProps } from "@seed-design/lynx-css/recipes/select-item";
|
|
13
|
+
import { select as selectVars } from "@seed-design/lynx-css/vars/component";
|
|
14
|
+
|
|
15
|
+
import { useControllableState } from "../../hooks/useControllableState";
|
|
16
|
+
import { usePressTap } from "../../hooks/usePressTap";
|
|
17
|
+
import type {
|
|
18
|
+
LynxAccessibilityProps,
|
|
19
|
+
LynxPressableProps,
|
|
20
|
+
LynxIconElementProps,
|
|
21
|
+
LynxStyledElementProps,
|
|
22
|
+
LynxTextRef,
|
|
23
|
+
LynxViewProps,
|
|
24
|
+
LynxViewRef,
|
|
25
|
+
} from "../../types";
|
|
26
|
+
import { toArray } from "../../utils/children";
|
|
27
|
+
import { useFieldContext } from "../Field/context";
|
|
28
|
+
import { InternalIcon, type InternalIconProps } from "../Icon/Icon";
|
|
29
|
+
import {
|
|
30
|
+
computePosition,
|
|
31
|
+
type Placement as SelectPlacement,
|
|
32
|
+
type Position as SelectPosition,
|
|
33
|
+
type Rect as SelectRect,
|
|
34
|
+
} from "../private/Positioning";
|
|
35
|
+
|
|
36
|
+
const EMPTY_VALUE: string[] = [];
|
|
37
|
+
// `$dimension.x2` is the Rootage Select spacing token. Generated component vars retain
|
|
38
|
+
// it as CSS `var()` for styling, but floating-point collision math needs its numeric 8px value.
|
|
39
|
+
const selectGutter = 8;
|
|
40
|
+
const selectOverflowPadding = 8;
|
|
41
|
+
const selectMaxHeight = Number.parseFloat(selectVars.base.enabled.root.maxHeight);
|
|
42
|
+
// This protects placement near the screen edge; it does not impose a minimum list viewport.
|
|
43
|
+
const selectMinimumAvailableHeight = 200;
|
|
44
|
+
let nextSelectScrollAreaId = 0;
|
|
45
|
+
|
|
46
|
+
interface SelectClassNames {
|
|
47
|
+
positioner: string;
|
|
48
|
+
backdrop: string;
|
|
49
|
+
content: string;
|
|
50
|
+
scrollArea: string;
|
|
51
|
+
scrollContent: string;
|
|
52
|
+
group: string;
|
|
53
|
+
groupLabel: string;
|
|
54
|
+
separator: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface SelectItemClassNames {
|
|
58
|
+
root: string;
|
|
59
|
+
pressedOverlay: string;
|
|
60
|
+
body: string;
|
|
61
|
+
label: string;
|
|
62
|
+
description: string;
|
|
63
|
+
prefixIcon: string;
|
|
64
|
+
indicator: string;
|
|
65
|
+
}
|
|
66
|
+
type NativeTapHandler = NonNullable<LynxViewProps["bindtap"]>;
|
|
67
|
+
type NativeTransitionHandler = NonNullable<LynxViewProps["bindtransitionend"]>;
|
|
68
|
+
type NativeLayoutHandler = NonNullable<LynxViewProps["bindlayoutchange"]>;
|
|
69
|
+
type SelectTriggerHandlers = Pick<LynxViewProps, "bindtap" | "main-thread:bindtap">;
|
|
70
|
+
|
|
71
|
+
type SelectPublicVariantProps = Omit<SelectVariantProps, "size" | "open" | "positioned"> & {
|
|
72
|
+
size?: "large" | "medium" | "responsive";
|
|
73
|
+
};
|
|
74
|
+
type SelectTriggerPublicVariantProps = Omit<
|
|
75
|
+
SelectTriggerVariantProps,
|
|
76
|
+
"size" | "open" | "pressed" | "disabled" | "readOnly" | "invalid"
|
|
77
|
+
>;
|
|
78
|
+
type SelectItemPublicVariantProps = Omit<
|
|
79
|
+
SelectItemVariantProps,
|
|
80
|
+
"size" | "selected" | "pressed" | "disabled"
|
|
81
|
+
>;
|
|
82
|
+
|
|
83
|
+
export type SelectOpenChangeReason = "trigger" | "interactOutside" | "itemSelect" | "dismiss";
|
|
84
|
+
|
|
85
|
+
export interface SelectOpenChangeDetails {
|
|
86
|
+
reason: SelectOpenChangeReason;
|
|
87
|
+
event: BaseEvent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SelectSelectedItem {
|
|
91
|
+
value: string;
|
|
92
|
+
label: React.ReactNode;
|
|
93
|
+
textValue: string;
|
|
94
|
+
prefixIcon?: React.ReactNode;
|
|
95
|
+
resolved: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface SelectOptionEntry {
|
|
99
|
+
label: React.ReactNode;
|
|
100
|
+
textValue: string;
|
|
101
|
+
prefixIcon?: React.ReactNode;
|
|
102
|
+
node: NodesRef | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface RegisteredSelectItem extends SelectSelectedItem {
|
|
106
|
+
node: NodesRef | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SelectContextValue {
|
|
110
|
+
value: string[];
|
|
111
|
+
selectedItems: SelectSelectedItem[];
|
|
112
|
+
selectedItem: RegisteredSelectItem | undefined;
|
|
113
|
+
displayValue: React.ReactNode;
|
|
114
|
+
showPlaceholder: boolean;
|
|
115
|
+
multiple: boolean;
|
|
116
|
+
open: boolean;
|
|
117
|
+
mounted: boolean;
|
|
118
|
+
openEpoch: number;
|
|
119
|
+
disabled: boolean;
|
|
120
|
+
readOnly: boolean;
|
|
121
|
+
invalid: boolean;
|
|
122
|
+
required: boolean;
|
|
123
|
+
size: "large" | "medium";
|
|
124
|
+
placement: SelectPlacement;
|
|
125
|
+
gutter: number;
|
|
126
|
+
overflowPadding: number;
|
|
127
|
+
isOpenRef: React.MutableRefObject<boolean>;
|
|
128
|
+
openEpochRef: React.MutableRefObject<number>;
|
|
129
|
+
triggerRef: React.MutableRefObject<NodesRef | null>;
|
|
130
|
+
triggerHandlers: SelectTriggerHandlers;
|
|
131
|
+
setTriggerHandlers: (handlers: SelectTriggerHandlers) => void;
|
|
132
|
+
classes: SelectClassNames;
|
|
133
|
+
positioned: boolean;
|
|
134
|
+
setPositioned: (positioned: boolean) => void;
|
|
135
|
+
requestOpen: (open: boolean, details: SelectOpenChangeDetails) => void;
|
|
136
|
+
finishClose: (immediate?: boolean) => void;
|
|
137
|
+
selectValue: (value: string, event: BaseEvent) => void;
|
|
138
|
+
registerOption: (value: string, entry: SelectOptionEntry) => void;
|
|
139
|
+
unregisterOption: (value: string) => void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface SelectItemContextValue {
|
|
143
|
+
label: React.ReactNode;
|
|
144
|
+
prefixIcon?: React.ReactNode;
|
|
145
|
+
selected: boolean;
|
|
146
|
+
disabled: boolean;
|
|
147
|
+
pressed: boolean;
|
|
148
|
+
classes: SelectItemClassNames;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const SelectContext = React.createContext<SelectContextValue | null>(null);
|
|
152
|
+
const SelectItemContext = React.createContext<SelectItemContextValue | null>(null);
|
|
153
|
+
const SelectGroupPositionContext = React.createContext({ isFirst: true });
|
|
154
|
+
|
|
155
|
+
function useSelectContext(consumer: string): SelectContextValue {
|
|
156
|
+
const context = React.useContext(SelectContext);
|
|
157
|
+
if (!context) throw new Error(`<${consumer}/> must be rendered inside <SelectRoot/>.`);
|
|
158
|
+
return context;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function useSelectItemContext(consumer: string): SelectItemContextValue {
|
|
162
|
+
const context = React.useContext(SelectItemContext);
|
|
163
|
+
if (!context) throw new Error(`<${consumer}/> must be rendered inside <SelectItem/>.`);
|
|
164
|
+
return context;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mergeNodeRef(forwardedRef: React.ForwardedRef<unknown>, node: NodesRef | null) {
|
|
168
|
+
if (typeof forwardedRef === "function") {
|
|
169
|
+
forwardedRef(node);
|
|
170
|
+
} else if (forwardedRef) {
|
|
171
|
+
forwardedRef.current = node;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getScreenRect(): SelectRect | null {
|
|
176
|
+
const systemInfo = typeof SystemInfo === "undefined" ? undefined : SystemInfo;
|
|
177
|
+
const pixelWidth = systemInfo?.pixelWidth;
|
|
178
|
+
const pixelHeight = systemInfo?.pixelHeight;
|
|
179
|
+
const pixelRatio = systemInfo?.pixelRatio;
|
|
180
|
+
if (
|
|
181
|
+
typeof pixelWidth !== "number" ||
|
|
182
|
+
typeof pixelHeight !== "number" ||
|
|
183
|
+
typeof pixelRatio !== "number" ||
|
|
184
|
+
pixelWidth <= 0 ||
|
|
185
|
+
pixelHeight <= 0 ||
|
|
186
|
+
pixelRatio <= 0
|
|
187
|
+
) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const width = pixelWidth / pixelRatio;
|
|
192
|
+
const height = pixelHeight / pixelRatio;
|
|
193
|
+
return { left: 0, top: 0, right: width, bottom: height, width, height };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getRootRect() {
|
|
197
|
+
"background only";
|
|
198
|
+
return getRectByRef({ current: lynx.createSelectorQuery().selectRoot() }, true);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function toPixel(value: number) {
|
|
202
|
+
return `${value}px`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function areStylesEqual(left: object | undefined, right: object | undefined) {
|
|
206
|
+
if (left === right) return true;
|
|
207
|
+
if (!left || !right) return false;
|
|
208
|
+
const leftEntries = Object.entries(left);
|
|
209
|
+
if (leftEntries.length !== Object.keys(right).length) return false;
|
|
210
|
+
return leftEntries.every(([key, value]) =>
|
|
211
|
+
Object.is(value, (right as Record<string, unknown>)[key]),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hasExitTransition(event: Parameters<NativeTransitionHandler>[0]): boolean {
|
|
216
|
+
if (event.target.uid !== event.currentTarget.uid) return false;
|
|
217
|
+
return (
|
|
218
|
+
event.params.animation_type === "transition-opacity" ||
|
|
219
|
+
event.params.animation_name === "opacity"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isSameValue(a: string[], b: string[]) {
|
|
224
|
+
return a.length === b.length && a.every((entry, index) => entry === b[index]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderTextContent(
|
|
228
|
+
className: string,
|
|
229
|
+
value: React.ReactNode,
|
|
230
|
+
ref: React.ForwardedRef<unknown>,
|
|
231
|
+
nativeProps: Omit<LynxStyledElementProps, "children" | "className">,
|
|
232
|
+
) {
|
|
233
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
234
|
+
return (
|
|
235
|
+
<text {...(ref ? { ref: ref as LynxTextRef } : {})} className={className} {...nativeProps}>
|
|
236
|
+
{value}
|
|
237
|
+
</text>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<view {...(ref ? { ref: ref as LynxViewRef } : {})} className={className} {...nativeProps}>
|
|
243
|
+
{value}
|
|
244
|
+
</view>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @platform Lynx
|
|
252
|
+
*
|
|
253
|
+
* Web-only DOM APIs are intentionally omitted: `asChild`, hidden native select/name/form
|
|
254
|
+
* submission, DOM focus/typeahead/keyboard navigation, and ARIA ids all rely on a browser DOM.
|
|
255
|
+
* Lynx uses native tap, overlay dismissal, `accessibility-*` attributes, and app-owned state.
|
|
256
|
+
*/
|
|
257
|
+
export interface SelectRootProps
|
|
258
|
+
extends SelectPublicVariantProps,
|
|
259
|
+
SelectTriggerPublicVariantProps,
|
|
260
|
+
SelectItemPublicVariantProps,
|
|
261
|
+
LynxStyledElementProps {
|
|
262
|
+
value?: string[];
|
|
263
|
+
defaultValue?: string[];
|
|
264
|
+
onValueChange?: (value: string[]) => void;
|
|
265
|
+
multiple?: boolean;
|
|
266
|
+
open?: boolean;
|
|
267
|
+
defaultOpen?: boolean;
|
|
268
|
+
onOpenChange?: (open: boolean, details: SelectOpenChangeDetails) => void;
|
|
269
|
+
disabled?: boolean;
|
|
270
|
+
readOnly?: boolean;
|
|
271
|
+
invalid?: boolean;
|
|
272
|
+
required?: boolean;
|
|
273
|
+
placement?: SelectPlacement;
|
|
274
|
+
gutter?: number;
|
|
275
|
+
overflowPadding?: number;
|
|
276
|
+
formatValue?: (items: SelectSelectedItem[]) => React.ReactNode;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const SelectRoot = React.forwardRef<unknown, SelectRootProps>((props, ref) => {
|
|
280
|
+
const screenRect = getScreenRect();
|
|
281
|
+
const [variantProps, otherProps] = select.splitVariantProps({
|
|
282
|
+
...props,
|
|
283
|
+
size:
|
|
284
|
+
props.size === "responsive"
|
|
285
|
+
? screenRect?.width != null && screenRect.width >= 1280
|
|
286
|
+
? "medium"
|
|
287
|
+
: "large"
|
|
288
|
+
: props.size,
|
|
289
|
+
});
|
|
290
|
+
const { size = "large", open: openProp } = variantProps;
|
|
291
|
+
const {
|
|
292
|
+
children,
|
|
293
|
+
className,
|
|
294
|
+
value: valueProp,
|
|
295
|
+
defaultValue = EMPTY_VALUE,
|
|
296
|
+
onValueChange,
|
|
297
|
+
multiple = false,
|
|
298
|
+
defaultOpen = false,
|
|
299
|
+
onOpenChange,
|
|
300
|
+
disabled: disabledProp,
|
|
301
|
+
readOnly: readOnlyProp,
|
|
302
|
+
invalid: invalidProp,
|
|
303
|
+
required: requiredProp,
|
|
304
|
+
placement = "bottom",
|
|
305
|
+
gutter = selectGutter,
|
|
306
|
+
overflowPadding = selectOverflowPadding,
|
|
307
|
+
formatValue,
|
|
308
|
+
...nativeProps
|
|
309
|
+
} = otherProps;
|
|
310
|
+
const fieldContext = useFieldContext({ strict: false });
|
|
311
|
+
const disabled = disabledProp ?? fieldContext?.disabled ?? false;
|
|
312
|
+
const readOnly = readOnlyProp ?? fieldContext?.readOnly ?? false;
|
|
313
|
+
const invalid = invalidProp ?? fieldContext?.invalid ?? false;
|
|
314
|
+
const required = requiredProp ?? fieldContext?.required ?? false;
|
|
315
|
+
const [value, setValueState] = useControllableState({
|
|
316
|
+
value: valueProp,
|
|
317
|
+
defaultValue,
|
|
318
|
+
onChange: onValueChange,
|
|
319
|
+
});
|
|
320
|
+
const [open, setOpenState] = useControllableState({
|
|
321
|
+
value: openProp,
|
|
322
|
+
defaultValue: defaultOpen,
|
|
323
|
+
});
|
|
324
|
+
const [optionRegistry, setOptionRegistry] = React.useState<
|
|
325
|
+
ReadonlyMap<string, SelectOptionEntry>
|
|
326
|
+
>(() => new Map());
|
|
327
|
+
const [mounted, setMounted] = React.useState(open);
|
|
328
|
+
const [positionedEpoch, setPositionedEpoch] = React.useState<number | null>(null);
|
|
329
|
+
const isOpenRef = React.useRef(open);
|
|
330
|
+
const openEpochRef = React.useRef(0);
|
|
331
|
+
if (open && !isOpenRef.current) openEpochRef.current++;
|
|
332
|
+
isOpenRef.current = open;
|
|
333
|
+
const openEpoch = openEpochRef.current;
|
|
334
|
+
const positioned = positionedEpoch === openEpoch;
|
|
335
|
+
const [triggerHandlers, setTriggerHandlers] = React.useState<SelectTriggerHandlers>({});
|
|
336
|
+
const triggerRef = React.useRef<NodesRef | null>(null);
|
|
337
|
+
const classes = select({ ...variantProps, open, positioned });
|
|
338
|
+
|
|
339
|
+
const selectedItems = React.useMemo(
|
|
340
|
+
() =>
|
|
341
|
+
value.map((optionValue): SelectSelectedItem => {
|
|
342
|
+
const entry = optionRegistry.get(optionValue);
|
|
343
|
+
return entry
|
|
344
|
+
? { ...entry, value: optionValue, resolved: true }
|
|
345
|
+
: { value: optionValue, label: null, textValue: "", resolved: false };
|
|
346
|
+
}),
|
|
347
|
+
[optionRegistry, value],
|
|
348
|
+
);
|
|
349
|
+
const selectedOption = value.length === 1 ? optionRegistry.get(value[0] ?? "") : undefined;
|
|
350
|
+
const selectedItem = selectedOption
|
|
351
|
+
? { ...selectedOption, value: value[0] ?? "", resolved: true }
|
|
352
|
+
: undefined;
|
|
353
|
+
const showPlaceholder =
|
|
354
|
+
value.length === 0 ||
|
|
355
|
+
(optionRegistry.size > 0 && selectedItems.every((item) => !item.resolved));
|
|
356
|
+
const displayValue = showPlaceholder
|
|
357
|
+
? undefined
|
|
358
|
+
: formatValue
|
|
359
|
+
? formatValue(selectedItems)
|
|
360
|
+
: selectedItems
|
|
361
|
+
.filter((item) => item.resolved)
|
|
362
|
+
.map((item) => item.textValue)
|
|
363
|
+
.join(", ");
|
|
364
|
+
|
|
365
|
+
React.useEffect(() => {
|
|
366
|
+
"background only";
|
|
367
|
+
if (open) {
|
|
368
|
+
setMounted(true);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!positioned) setMounted(false);
|
|
372
|
+
}, [open, positioned]);
|
|
373
|
+
|
|
374
|
+
const setPositioned = React.useCallback(
|
|
375
|
+
(nextPositioned: boolean) => setPositionedEpoch(nextPositioned ? openEpochRef.current : null),
|
|
376
|
+
[],
|
|
377
|
+
);
|
|
378
|
+
const requestOpen = React.useCallback(
|
|
379
|
+
(nextOpen: boolean, details: SelectOpenChangeDetails) => {
|
|
380
|
+
"background only";
|
|
381
|
+
if (nextOpen && (disabled || readOnly)) return;
|
|
382
|
+
if (nextOpen === open) return;
|
|
383
|
+
setOpenState(nextOpen);
|
|
384
|
+
onOpenChange?.(nextOpen, details);
|
|
385
|
+
},
|
|
386
|
+
[disabled, onOpenChange, open, readOnly, setOpenState],
|
|
387
|
+
);
|
|
388
|
+
const finishClose = React.useCallback((immediate = false) => {
|
|
389
|
+
"background only";
|
|
390
|
+
if (immediate || !isOpenRef.current) {
|
|
391
|
+
setMounted(false);
|
|
392
|
+
setPositionedEpoch(null);
|
|
393
|
+
}
|
|
394
|
+
}, []);
|
|
395
|
+
const selectValue = React.useCallback(
|
|
396
|
+
(nextValue: string, event: BaseEvent) => {
|
|
397
|
+
"background only";
|
|
398
|
+
if (disabled || readOnly) return;
|
|
399
|
+
const next = multiple
|
|
400
|
+
? value.includes(nextValue)
|
|
401
|
+
? value.filter((entry) => entry !== nextValue)
|
|
402
|
+
: [...value, nextValue]
|
|
403
|
+
: [nextValue];
|
|
404
|
+
if (!isSameValue(value, next)) setValueState(next);
|
|
405
|
+
if (!multiple) requestOpen(false, { reason: "itemSelect", event });
|
|
406
|
+
},
|
|
407
|
+
[disabled, multiple, readOnly, requestOpen, setValueState, value],
|
|
408
|
+
);
|
|
409
|
+
const registerOption = React.useCallback((optionValue: string, entry: SelectOptionEntry) => {
|
|
410
|
+
setOptionRegistry((current) => {
|
|
411
|
+
const previous = current.get(optionValue);
|
|
412
|
+
if (
|
|
413
|
+
previous &&
|
|
414
|
+
previous.label === entry.label &&
|
|
415
|
+
previous.textValue === entry.textValue &&
|
|
416
|
+
previous.prefixIcon === entry.prefixIcon &&
|
|
417
|
+
previous.node === entry.node
|
|
418
|
+
) {
|
|
419
|
+
return current;
|
|
420
|
+
}
|
|
421
|
+
return new Map(current).set(optionValue, entry);
|
|
422
|
+
});
|
|
423
|
+
}, []);
|
|
424
|
+
const unregisterOption = React.useCallback((optionValue: string) => {
|
|
425
|
+
setOptionRegistry((current) => {
|
|
426
|
+
if (!current.has(optionValue)) return current;
|
|
427
|
+
const next = new Map(current);
|
|
428
|
+
next.delete(optionValue);
|
|
429
|
+
return next;
|
|
430
|
+
});
|
|
431
|
+
}, []);
|
|
432
|
+
|
|
433
|
+
const contextValue = React.useMemo<SelectContextValue>(
|
|
434
|
+
() => ({
|
|
435
|
+
value,
|
|
436
|
+
selectedItems,
|
|
437
|
+
selectedItem,
|
|
438
|
+
displayValue,
|
|
439
|
+
showPlaceholder,
|
|
440
|
+
multiple,
|
|
441
|
+
open,
|
|
442
|
+
mounted,
|
|
443
|
+
openEpoch,
|
|
444
|
+
disabled,
|
|
445
|
+
readOnly,
|
|
446
|
+
invalid,
|
|
447
|
+
required,
|
|
448
|
+
size,
|
|
449
|
+
placement,
|
|
450
|
+
gutter,
|
|
451
|
+
overflowPadding,
|
|
452
|
+
isOpenRef,
|
|
453
|
+
openEpochRef,
|
|
454
|
+
triggerRef,
|
|
455
|
+
triggerHandlers,
|
|
456
|
+
setTriggerHandlers,
|
|
457
|
+
classes,
|
|
458
|
+
positioned,
|
|
459
|
+
setPositioned,
|
|
460
|
+
requestOpen,
|
|
461
|
+
finishClose,
|
|
462
|
+
selectValue,
|
|
463
|
+
registerOption,
|
|
464
|
+
unregisterOption,
|
|
465
|
+
}),
|
|
466
|
+
[
|
|
467
|
+
classes,
|
|
468
|
+
disabled,
|
|
469
|
+
displayValue,
|
|
470
|
+
finishClose,
|
|
471
|
+
gutter,
|
|
472
|
+
invalid,
|
|
473
|
+
mounted,
|
|
474
|
+
multiple,
|
|
475
|
+
open,
|
|
476
|
+
openEpoch,
|
|
477
|
+
overflowPadding,
|
|
478
|
+
placement,
|
|
479
|
+
positioned,
|
|
480
|
+
readOnly,
|
|
481
|
+
registerOption,
|
|
482
|
+
requestOpen,
|
|
483
|
+
required,
|
|
484
|
+
selectValue,
|
|
485
|
+
selectedItem,
|
|
486
|
+
selectedItems,
|
|
487
|
+
setPositioned,
|
|
488
|
+
showPlaceholder,
|
|
489
|
+
size,
|
|
490
|
+
triggerHandlers,
|
|
491
|
+
unregisterOption,
|
|
492
|
+
value,
|
|
493
|
+
],
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<SelectContext.Provider value={contextValue}>
|
|
498
|
+
<view {...(ref ? { ref: ref as LynxViewRef } : {})} className={className} {...nativeProps}>
|
|
499
|
+
{children}
|
|
500
|
+
</view>
|
|
501
|
+
</SelectContext.Provider>
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
SelectRoot.displayName = "SelectRoot";
|
|
505
|
+
|
|
506
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
507
|
+
|
|
508
|
+
export interface SelectTriggerProps
|
|
509
|
+
extends SelectTriggerPublicVariantProps,
|
|
510
|
+
LynxStyledElementProps,
|
|
511
|
+
LynxPressableProps,
|
|
512
|
+
LynxAccessibilityProps {
|
|
513
|
+
placeholder?: React.ReactNode;
|
|
514
|
+
prefixIcon?: React.ReactNode;
|
|
515
|
+
suffixIcon?: React.ReactNode;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export const SelectTrigger = React.forwardRef<unknown, SelectTriggerProps>((props, ref) => {
|
|
519
|
+
const {
|
|
520
|
+
children,
|
|
521
|
+
className,
|
|
522
|
+
placeholder,
|
|
523
|
+
prefixIcon,
|
|
524
|
+
suffixIcon,
|
|
525
|
+
bindtap,
|
|
526
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
527
|
+
"accessibility-element": accessibilityElement = true,
|
|
528
|
+
"accessibility-label": accessibilityLabel,
|
|
529
|
+
"accessibility-traits": accessibilityTraits,
|
|
530
|
+
...nativeProps
|
|
531
|
+
} = props;
|
|
532
|
+
const context = useSelectContext("SelectTrigger");
|
|
533
|
+
const nonInteractive = context.disabled || context.readOnly;
|
|
534
|
+
const handleTap = React.useCallback<NativeTapHandler>(
|
|
535
|
+
(event, instance) => {
|
|
536
|
+
"background only";
|
|
537
|
+
context.requestOpen(!context.open, { reason: "trigger", event });
|
|
538
|
+
bindtap?.(event, instance);
|
|
539
|
+
},
|
|
540
|
+
[bindtap, context.open, context.requestOpen],
|
|
541
|
+
);
|
|
542
|
+
const {
|
|
543
|
+
pressed,
|
|
544
|
+
bindtap: proxyBindtap,
|
|
545
|
+
...pressHandlers
|
|
546
|
+
} = usePressTap({
|
|
547
|
+
disabled: nonInteractive,
|
|
548
|
+
onTap: handleTap,
|
|
549
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
550
|
+
});
|
|
551
|
+
const mainThreadProxyBindtap = pressHandlers["main-thread:bindtap"];
|
|
552
|
+
React.useEffect(() => {
|
|
553
|
+
"background only";
|
|
554
|
+
context.setTriggerHandlers({
|
|
555
|
+
bindtap: proxyBindtap,
|
|
556
|
+
"main-thread:bindtap": mainThreadProxyBindtap,
|
|
557
|
+
});
|
|
558
|
+
return () => {
|
|
559
|
+
context.setTriggerHandlers({});
|
|
560
|
+
};
|
|
561
|
+
}, [context.setTriggerHandlers, mainThreadProxyBindtap, proxyBindtap]);
|
|
562
|
+
const handleRef = React.useCallback(
|
|
563
|
+
(node: NodesRef | null) => {
|
|
564
|
+
context.triggerRef.current = node;
|
|
565
|
+
mergeNodeRef(ref, node);
|
|
566
|
+
},
|
|
567
|
+
[context.triggerRef, ref],
|
|
568
|
+
);
|
|
569
|
+
const classes = selectTrigger({
|
|
570
|
+
size: context.size,
|
|
571
|
+
open: context.open,
|
|
572
|
+
pressed,
|
|
573
|
+
disabled: context.disabled,
|
|
574
|
+
readOnly: context.readOnly,
|
|
575
|
+
invalid: context.invalid,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<view
|
|
580
|
+
ref={handleRef as LynxViewRef}
|
|
581
|
+
className={clsx(classes.root, className)}
|
|
582
|
+
accessibility-element={accessibilityElement}
|
|
583
|
+
accessibility-label={accessibilityLabel}
|
|
584
|
+
accessibility-role-description="button"
|
|
585
|
+
accessibility-value={context.open ? "expanded" : "collapsed"}
|
|
586
|
+
accessibility-traits={nonInteractive ? "disabled" : (accessibilityTraits ?? "button")}
|
|
587
|
+
{...nativeProps}
|
|
588
|
+
{...pressHandlers}
|
|
589
|
+
bindtap={proxyBindtap}
|
|
590
|
+
>
|
|
591
|
+
<view className={classes.pressedOverlay} accessibility-elements-hidden={true} />
|
|
592
|
+
{children ?? (
|
|
593
|
+
<>
|
|
594
|
+
<SelectPrefixIcon fallback={prefixIcon} />
|
|
595
|
+
<SelectValue />
|
|
596
|
+
<SelectPlaceholder>{placeholder}</SelectPlaceholder>
|
|
597
|
+
<SelectSuffixIcon icon={suffixIcon} />
|
|
598
|
+
</>
|
|
599
|
+
)}
|
|
600
|
+
</view>
|
|
601
|
+
);
|
|
602
|
+
});
|
|
603
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
604
|
+
|
|
605
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
606
|
+
|
|
607
|
+
export interface SelectValueProps extends LynxStyledElementProps {}
|
|
608
|
+
|
|
609
|
+
export const SelectValue = React.forwardRef<unknown, SelectValueProps>((props, ref) => {
|
|
610
|
+
const { children, className, ...nativeProps } = props;
|
|
611
|
+
const context = useSelectContext("SelectValue");
|
|
612
|
+
if (context.showPlaceholder) return null;
|
|
613
|
+
return renderTextContent(
|
|
614
|
+
clsx(
|
|
615
|
+
selectTrigger({
|
|
616
|
+
size: context.size,
|
|
617
|
+
open: context.open,
|
|
618
|
+
disabled: context.disabled,
|
|
619
|
+
readOnly: context.readOnly,
|
|
620
|
+
invalid: context.invalid,
|
|
621
|
+
}).value,
|
|
622
|
+
className,
|
|
623
|
+
),
|
|
624
|
+
children ?? context.displayValue,
|
|
625
|
+
ref,
|
|
626
|
+
nativeProps,
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
SelectValue.displayName = "SelectValue";
|
|
630
|
+
|
|
631
|
+
export interface SelectPlaceholderProps extends LynxStyledElementProps {}
|
|
632
|
+
|
|
633
|
+
export const SelectPlaceholder = React.forwardRef<unknown, SelectPlaceholderProps>((props, ref) => {
|
|
634
|
+
const { children, className, ...nativeProps } = props;
|
|
635
|
+
const context = useSelectContext("SelectPlaceholder");
|
|
636
|
+
if (!context.showPlaceholder) return null;
|
|
637
|
+
return renderTextContent(
|
|
638
|
+
clsx(
|
|
639
|
+
selectTrigger({
|
|
640
|
+
size: context.size,
|
|
641
|
+
open: context.open,
|
|
642
|
+
disabled: context.disabled,
|
|
643
|
+
readOnly: context.readOnly,
|
|
644
|
+
invalid: context.invalid,
|
|
645
|
+
}).placeholder,
|
|
646
|
+
className,
|
|
647
|
+
),
|
|
648
|
+
children,
|
|
649
|
+
ref,
|
|
650
|
+
nativeProps,
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
SelectPlaceholder.displayName = "SelectPlaceholder";
|
|
654
|
+
|
|
655
|
+
export interface SelectPrefixIconProps extends Omit<InternalIconProps, "icon" | "deps"> {
|
|
656
|
+
fallback?: React.ReactNode;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export const SelectPrefixIcon = React.forwardRef<unknown, SelectPrefixIconProps>((props, ref) => {
|
|
660
|
+
const { fallback, className, ...nativeProps } = props;
|
|
661
|
+
const context = useSelectContext("SelectPrefixIcon");
|
|
662
|
+
const icon = context.selectedItem?.prefixIcon ?? fallback;
|
|
663
|
+
if (!isValidElement<LynxIconElementProps>(icon)) return null;
|
|
664
|
+
const classes = selectTrigger({
|
|
665
|
+
size: context.size,
|
|
666
|
+
open: context.open,
|
|
667
|
+
disabled: context.disabled,
|
|
668
|
+
readOnly: context.readOnly,
|
|
669
|
+
invalid: context.invalid,
|
|
670
|
+
});
|
|
671
|
+
return (
|
|
672
|
+
<InternalIcon
|
|
673
|
+
ref={ref}
|
|
674
|
+
icon={icon}
|
|
675
|
+
className={clsx(classes.prefixIcon, className)}
|
|
676
|
+
accessibility-elements-hidden={true}
|
|
677
|
+
deps={[context.selectedItem?.prefixIcon, context.value]}
|
|
678
|
+
{...nativeProps}
|
|
679
|
+
/>
|
|
680
|
+
);
|
|
681
|
+
});
|
|
682
|
+
SelectPrefixIcon.displayName = "SelectPrefixIcon";
|
|
683
|
+
|
|
684
|
+
export interface SelectSuffixIconProps extends Omit<InternalIconProps, "icon" | "deps"> {
|
|
685
|
+
icon?: React.ReactNode;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export const SelectSuffixIcon = React.forwardRef<unknown, SelectSuffixIconProps>((props, ref) => {
|
|
689
|
+
const { icon, className, ...nativeProps } = props;
|
|
690
|
+
const context = useSelectContext("SelectSuffixIcon");
|
|
691
|
+
const classes = selectTrigger({
|
|
692
|
+
size: context.size,
|
|
693
|
+
open: context.open,
|
|
694
|
+
disabled: context.disabled,
|
|
695
|
+
readOnly: context.readOnly,
|
|
696
|
+
invalid: context.invalid,
|
|
697
|
+
});
|
|
698
|
+
if (!isValidElement<LynxIconElementProps>(icon)) {
|
|
699
|
+
return (
|
|
700
|
+
<text
|
|
701
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
702
|
+
className={clsx(classes.suffixIcon, className)}
|
|
703
|
+
accessibility-elements-hidden={true}
|
|
704
|
+
{...nativeProps}
|
|
705
|
+
>
|
|
706
|
+
⌄
|
|
707
|
+
</text>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
return (
|
|
711
|
+
<InternalIcon
|
|
712
|
+
ref={ref}
|
|
713
|
+
icon={icon}
|
|
714
|
+
className={clsx(classes.suffixIcon, className)}
|
|
715
|
+
accessibility-elements-hidden={true}
|
|
716
|
+
deps={[context.open]}
|
|
717
|
+
{...nativeProps}
|
|
718
|
+
/>
|
|
719
|
+
);
|
|
720
|
+
});
|
|
721
|
+
SelectSuffixIcon.displayName = "SelectSuffixIcon";
|
|
722
|
+
|
|
723
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
724
|
+
|
|
725
|
+
export interface SelectContentProps extends LynxStyledElementProps {}
|
|
726
|
+
|
|
727
|
+
export const SelectContent = React.forwardRef<unknown, SelectContentProps>((props, ref) => {
|
|
728
|
+
const { children, className, style, ...nativeProps } = props;
|
|
729
|
+
const context = useSelectContext("SelectContent");
|
|
730
|
+
const measurementVersionRef = React.useRef(0);
|
|
731
|
+
const intrinsicMeasurementVersionRef = React.useRef(0);
|
|
732
|
+
const scrollRequestVersionRef = React.useRef(0);
|
|
733
|
+
const scrolledEpochRef = React.useRef<number | null>(null);
|
|
734
|
+
// Snapshot refs can be recreated on each patch. Their current native node belongs in a ref;
|
|
735
|
+
// the one-time attachment state merely starts effects that require an attached node.
|
|
736
|
+
const overlayNodeRef = React.useRef<NodesRef | null>(null);
|
|
737
|
+
const scrollNodeRef = React.useRef<NodesRef | null>(null);
|
|
738
|
+
const scrollContentNodeRef = React.useRef<NodesRef | null>(null);
|
|
739
|
+
const overlayAttachedRef = React.useRef(false);
|
|
740
|
+
const scrollAttachedRef = React.useRef(false);
|
|
741
|
+
const scrollContentAttachedRef = React.useRef(false);
|
|
742
|
+
const [overlayAttached, setOverlayAttached] = React.useState(false);
|
|
743
|
+
const [scrollAttached, setScrollAttached] = React.useState(false);
|
|
744
|
+
const [scrollContentAttached, setScrollContentAttached] = React.useState(false);
|
|
745
|
+
const [scrollAreaId, setScrollAreaId] = React.useState<string | undefined>(undefined);
|
|
746
|
+
React.useEffect(() => {
|
|
747
|
+
"background only";
|
|
748
|
+
// Generate once on the background thread, then share the id through state.
|
|
749
|
+
setScrollAreaId(`seed-select-scroll-area-${nextSelectScrollAreaId++}`);
|
|
750
|
+
}, []);
|
|
751
|
+
const measurementConfigRef = React.useRef(0);
|
|
752
|
+
const configuredClassNameRef = React.useRef(className);
|
|
753
|
+
const configuredSizeRef = React.useRef(context.size);
|
|
754
|
+
const configuredStyleRef = React.useRef(style);
|
|
755
|
+
const [intrinsicSize, setIntrinsicSize] = React.useState<{
|
|
756
|
+
width: number;
|
|
757
|
+
height: number;
|
|
758
|
+
epoch: number;
|
|
759
|
+
config: number;
|
|
760
|
+
} | null>(null);
|
|
761
|
+
const [position, setPosition] = React.useState<SelectPosition | null>(null);
|
|
762
|
+
const [widthConstraint, setWidthConstraint] = React.useState<number | null>(null);
|
|
763
|
+
const [overlayRect, setOverlayRect] = React.useState<SelectRect | null>(null);
|
|
764
|
+
const [triggerRect, setTriggerRect] = React.useState<SelectRect | null>(null);
|
|
765
|
+
const selectedNode = context.selectedItem?.node;
|
|
766
|
+
|
|
767
|
+
const measurePosition = React.useCallback(async () => {
|
|
768
|
+
"background only";
|
|
769
|
+
const referenceNode = context.triggerRef.current;
|
|
770
|
+
const openEpoch = context.openEpoch;
|
|
771
|
+
const overlayNode = overlayNodeRef.current;
|
|
772
|
+
if (
|
|
773
|
+
!context.open ||
|
|
774
|
+
!referenceNode ||
|
|
775
|
+
!overlayNode ||
|
|
776
|
+
!intrinsicSize ||
|
|
777
|
+
intrinsicSize.epoch !== openEpoch ||
|
|
778
|
+
intrinsicSize.config !== measurementConfigRef.current
|
|
779
|
+
) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const version = ++measurementVersionRef.current;
|
|
783
|
+
try {
|
|
784
|
+
const [reference, boundary, overlay] = await Promise.all([
|
|
785
|
+
getRectByRef({ current: referenceNode }, true),
|
|
786
|
+
getRootRect(),
|
|
787
|
+
getRectByRef({ current: overlayNode }, true),
|
|
788
|
+
]);
|
|
789
|
+
if (
|
|
790
|
+
version !== measurementVersionRef.current ||
|
|
791
|
+
!context.isOpenRef.current ||
|
|
792
|
+
openEpoch !== context.openEpochRef.current
|
|
793
|
+
) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const width = widthConstraint ?? reference.width;
|
|
797
|
+
const nextPosition = await computePosition({
|
|
798
|
+
reference,
|
|
799
|
+
boundary,
|
|
800
|
+
width,
|
|
801
|
+
height: Math.min(intrinsicSize.height, selectMaxHeight),
|
|
802
|
+
placement: context.placement,
|
|
803
|
+
gutter: context.gutter,
|
|
804
|
+
overflowPadding: context.overflowPadding,
|
|
805
|
+
flip: { fallbackStrategy: "bestFit" },
|
|
806
|
+
shift: { crossAxis: true },
|
|
807
|
+
size: { order: "beforeFlip", minimumHeight: selectMinimumAvailableHeight },
|
|
808
|
+
});
|
|
809
|
+
if (
|
|
810
|
+
version !== measurementVersionRef.current ||
|
|
811
|
+
!context.isOpenRef.current ||
|
|
812
|
+
openEpoch !== context.openEpochRef.current
|
|
813
|
+
) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (nextPosition.availableWidth < width) {
|
|
817
|
+
const constrainedWidth = nextPosition.availableWidth;
|
|
818
|
+
if (widthConstraint !== constrainedWidth) {
|
|
819
|
+
measurementVersionRef.current++;
|
|
820
|
+
measurementConfigRef.current++;
|
|
821
|
+
intrinsicMeasurementVersionRef.current++;
|
|
822
|
+
scrollRequestVersionRef.current++;
|
|
823
|
+
setWidthConstraint(constrainedWidth);
|
|
824
|
+
setIntrinsicSize(null);
|
|
825
|
+
setPosition(null);
|
|
826
|
+
setOverlayRect(null);
|
|
827
|
+
setTriggerRect(null);
|
|
828
|
+
context.setPositioned(false);
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
setPosition((previous) => {
|
|
833
|
+
if (
|
|
834
|
+
previous?.left === nextPosition.left &&
|
|
835
|
+
previous.top === nextPosition.top &&
|
|
836
|
+
previous.width === nextPosition.width &&
|
|
837
|
+
previous.height === nextPosition.height &&
|
|
838
|
+
previous.placement === nextPosition.placement
|
|
839
|
+
) {
|
|
840
|
+
return previous;
|
|
841
|
+
}
|
|
842
|
+
return nextPosition;
|
|
843
|
+
});
|
|
844
|
+
setOverlayRect(overlay);
|
|
845
|
+
setTriggerRect(reference);
|
|
846
|
+
context.setPositioned(true);
|
|
847
|
+
} catch {
|
|
848
|
+
// Native nodes can disappear while a selector query is in flight; keep the popup hidden.
|
|
849
|
+
}
|
|
850
|
+
}, [
|
|
851
|
+
context.gutter,
|
|
852
|
+
context.isOpenRef,
|
|
853
|
+
context.open,
|
|
854
|
+
context.openEpoch,
|
|
855
|
+
context.openEpochRef,
|
|
856
|
+
context.overflowPadding,
|
|
857
|
+
context.placement,
|
|
858
|
+
context.setPositioned,
|
|
859
|
+
context.triggerRef,
|
|
860
|
+
intrinsicSize,
|
|
861
|
+
overlayAttached,
|
|
862
|
+
widthConstraint,
|
|
863
|
+
]);
|
|
864
|
+
|
|
865
|
+
const measureIntrinsicSize = React.useCallback(async () => {
|
|
866
|
+
"background only";
|
|
867
|
+
const scrollNode = scrollNodeRef.current;
|
|
868
|
+
const scrollContentNode = scrollContentNodeRef.current;
|
|
869
|
+
const openEpoch = context.openEpochRef.current;
|
|
870
|
+
const config = measurementConfigRef.current;
|
|
871
|
+
if (!context.isOpenRef.current || !scrollAreaId || !scrollNode || !scrollContentNode) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const version = ++intrinsicMeasurementVersionRef.current;
|
|
875
|
+
try {
|
|
876
|
+
const rect = await getRectByRef({ current: scrollContentNode }, false, scrollAreaId);
|
|
877
|
+
if (
|
|
878
|
+
version !== intrinsicMeasurementVersionRef.current ||
|
|
879
|
+
!context.isOpenRef.current ||
|
|
880
|
+
openEpoch !== context.openEpochRef.current ||
|
|
881
|
+
config !== measurementConfigRef.current
|
|
882
|
+
) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
setIntrinsicSize((current) => {
|
|
886
|
+
if (
|
|
887
|
+
current?.width === rect.width &&
|
|
888
|
+
current.height === rect.height &&
|
|
889
|
+
current.epoch === openEpoch &&
|
|
890
|
+
current.config === config
|
|
891
|
+
) {
|
|
892
|
+
return current;
|
|
893
|
+
}
|
|
894
|
+
return { width: rect.width, height: rect.height, epoch: openEpoch, config };
|
|
895
|
+
});
|
|
896
|
+
} catch {
|
|
897
|
+
// The hidden overlay may release either node before the native query resolves.
|
|
898
|
+
}
|
|
899
|
+
}, [context.isOpenRef, context.openEpochRef, scrollAreaId]);
|
|
900
|
+
|
|
901
|
+
React.useEffect(() => {
|
|
902
|
+
"background only";
|
|
903
|
+
measurementVersionRef.current++;
|
|
904
|
+
intrinsicMeasurementVersionRef.current++;
|
|
905
|
+
scrollRequestVersionRef.current++;
|
|
906
|
+
setWidthConstraint(null);
|
|
907
|
+
setPosition(null);
|
|
908
|
+
setOverlayRect(null);
|
|
909
|
+
setTriggerRect(null);
|
|
910
|
+
setIntrinsicSize((current) => (current ? { ...current, epoch: context.openEpoch } : current));
|
|
911
|
+
scrolledEpochRef.current = null;
|
|
912
|
+
if (scrollNodeRef.current && scrollContentNodeRef.current) void measureIntrinsicSize();
|
|
913
|
+
}, [context.openEpoch, measureIntrinsicSize]);
|
|
914
|
+
React.useEffect(() => {
|
|
915
|
+
"background only";
|
|
916
|
+
if (context.open) return;
|
|
917
|
+
measurementVersionRef.current++;
|
|
918
|
+
intrinsicMeasurementVersionRef.current++;
|
|
919
|
+
scrollRequestVersionRef.current++;
|
|
920
|
+
}, [context.open]);
|
|
921
|
+
React.useEffect(() => {
|
|
922
|
+
"background only";
|
|
923
|
+
return () => {
|
|
924
|
+
measurementVersionRef.current++;
|
|
925
|
+
intrinsicMeasurementVersionRef.current++;
|
|
926
|
+
scrollRequestVersionRef.current++;
|
|
927
|
+
};
|
|
928
|
+
}, []);
|
|
929
|
+
|
|
930
|
+
React.useEffect(() => {
|
|
931
|
+
"background only";
|
|
932
|
+
if (
|
|
933
|
+
configuredClassNameRef.current === className &&
|
|
934
|
+
configuredSizeRef.current === context.size &&
|
|
935
|
+
areStylesEqual(configuredStyleRef.current, style)
|
|
936
|
+
) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
measurementVersionRef.current++;
|
|
940
|
+
intrinsicMeasurementVersionRef.current++;
|
|
941
|
+
scrollRequestVersionRef.current++;
|
|
942
|
+
measurementConfigRef.current++;
|
|
943
|
+
configuredClassNameRef.current = className;
|
|
944
|
+
configuredSizeRef.current = context.size;
|
|
945
|
+
configuredStyleRef.current = style;
|
|
946
|
+
setWidthConstraint(null);
|
|
947
|
+
setIntrinsicSize(null);
|
|
948
|
+
setPosition(null);
|
|
949
|
+
setOverlayRect(null);
|
|
950
|
+
setTriggerRect(null);
|
|
951
|
+
context.setPositioned(false);
|
|
952
|
+
if (scrollNodeRef.current && scrollContentNodeRef.current) void measureIntrinsicSize();
|
|
953
|
+
}, [className, context.setPositioned, context.size, measureIntrinsicSize, style]);
|
|
954
|
+
|
|
955
|
+
React.useEffect(() => {
|
|
956
|
+
"background only";
|
|
957
|
+
if (scrollAttached && scrollContentAttached) void measureIntrinsicSize();
|
|
958
|
+
}, [measureIntrinsicSize, scrollAttached, scrollContentAttached]);
|
|
959
|
+
|
|
960
|
+
React.useEffect(() => {
|
|
961
|
+
"background only";
|
|
962
|
+
if (context.open) void measurePosition();
|
|
963
|
+
}, [context.open, measurePosition]);
|
|
964
|
+
|
|
965
|
+
React.useEffect(() => {
|
|
966
|
+
"background only";
|
|
967
|
+
if (context.open && widthConstraint != null) void measureIntrinsicSize();
|
|
968
|
+
}, [context.open, measureIntrinsicSize, widthConstraint]);
|
|
969
|
+
React.useEffect(() => {
|
|
970
|
+
"background only";
|
|
971
|
+
const requestVersion = ++scrollRequestVersionRef.current;
|
|
972
|
+
const openEpoch = context.openEpoch;
|
|
973
|
+
const selected = context.selectedItem;
|
|
974
|
+
const scrollNode = scrollNodeRef.current;
|
|
975
|
+
if (
|
|
976
|
+
!context.open ||
|
|
977
|
+
!context.positioned ||
|
|
978
|
+
!position ||
|
|
979
|
+
!intrinsicSize ||
|
|
980
|
+
intrinsicSize.epoch !== openEpoch ||
|
|
981
|
+
intrinsicSize.config !== measurementConfigRef.current ||
|
|
982
|
+
!selected?.node ||
|
|
983
|
+
!scrollNode ||
|
|
984
|
+
!scrollAreaId ||
|
|
985
|
+
scrolledEpochRef.current === openEpoch
|
|
986
|
+
) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (intrinsicSize.height <= position.height) {
|
|
990
|
+
scrolledEpochRef.current = openEpoch;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const entry = selected.node;
|
|
994
|
+
void getRectByRef({ current: entry }, false, scrollAreaId)
|
|
995
|
+
.then((item) => {
|
|
996
|
+
if (
|
|
997
|
+
requestVersion !== scrollRequestVersionRef.current ||
|
|
998
|
+
!context.isOpenRef.current ||
|
|
999
|
+
openEpoch !== context.openEpochRef.current ||
|
|
1000
|
+
scrolledEpochRef.current === openEpoch
|
|
1001
|
+
) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const offset =
|
|
1005
|
+
item.top < 0
|
|
1006
|
+
? item.top
|
|
1007
|
+
: item.bottom > position.height
|
|
1008
|
+
? item.bottom - position.height
|
|
1009
|
+
: 0;
|
|
1010
|
+
if (offset === 0) {
|
|
1011
|
+
scrolledEpochRef.current = openEpoch;
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
scrollNode.invoke({ method: "scrollBy", params: { offset } }).exec();
|
|
1016
|
+
scrolledEpochRef.current = openEpoch;
|
|
1017
|
+
} catch {
|
|
1018
|
+
// Test refs do not implement native UI methods.
|
|
1019
|
+
}
|
|
1020
|
+
})
|
|
1021
|
+
.catch(() => {
|
|
1022
|
+
// The entry can be replaced before its native bounds are available.
|
|
1023
|
+
});
|
|
1024
|
+
}, [
|
|
1025
|
+
context.isOpenRef,
|
|
1026
|
+
context.open,
|
|
1027
|
+
context.openEpoch,
|
|
1028
|
+
context.openEpochRef,
|
|
1029
|
+
context.positioned,
|
|
1030
|
+
context.selectedItem,
|
|
1031
|
+
intrinsicSize,
|
|
1032
|
+
position,
|
|
1033
|
+
scrollAreaId,
|
|
1034
|
+
scrollAttached,
|
|
1035
|
+
selectedNode,
|
|
1036
|
+
]);
|
|
1037
|
+
|
|
1038
|
+
const handleShowOverlay = React.useCallback(() => {
|
|
1039
|
+
"background only";
|
|
1040
|
+
void measureIntrinsicSize();
|
|
1041
|
+
void measurePosition();
|
|
1042
|
+
}, [measureIntrinsicSize, measurePosition]);
|
|
1043
|
+
const handleRef = React.useCallback(
|
|
1044
|
+
(node: NodesRef | null) => {
|
|
1045
|
+
mergeNodeRef(ref, node);
|
|
1046
|
+
},
|
|
1047
|
+
[ref],
|
|
1048
|
+
);
|
|
1049
|
+
const handlePositionerRef = React.useCallback((node: NodesRef | null) => {
|
|
1050
|
+
overlayNodeRef.current = node;
|
|
1051
|
+
if (node && !overlayAttachedRef.current) {
|
|
1052
|
+
overlayAttachedRef.current = true;
|
|
1053
|
+
setOverlayAttached(true);
|
|
1054
|
+
}
|
|
1055
|
+
}, []);
|
|
1056
|
+
const handleScrollRef = React.useCallback((node: NodesRef | null) => {
|
|
1057
|
+
"background only";
|
|
1058
|
+
scrollNodeRef.current = node;
|
|
1059
|
+
if (node && !scrollAttachedRef.current) {
|
|
1060
|
+
scrollAttachedRef.current = true;
|
|
1061
|
+
setScrollAttached(true);
|
|
1062
|
+
}
|
|
1063
|
+
}, []);
|
|
1064
|
+
const handleScrollContentRef = React.useCallback((node: NodesRef | null) => {
|
|
1065
|
+
"background only";
|
|
1066
|
+
scrollContentNodeRef.current = node;
|
|
1067
|
+
if (node && !scrollContentAttachedRef.current) {
|
|
1068
|
+
scrollContentAttachedRef.current = true;
|
|
1069
|
+
setScrollContentAttached(true);
|
|
1070
|
+
}
|
|
1071
|
+
}, []);
|
|
1072
|
+
const handleIntrinsicLayoutChange = React.useCallback<NativeLayoutHandler>(() => {
|
|
1073
|
+
"background only";
|
|
1074
|
+
void measureIntrinsicSize();
|
|
1075
|
+
}, [measureIntrinsicSize]);
|
|
1076
|
+
const handleTransitionEnd = React.useCallback<NativeTransitionHandler>(
|
|
1077
|
+
(event) => {
|
|
1078
|
+
"background only";
|
|
1079
|
+
if (!context.open && hasExitTransition(event)) context.finishClose();
|
|
1080
|
+
},
|
|
1081
|
+
[context],
|
|
1082
|
+
);
|
|
1083
|
+
const handleBackdropTap = React.useCallback<NativeTapHandler>(
|
|
1084
|
+
(event) => {
|
|
1085
|
+
"background only";
|
|
1086
|
+
context.requestOpen(false, { reason: "interactOutside", event });
|
|
1087
|
+
},
|
|
1088
|
+
[context],
|
|
1089
|
+
);
|
|
1090
|
+
const handleNativeDismiss = React.useCallback(
|
|
1091
|
+
(event: BaseEvent) => {
|
|
1092
|
+
"background only";
|
|
1093
|
+
context.requestOpen(false, { reason: "dismiss", event });
|
|
1094
|
+
context.finishClose(true);
|
|
1095
|
+
},
|
|
1096
|
+
[context],
|
|
1097
|
+
);
|
|
1098
|
+
const handleRequestClose = React.useCallback(
|
|
1099
|
+
(event: BaseEvent) => {
|
|
1100
|
+
"background only";
|
|
1101
|
+
context.requestOpen(false, { reason: "dismiss", event });
|
|
1102
|
+
},
|
|
1103
|
+
[context],
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
const geometryStyle =
|
|
1107
|
+
position && overlayRect
|
|
1108
|
+
? {
|
|
1109
|
+
left: toPixel(position.left - overlayRect.left),
|
|
1110
|
+
top: toPixel(position.top - overlayRect.top),
|
|
1111
|
+
width: toPixel(position.width),
|
|
1112
|
+
transformOrigin: position.transformOrigin,
|
|
1113
|
+
}
|
|
1114
|
+
: undefined;
|
|
1115
|
+
const scrollStyle = position ? { height: toPixel(position.height) } : undefined;
|
|
1116
|
+
const triggerProxyStyle =
|
|
1117
|
+
triggerRect && overlayRect
|
|
1118
|
+
? {
|
|
1119
|
+
left: toPixel(triggerRect.left - overlayRect.left),
|
|
1120
|
+
top: toPixel(triggerRect.top - overlayRect.top),
|
|
1121
|
+
width: toPixel(triggerRect.width),
|
|
1122
|
+
height: toPixel(triggerRect.height),
|
|
1123
|
+
}
|
|
1124
|
+
: undefined;
|
|
1125
|
+
const groups = toArray(children);
|
|
1126
|
+
|
|
1127
|
+
return (
|
|
1128
|
+
// Keep options mounted while closed so defaultValue can resolve label/icon metadata before open.
|
|
1129
|
+
<overlay
|
|
1130
|
+
visible={context.mounted}
|
|
1131
|
+
style={{ position: "fixed" }}
|
|
1132
|
+
binddismissoverlay={handleNativeDismiss}
|
|
1133
|
+
bindshowoverlay={handleShowOverlay}
|
|
1134
|
+
bindrequestclose={handleRequestClose}
|
|
1135
|
+
>
|
|
1136
|
+
<view ref={handlePositionerRef as LynxViewRef} className={context.classes.positioner}>
|
|
1137
|
+
<view className={context.classes.backdrop} bindtap={handleBackdropTap} />
|
|
1138
|
+
{triggerProxyStyle && (
|
|
1139
|
+
<view
|
|
1140
|
+
style={{ position: "absolute", ...triggerProxyStyle }}
|
|
1141
|
+
bindtap={context.triggerHandlers.bindtap}
|
|
1142
|
+
main-thread:bindtap={context.triggerHandlers["main-thread:bindtap"]}
|
|
1143
|
+
/>
|
|
1144
|
+
)}
|
|
1145
|
+
<view
|
|
1146
|
+
ref={handleRef as LynxViewRef}
|
|
1147
|
+
className={clsx(context.classes.content, className)}
|
|
1148
|
+
style={{
|
|
1149
|
+
...geometryStyle,
|
|
1150
|
+
...style,
|
|
1151
|
+
...(widthConstraint != null ? { width: toPixel(widthConstraint) } : {}),
|
|
1152
|
+
}}
|
|
1153
|
+
bindtransitionend={handleTransitionEnd}
|
|
1154
|
+
{...nativeProps}
|
|
1155
|
+
>
|
|
1156
|
+
<scroll-view
|
|
1157
|
+
ref={handleScrollRef as LynxViewRef}
|
|
1158
|
+
id={scrollAreaId}
|
|
1159
|
+
className={context.classes.scrollArea}
|
|
1160
|
+
scroll-orientation="vertical"
|
|
1161
|
+
enable-scroll={Boolean(
|
|
1162
|
+
position &&
|
|
1163
|
+
intrinsicSize &&
|
|
1164
|
+
intrinsicSize.epoch === context.openEpoch &&
|
|
1165
|
+
intrinsicSize.config === measurementConfigRef.current &&
|
|
1166
|
+
intrinsicSize.height > position.height,
|
|
1167
|
+
)}
|
|
1168
|
+
style={scrollStyle}
|
|
1169
|
+
>
|
|
1170
|
+
<view
|
|
1171
|
+
ref={handleScrollContentRef as LynxViewRef}
|
|
1172
|
+
className={context.classes.scrollContent}
|
|
1173
|
+
bindlayoutchange={handleIntrinsicLayoutChange}
|
|
1174
|
+
>
|
|
1175
|
+
{groups.map((group, index) => (
|
|
1176
|
+
<SelectGroupPositionContext.Provider
|
|
1177
|
+
key={React.isValidElement(group) ? (group.key ?? index) : index}
|
|
1178
|
+
value={{ isFirst: index === 0 }}
|
|
1179
|
+
>
|
|
1180
|
+
{group}
|
|
1181
|
+
</SelectGroupPositionContext.Provider>
|
|
1182
|
+
))}
|
|
1183
|
+
</view>
|
|
1184
|
+
</scroll-view>
|
|
1185
|
+
</view>
|
|
1186
|
+
</view>
|
|
1187
|
+
</overlay>
|
|
1188
|
+
);
|
|
1189
|
+
});
|
|
1190
|
+
SelectContent.displayName = "SelectContent";
|
|
1191
|
+
|
|
1192
|
+
/** The viewport is normally owned by SelectContent; use this only for custom content composition. */
|
|
1193
|
+
export interface SelectScrollAreaProps extends LynxStyledElementProps {}
|
|
1194
|
+
|
|
1195
|
+
export const SelectScrollArea = React.forwardRef<unknown, SelectScrollAreaProps>((props, ref) => {
|
|
1196
|
+
const { children, className, ...nativeProps } = props;
|
|
1197
|
+
const context = useSelectContext("SelectScrollArea");
|
|
1198
|
+
return (
|
|
1199
|
+
<scroll-view
|
|
1200
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
1201
|
+
className={clsx(context.classes.scrollArea, className)}
|
|
1202
|
+
scroll-orientation="vertical"
|
|
1203
|
+
{...nativeProps}
|
|
1204
|
+
>
|
|
1205
|
+
<view className={context.classes.scrollContent}>{children}</view>
|
|
1206
|
+
</scroll-view>
|
|
1207
|
+
);
|
|
1208
|
+
});
|
|
1209
|
+
SelectScrollArea.displayName = "SelectScrollArea";
|
|
1210
|
+
|
|
1211
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
1212
|
+
|
|
1213
|
+
export interface SelectGroupProps extends LynxStyledElementProps {}
|
|
1214
|
+
|
|
1215
|
+
export const SelectGroup = React.forwardRef<unknown, SelectGroupProps>((props, ref) => {
|
|
1216
|
+
const { children, className, ...nativeProps } = props;
|
|
1217
|
+
const context = useSelectContext("SelectGroup");
|
|
1218
|
+
const position = React.useContext(SelectGroupPositionContext);
|
|
1219
|
+
return (
|
|
1220
|
+
<view
|
|
1221
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
1222
|
+
className={clsx(context.classes.group, className)}
|
|
1223
|
+
{...nativeProps}
|
|
1224
|
+
>
|
|
1225
|
+
{!position.isFirst ? (
|
|
1226
|
+
<view className={context.classes.separator} accessibility-elements-hidden={true} />
|
|
1227
|
+
) : null}
|
|
1228
|
+
{children}
|
|
1229
|
+
</view>
|
|
1230
|
+
);
|
|
1231
|
+
});
|
|
1232
|
+
SelectGroup.displayName = "SelectGroup";
|
|
1233
|
+
|
|
1234
|
+
export interface SelectGroupLabelProps extends LynxStyledElementProps, LynxAccessibilityProps {}
|
|
1235
|
+
|
|
1236
|
+
export const SelectGroupLabel = React.forwardRef<unknown, SelectGroupLabelProps>((props, ref) => {
|
|
1237
|
+
const {
|
|
1238
|
+
children,
|
|
1239
|
+
className,
|
|
1240
|
+
"accessibility-heading": accessibilityHeading = true,
|
|
1241
|
+
...nativeProps
|
|
1242
|
+
} = props;
|
|
1243
|
+
const context = useSelectContext("SelectGroupLabel");
|
|
1244
|
+
return (
|
|
1245
|
+
<text
|
|
1246
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
1247
|
+
className={clsx(context.classes.groupLabel, className)}
|
|
1248
|
+
accessibility-heading={accessibilityHeading}
|
|
1249
|
+
{...nativeProps}
|
|
1250
|
+
>
|
|
1251
|
+
{children}
|
|
1252
|
+
</text>
|
|
1253
|
+
);
|
|
1254
|
+
});
|
|
1255
|
+
SelectGroupLabel.displayName = "SelectGroupLabel";
|
|
1256
|
+
|
|
1257
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
1258
|
+
|
|
1259
|
+
export interface SelectItemProps
|
|
1260
|
+
extends SelectItemPublicVariantProps,
|
|
1261
|
+
LynxStyledElementProps,
|
|
1262
|
+
LynxPressableProps,
|
|
1263
|
+
LynxAccessibilityProps {
|
|
1264
|
+
value: string;
|
|
1265
|
+
label?: React.ReactNode;
|
|
1266
|
+
textValue?: string;
|
|
1267
|
+
prefixIcon?: React.ReactNode;
|
|
1268
|
+
disabled?: boolean;
|
|
1269
|
+
readOnly?: boolean;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export const SelectItem = React.forwardRef<unknown, SelectItemProps>((props, ref) => {
|
|
1273
|
+
const [variantProps, otherProps] = selectItem.splitVariantProps(props);
|
|
1274
|
+
const { disabled: disabledProp = false } = variantProps;
|
|
1275
|
+
const {
|
|
1276
|
+
value,
|
|
1277
|
+
label,
|
|
1278
|
+
textValue,
|
|
1279
|
+
prefixIcon,
|
|
1280
|
+
readOnly = false,
|
|
1281
|
+
children,
|
|
1282
|
+
className,
|
|
1283
|
+
bindtap,
|
|
1284
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
1285
|
+
"accessibility-element": accessibilityElement = true,
|
|
1286
|
+
"accessibility-label": accessibilityLabel,
|
|
1287
|
+
"accessibility-traits": accessibilityTraits,
|
|
1288
|
+
"accessibility-value": accessibilityValue,
|
|
1289
|
+
...nativeProps
|
|
1290
|
+
} = otherProps;
|
|
1291
|
+
const context = useSelectContext("SelectItem");
|
|
1292
|
+
// A fresh snapshot ref is not a semantic option change. Register after its first attachment
|
|
1293
|
+
// without turning every native ref patch into a component state update.
|
|
1294
|
+
const nodeRef = React.useRef<NodesRef | null>(null);
|
|
1295
|
+
const nodeAttachedRef = React.useRef(false);
|
|
1296
|
+
const [nodeAttached, setNodeAttached] = React.useState(false);
|
|
1297
|
+
const disabled = context.disabled || context.readOnly || disabledProp || readOnly;
|
|
1298
|
+
const selected = context.value.includes(value);
|
|
1299
|
+
const resolvedTextValue = textValue ?? (typeof label === "string" ? label : value);
|
|
1300
|
+
const handleTap = React.useCallback<NativeTapHandler>(
|
|
1301
|
+
(event, instance) => {
|
|
1302
|
+
"background only";
|
|
1303
|
+
context.selectValue(value, event);
|
|
1304
|
+
bindtap?.(event, instance);
|
|
1305
|
+
},
|
|
1306
|
+
[bindtap, context, value],
|
|
1307
|
+
);
|
|
1308
|
+
const { pressed, ...pressHandlers } = usePressTap({
|
|
1309
|
+
disabled,
|
|
1310
|
+
onTap: handleTap,
|
|
1311
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
1312
|
+
});
|
|
1313
|
+
const handleRef = React.useCallback(
|
|
1314
|
+
(nextNode: NodesRef | null) => {
|
|
1315
|
+
nodeRef.current = nextNode;
|
|
1316
|
+
if (nextNode && !nodeAttachedRef.current) {
|
|
1317
|
+
nodeAttachedRef.current = true;
|
|
1318
|
+
setNodeAttached(true);
|
|
1319
|
+
}
|
|
1320
|
+
mergeNodeRef(ref, nextNode);
|
|
1321
|
+
},
|
|
1322
|
+
[ref],
|
|
1323
|
+
);
|
|
1324
|
+
React.useEffect(() => {
|
|
1325
|
+
"background only";
|
|
1326
|
+
context.registerOption(value, {
|
|
1327
|
+
label,
|
|
1328
|
+
textValue: resolvedTextValue,
|
|
1329
|
+
prefixIcon,
|
|
1330
|
+
node: nodeRef.current,
|
|
1331
|
+
});
|
|
1332
|
+
return () => {
|
|
1333
|
+
context.unregisterOption(value);
|
|
1334
|
+
};
|
|
1335
|
+
}, [
|
|
1336
|
+
context.registerOption,
|
|
1337
|
+
context.unregisterOption,
|
|
1338
|
+
label,
|
|
1339
|
+
nodeAttached,
|
|
1340
|
+
prefixIcon,
|
|
1341
|
+
resolvedTextValue,
|
|
1342
|
+
value,
|
|
1343
|
+
]);
|
|
1344
|
+
const classes = selectItem({
|
|
1345
|
+
...variantProps,
|
|
1346
|
+
size: context.size,
|
|
1347
|
+
disabled,
|
|
1348
|
+
selected,
|
|
1349
|
+
pressed,
|
|
1350
|
+
});
|
|
1351
|
+
const itemContextValue = React.useMemo<SelectItemContextValue>(
|
|
1352
|
+
() => ({ label, prefixIcon, selected, disabled, pressed, classes }),
|
|
1353
|
+
[classes, disabled, label, prefixIcon, pressed, selected],
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
return (
|
|
1357
|
+
<SelectItemContext.Provider value={itemContextValue}>
|
|
1358
|
+
<view
|
|
1359
|
+
ref={handleRef as LynxViewRef}
|
|
1360
|
+
className={clsx(classes.root, className)}
|
|
1361
|
+
accessibility-element={accessibilityElement}
|
|
1362
|
+
accessibility-label={accessibilityLabel ?? resolvedTextValue}
|
|
1363
|
+
accessibility-role-description="option"
|
|
1364
|
+
accessibility-value={accessibilityValue ?? (selected ? "selected" : "not selected")}
|
|
1365
|
+
accessibility-traits={disabled ? "disabled" : accessibilityTraits}
|
|
1366
|
+
{...nativeProps}
|
|
1367
|
+
{...pressHandlers}
|
|
1368
|
+
>
|
|
1369
|
+
<view className={classes.pressedOverlay} accessibility-elements-hidden={true} />
|
|
1370
|
+
{children}
|
|
1371
|
+
</view>
|
|
1372
|
+
</SelectItemContext.Provider>
|
|
1373
|
+
);
|
|
1374
|
+
});
|
|
1375
|
+
SelectItem.displayName = "SelectItem";
|
|
1376
|
+
|
|
1377
|
+
export interface SelectItemPrefixIconProps extends Omit<InternalIconProps, "icon" | "deps"> {
|
|
1378
|
+
icon?: React.ReactNode;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
export const SelectItemPrefixIcon = React.forwardRef<unknown, SelectItemPrefixIconProps>(
|
|
1382
|
+
(props, ref) => {
|
|
1383
|
+
const { icon: iconOverride, className, ...nativeProps } = props;
|
|
1384
|
+
const context = useSelectItemContext("SelectItemPrefixIcon");
|
|
1385
|
+
const icon = iconOverride ?? context.prefixIcon;
|
|
1386
|
+
if (!isValidElement<LynxIconElementProps>(icon)) return null;
|
|
1387
|
+
return (
|
|
1388
|
+
<InternalIcon
|
|
1389
|
+
ref={ref}
|
|
1390
|
+
icon={icon}
|
|
1391
|
+
className={clsx(context.classes.prefixIcon, className)}
|
|
1392
|
+
accessibility-elements-hidden={true}
|
|
1393
|
+
deps={[context.selected, context.disabled, context.pressed, icon]}
|
|
1394
|
+
{...nativeProps}
|
|
1395
|
+
/>
|
|
1396
|
+
);
|
|
1397
|
+
},
|
|
1398
|
+
);
|
|
1399
|
+
SelectItemPrefixIcon.displayName = "SelectItemPrefixIcon";
|
|
1400
|
+
|
|
1401
|
+
export interface SelectItemBodyProps extends LynxStyledElementProps {}
|
|
1402
|
+
|
|
1403
|
+
export const SelectItemBody = React.forwardRef<unknown, SelectItemBodyProps>((props, ref) => {
|
|
1404
|
+
const { children, className, ...nativeProps } = props;
|
|
1405
|
+
const context = useSelectItemContext("SelectItemBody");
|
|
1406
|
+
return (
|
|
1407
|
+
<view
|
|
1408
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
1409
|
+
className={clsx(context.classes.body, className)}
|
|
1410
|
+
{...nativeProps}
|
|
1411
|
+
>
|
|
1412
|
+
{children}
|
|
1413
|
+
</view>
|
|
1414
|
+
);
|
|
1415
|
+
});
|
|
1416
|
+
SelectItemBody.displayName = "SelectItemBody";
|
|
1417
|
+
|
|
1418
|
+
export interface SelectItemLabelProps extends LynxStyledElementProps {}
|
|
1419
|
+
|
|
1420
|
+
export const SelectItemLabel = React.forwardRef<unknown, SelectItemLabelProps>((props, ref) => {
|
|
1421
|
+
const { children, className, ...nativeProps } = props;
|
|
1422
|
+
const context = useSelectItemContext("SelectItemLabel");
|
|
1423
|
+
return renderTextContent(
|
|
1424
|
+
clsx(context.classes.label, className),
|
|
1425
|
+
children ?? context.label,
|
|
1426
|
+
ref,
|
|
1427
|
+
nativeProps,
|
|
1428
|
+
);
|
|
1429
|
+
});
|
|
1430
|
+
SelectItemLabel.displayName = "SelectItemLabel";
|
|
1431
|
+
|
|
1432
|
+
export interface SelectItemDescriptionProps extends LynxStyledElementProps {}
|
|
1433
|
+
|
|
1434
|
+
export const SelectItemDescription = React.forwardRef<unknown, SelectItemDescriptionProps>(
|
|
1435
|
+
(props, ref) => {
|
|
1436
|
+
const { children, className, ...nativeProps } = props;
|
|
1437
|
+
const context = useSelectItemContext("SelectItemDescription");
|
|
1438
|
+
return renderTextContent(
|
|
1439
|
+
clsx(context.classes.description, className),
|
|
1440
|
+
children,
|
|
1441
|
+
ref,
|
|
1442
|
+
nativeProps,
|
|
1443
|
+
);
|
|
1444
|
+
},
|
|
1445
|
+
);
|
|
1446
|
+
SelectItemDescription.displayName = "SelectItemDescription";
|
|
1447
|
+
|
|
1448
|
+
export interface SelectItemIndicatorProps extends Omit<InternalIconProps, "icon" | "deps"> {
|
|
1449
|
+
selected?: React.ReactNode;
|
|
1450
|
+
unselected?: React.ReactNode;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export const SelectItemIndicator = React.forwardRef<unknown, SelectItemIndicatorProps>(
|
|
1454
|
+
(props, ref) => {
|
|
1455
|
+
const { selected: selectedIcon, unselected, className, ...nativeProps } = props;
|
|
1456
|
+
const context = useSelectItemContext("SelectItemIndicator");
|
|
1457
|
+
const icon = context.selected ? selectedIcon : unselected;
|
|
1458
|
+
if (!isValidElement<LynxIconElementProps>(icon)) {
|
|
1459
|
+
if (!context.selected) return null;
|
|
1460
|
+
return (
|
|
1461
|
+
<text
|
|
1462
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
1463
|
+
className={clsx(context.classes.indicator, className)}
|
|
1464
|
+
accessibility-elements-hidden={true}
|
|
1465
|
+
{...nativeProps}
|
|
1466
|
+
>
|
|
1467
|
+
✓
|
|
1468
|
+
</text>
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return (
|
|
1472
|
+
<InternalIcon
|
|
1473
|
+
ref={ref}
|
|
1474
|
+
icon={icon}
|
|
1475
|
+
className={clsx(context.classes.indicator, className)}
|
|
1476
|
+
accessibility-elements-hidden={true}
|
|
1477
|
+
deps={[context.selected, context.disabled, context.pressed, icon]}
|
|
1478
|
+
{...nativeProps}
|
|
1479
|
+
/>
|
|
1480
|
+
);
|
|
1481
|
+
},
|
|
1482
|
+
);
|
|
1483
|
+
SelectItemIndicator.displayName = "SelectItemIndicator";
|