@lattice-ui/combobox 0.5.0-next.1 → 0.5.0-next.3
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/out/Combobox/ComboboxContent.luau +67 -26
- package/out/Combobox/ComboboxInput.luau +9 -1
- package/out/Combobox/ComboboxItem.luau +2 -6
- package/out/Combobox/ComboboxRoot.luau +24 -3
- package/out/Combobox/ComboboxTrigger.luau +5 -1
- package/out/Combobox/types.d.ts +4 -2
- package/package.json +7 -7
- package/src/Combobox/ComboboxContent.tsx +74 -31
- package/src/Combobox/ComboboxInput.tsx +7 -1
- package/src/Combobox/ComboboxItem.tsx +2 -2
- package/src/Combobox/ComboboxRoot.tsx +22 -2
- package/src/Combobox/ComboboxTrigger.tsx +1 -1
- package/src/Combobox/types.ts +4 -2
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
2
|
local TS = _G[script]
|
|
3
3
|
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
|
+
local composeRefs = _core.composeRefs
|
|
5
|
+
local getElementRef = _core.getElementRef
|
|
4
6
|
local React = _core.React
|
|
5
|
-
local Slot = _core.Slot
|
|
6
7
|
local _layer = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out)
|
|
7
8
|
local DismissableLayer = _layer.DismissableLayer
|
|
8
9
|
local Presence = _layer.Presence
|
|
9
10
|
local _motion = TS.import(script, TS.getModule(script, "@lattice-ui", "motion").out)
|
|
10
|
-
local
|
|
11
|
-
local
|
|
11
|
+
local createCanvasGroupPopperEntranceRecipe = _motion.createCanvasGroupPopperEntranceRecipe
|
|
12
|
+
local usePresenceMotionController = _motion.usePresenceMotionController
|
|
12
13
|
local usePopper = TS.import(script, TS.getModule(script, "@lattice-ui", "popper").out).usePopper
|
|
13
14
|
local useComboboxContext = TS.import(script, script.Parent, "context").useComboboxContext
|
|
14
15
|
local CONTENT_OFFSET = 6
|
|
16
|
+
local HIDDEN_POSITION = UDim2.fromOffset(-9999, -9999)
|
|
17
|
+
local function toGuiPropBag(value)
|
|
18
|
+
local _value = value
|
|
19
|
+
return if type(_value) == "table" then value else {}
|
|
20
|
+
end
|
|
15
21
|
local function toGuiObject(instance)
|
|
16
|
-
|
|
22
|
+
local _result = instance
|
|
23
|
+
if _result ~= nil then
|
|
24
|
+
_result = _result:IsA("GuiObject")
|
|
25
|
+
end
|
|
26
|
+
if not _result then
|
|
17
27
|
return nil
|
|
18
28
|
end
|
|
19
29
|
return instance
|
|
@@ -21,43 +31,69 @@ end
|
|
|
21
31
|
local function ComboboxContentImpl(props)
|
|
22
32
|
local comboboxContext = useComboboxContext()
|
|
23
33
|
local open = comboboxContext.open
|
|
34
|
+
local shouldMeasure = open or props.motionPresent or props.onExitComplete ~= nil
|
|
35
|
+
local contentBoundaryRef = React.useRef()
|
|
24
36
|
local popper = usePopper({
|
|
25
37
|
anchorRef = comboboxContext.anchorRef,
|
|
26
38
|
contentRef = comboboxContext.contentRef,
|
|
39
|
+
alignOffset = props.alignOffset,
|
|
40
|
+
collisionPadding = props.collisionPadding,
|
|
41
|
+
sideOffset = props.sideOffset,
|
|
27
42
|
placement = props.placement,
|
|
28
|
-
|
|
29
|
-
padding = props.padding,
|
|
30
|
-
enabled = open,
|
|
43
|
+
enabled = shouldMeasure,
|
|
31
44
|
})
|
|
32
45
|
local defaultTransition = React.useMemo(function()
|
|
33
|
-
return
|
|
46
|
+
return createCanvasGroupPopperEntranceRecipe(popper.placement, CONTENT_OFFSET)
|
|
34
47
|
end, { popper.placement })
|
|
35
|
-
local
|
|
48
|
+
local recipe = props.transition or defaultTransition
|
|
49
|
+
local motion = usePresenceMotionController({
|
|
50
|
+
present = props.motionPresent,
|
|
51
|
+
ready = popper.isPositioned,
|
|
52
|
+
forceMount = props.forceMount,
|
|
53
|
+
config = recipe,
|
|
54
|
+
onExitComplete = props.onExitComplete,
|
|
55
|
+
})
|
|
36
56
|
local setContentRef = React.useCallback(function(instance)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
local guiObject = toGuiObject(instance)
|
|
58
|
+
comboboxContext.contentRef.current = guiObject
|
|
59
|
+
contentBoundaryRef.current = guiObject
|
|
60
|
+
motion.ref.current = guiObject
|
|
61
|
+
end, { comboboxContext.contentRef, motion.ref })
|
|
40
62
|
local handleDismiss = React.useCallback(function()
|
|
41
63
|
comboboxContext.setOpen(false)
|
|
42
64
|
end, { comboboxContext })
|
|
43
|
-
local
|
|
65
|
+
local shouldRender = motion.mounted
|
|
66
|
+
local contentVisible = shouldRender and (motion.present or motion.phase ~= "exited")
|
|
67
|
+
local popperPosition = if popper.isPositioned then popper.position else HIDDEN_POSITION
|
|
68
|
+
local popperContentSize = popper.contentSize or Vector2.new(0, 0)
|
|
69
|
+
local popperWrapperSize = if popper.isPositioned then UDim2.fromOffset(popperContentSize.X, popperContentSize.Y) else UDim2.fromOffset(0, 0)
|
|
44
70
|
local contentNode = if props.asChild then ((function()
|
|
45
71
|
local child = props.children
|
|
46
72
|
if not React.isValidElement(child) then
|
|
47
73
|
error("[ComboboxContent] `asChild` requires a child element.")
|
|
48
74
|
end
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
local childProps = toGuiPropBag(child.props)
|
|
76
|
+
local childRef = getElementRef(child)
|
|
77
|
+
local _exp = child
|
|
78
|
+
local _object = table.clone(childProps)
|
|
79
|
+
setmetatable(_object, nil)
|
|
80
|
+
_object.Position = UDim2.fromOffset(0, 0)
|
|
81
|
+
_object.Visible = contentVisible
|
|
82
|
+
_object.ref = composeRefs(childRef)
|
|
83
|
+
return React.createElement("canvasgroup", {
|
|
84
|
+
AutomaticSize = Enum.AutomaticSize.XY,
|
|
85
|
+
BackgroundTransparency = 1,
|
|
86
|
+
BorderSizePixel = 0,
|
|
87
|
+
Size = UDim2.fromOffset(0, 0),
|
|
88
|
+
Visible = contentVisible,
|
|
52
89
|
ref = setContentRef,
|
|
53
|
-
},
|
|
54
|
-
end)()) else (React.createElement("
|
|
55
|
-
AnchorPoint = popper.anchorPoint,
|
|
90
|
+
}, React.cloneElement(_exp, _object))
|
|
91
|
+
end)()) else (React.createElement("canvasgroup", {
|
|
56
92
|
AutomaticSize = Enum.AutomaticSize.XY,
|
|
57
93
|
BackgroundTransparency = 1,
|
|
58
94
|
BorderSizePixel = 0,
|
|
59
95
|
Size = UDim2.fromOffset(0, 0),
|
|
60
|
-
Visible =
|
|
96
|
+
Visible = contentVisible,
|
|
61
97
|
ref = setContentRef,
|
|
62
98
|
}, props.children))
|
|
63
99
|
return React.createElement(DismissableLayer, {
|
|
@@ -67,11 +103,14 @@ local function ComboboxContentImpl(props)
|
|
|
67
103
|
onDismiss = handleDismiss,
|
|
68
104
|
onInteractOutside = props.onInteractOutside,
|
|
69
105
|
onPointerDownOutside = props.onPointerDownOutside,
|
|
106
|
+
contentBoundaryRef = contentBoundaryRef,
|
|
70
107
|
}, React.createElement("frame", {
|
|
108
|
+
AnchorPoint = popper.anchorPoint,
|
|
71
109
|
BackgroundTransparency = 1,
|
|
72
110
|
BorderSizePixel = 0,
|
|
73
|
-
Position =
|
|
74
|
-
Size =
|
|
111
|
+
Position = popperPosition,
|
|
112
|
+
Size = popperWrapperSize,
|
|
113
|
+
Visible = shouldRender,
|
|
75
114
|
}, contentNode))
|
|
76
115
|
end
|
|
77
116
|
local function ComboboxContent(props)
|
|
@@ -80,13 +119,14 @@ local function ComboboxContent(props)
|
|
|
80
119
|
if props.forceMount then
|
|
81
120
|
return React.createElement(ComboboxContentImpl, {
|
|
82
121
|
asChild = props.asChild,
|
|
122
|
+
alignOffset = props.alignOffset,
|
|
123
|
+
collisionPadding = props.collisionPadding,
|
|
83
124
|
forceMount = props.forceMount,
|
|
84
125
|
motionPresent = open,
|
|
85
|
-
offset = props.offset,
|
|
86
126
|
onInteractOutside = props.onInteractOutside,
|
|
87
127
|
onPointerDownOutside = props.onPointerDownOutside,
|
|
88
|
-
padding = props.padding,
|
|
89
128
|
placement = props.placement,
|
|
129
|
+
sideOffset = props.sideOffset,
|
|
90
130
|
transition = props.transition,
|
|
91
131
|
}, props.children)
|
|
92
132
|
end
|
|
@@ -95,14 +135,15 @@ local function ComboboxContent(props)
|
|
|
95
135
|
render = function(state)
|
|
96
136
|
return React.createElement(ComboboxContentImpl, {
|
|
97
137
|
asChild = props.asChild,
|
|
138
|
+
alignOffset = props.alignOffset,
|
|
139
|
+
collisionPadding = props.collisionPadding,
|
|
98
140
|
forceMount = props.forceMount,
|
|
99
141
|
motionPresent = state.isPresent,
|
|
100
|
-
offset = props.offset,
|
|
101
142
|
onExitComplete = state.onExitComplete,
|
|
102
143
|
onInteractOutside = props.onInteractOutside,
|
|
103
144
|
onPointerDownOutside = props.onPointerDownOutside,
|
|
104
|
-
padding = props.padding,
|
|
105
145
|
placement = props.placement,
|
|
146
|
+
sideOffset = props.sideOffset,
|
|
106
147
|
transition = props.transition,
|
|
107
148
|
}, props.children)
|
|
108
149
|
end,
|
|
@@ -4,8 +4,13 @@ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
|
4
4
|
local React = _core.React
|
|
5
5
|
local Slot = _core.Slot
|
|
6
6
|
local useComboboxContext = TS.import(script, script.Parent, "context").useComboboxContext
|
|
7
|
+
local UserInputService = game:GetService("UserInputService")
|
|
7
8
|
local function toTextBox(instance)
|
|
8
|
-
|
|
9
|
+
local _result = instance
|
|
10
|
+
if _result ~= nil then
|
|
11
|
+
_result = _result:IsA("TextBox")
|
|
12
|
+
end
|
|
13
|
+
if not _result then
|
|
9
14
|
return nil
|
|
10
15
|
end
|
|
11
16
|
return instance
|
|
@@ -32,6 +37,9 @@ local function ComboboxInput(props)
|
|
|
32
37
|
if textBox.Text == lastInputValueRef.current then
|
|
33
38
|
return nil
|
|
34
39
|
end
|
|
40
|
+
if UserInputService:GetFocusedTextBox() ~= textBox then
|
|
41
|
+
return nil
|
|
42
|
+
end
|
|
35
43
|
if disabled or readOnly then
|
|
36
44
|
if textBox.Text ~= lastInputValueRef.current then
|
|
37
45
|
textBox.Text = lastInputValueRef.current
|
|
@@ -12,14 +12,10 @@ local function ComboboxItem(props)
|
|
|
12
12
|
if _condition == nil then
|
|
13
13
|
_condition = props.value
|
|
14
14
|
end
|
|
15
|
-
local
|
|
15
|
+
local textValue = _condition
|
|
16
|
+
local itemQueryMatch = comboboxContext.filterFn(textValue, comboboxContext.queryValue)
|
|
16
17
|
local disabled = comboboxContext.disabled or props.disabled == true
|
|
17
18
|
local interactionDisabled = disabled or not itemQueryMatch
|
|
18
|
-
local _condition_1 = props.textValue
|
|
19
|
-
if _condition_1 == nil then
|
|
20
|
-
_condition_1 = props.value
|
|
21
|
-
end
|
|
22
|
-
local textValue = _condition_1
|
|
23
19
|
local disabledRef = React.useRef(disabled)
|
|
24
20
|
local textValueRef = React.useRef(textValue)
|
|
25
21
|
React.useEffect(function()
|
|
@@ -74,6 +74,7 @@ local function ComboboxRoot(props)
|
|
|
74
74
|
local _binding_2 = useControllableState(_object_1)
|
|
75
75
|
local inputValue = _binding_2[1]
|
|
76
76
|
local setInputValueState = _binding_2[2]
|
|
77
|
+
local visibleQueryValue, setVisibleQueryValue = React.useState(inputValue)
|
|
77
78
|
local disabled = props.disabled == true
|
|
78
79
|
local readOnly = props.readOnly == true
|
|
79
80
|
local required = props.required == true
|
|
@@ -84,6 +85,7 @@ local function ComboboxRoot(props)
|
|
|
84
85
|
local contentRef = React.useRef()
|
|
85
86
|
local itemEntriesRef = React.useRef({})
|
|
86
87
|
local itemTextCacheRef = React.useRef({})
|
|
88
|
+
local programmaticInputValueRef = React.useRef()
|
|
87
89
|
local registryRevision, setRegistryRevision = React.useState(0)
|
|
88
90
|
local registerItem = React.useCallback(function(item)
|
|
89
91
|
local _exp = itemEntriesRef.current
|
|
@@ -169,6 +171,7 @@ local function ComboboxRoot(props)
|
|
|
169
171
|
_result = ""
|
|
170
172
|
end
|
|
171
173
|
local nextInputValue = _result
|
|
174
|
+
programmaticInputValueRef.current = nextInputValue
|
|
172
175
|
setInputValueState(nextInputValue)
|
|
173
176
|
end, { getItemText, setInputValueState, value })
|
|
174
177
|
local setOpen = React.useCallback(function(nextOpen)
|
|
@@ -195,7 +198,11 @@ local function ComboboxRoot(props)
|
|
|
195
198
|
end
|
|
196
199
|
-- ▲ ReadonlyArray.find ▲
|
|
197
200
|
local selected = _result
|
|
198
|
-
|
|
201
|
+
local _result_1 = selected
|
|
202
|
+
if _result_1 ~= nil then
|
|
203
|
+
_result_1 = _result_1.getDisabled()
|
|
204
|
+
end
|
|
205
|
+
if _result_1 then
|
|
199
206
|
return nil
|
|
200
207
|
end
|
|
201
208
|
setValueState(nextValue)
|
|
@@ -204,12 +211,21 @@ local function ComboboxRoot(props)
|
|
|
204
211
|
_condition_2 = nextValue
|
|
205
212
|
end
|
|
206
213
|
local nextInputValue = _condition_2
|
|
214
|
+
programmaticInputValueRef.current = nextInputValue
|
|
207
215
|
setInputValueState(nextInputValue)
|
|
208
216
|
end, { disabled, getItemText, resolveOrderedItems, setInputValueState, setValueState })
|
|
209
217
|
local setInputValue = React.useCallback(function(nextInputValue)
|
|
210
218
|
if disabled or readOnly then
|
|
211
219
|
return nil
|
|
212
220
|
end
|
|
221
|
+
if programmaticInputValueRef.current ~= nil and nextInputValue == programmaticInputValueRef.current then
|
|
222
|
+
programmaticInputValueRef.current = nil
|
|
223
|
+
return nil
|
|
224
|
+
end
|
|
225
|
+
programmaticInputValueRef.current = nil
|
|
226
|
+
setVisibleQueryValue(function(currentQueryValue)
|
|
227
|
+
return if currentQueryValue == nextInputValue then currentQueryValue else nextInputValue
|
|
228
|
+
end)
|
|
213
229
|
if nextInputValue == inputValue then
|
|
214
230
|
return nil
|
|
215
231
|
end
|
|
@@ -234,7 +250,11 @@ local function ComboboxRoot(props)
|
|
|
234
250
|
return nil
|
|
235
251
|
end
|
|
236
252
|
syncInputFromValue()
|
|
237
|
-
|
|
253
|
+
setVisibleQueryValue(function(currentQueryValue)
|
|
254
|
+
return if currentQueryValue == inputValue then currentQueryValue else inputValue
|
|
255
|
+
end)
|
|
256
|
+
end, { inputValue, open, syncInputFromValue, value })
|
|
257
|
+
local queryValue = if open then visibleQueryValue else inputValue
|
|
238
258
|
local contextValue = React.useMemo(function()
|
|
239
259
|
return {
|
|
240
260
|
open = open,
|
|
@@ -242,6 +262,7 @@ local function ComboboxRoot(props)
|
|
|
242
262
|
value = value,
|
|
243
263
|
setValue = setValue,
|
|
244
264
|
inputValue = inputValue,
|
|
265
|
+
queryValue = queryValue,
|
|
245
266
|
setInputValue = setInputValue,
|
|
246
267
|
syncInputFromValue = syncInputFromValue,
|
|
247
268
|
disabled = disabled,
|
|
@@ -255,7 +276,7 @@ local function ComboboxRoot(props)
|
|
|
255
276
|
registerItem = registerItem,
|
|
256
277
|
getItemText = getItemText,
|
|
257
278
|
}
|
|
258
|
-
end, { disabled, filterFn, getItemText, inputValue, open, readOnly, registerItem, required, resolveOrderedItems, setInputValue, setOpen, setValue, syncInputFromValue, value })
|
|
279
|
+
end, { disabled, filterFn, getItemText, inputValue, open, queryValue, readOnly, registerItem, required, resolveOrderedItems, setInputValue, setOpen, setValue, syncInputFromValue, value, visibleQueryValue })
|
|
259
280
|
return React.createElement(ComboboxContextProvider, {
|
|
260
281
|
value = contextValue,
|
|
261
282
|
}, props.children)
|
|
@@ -5,7 +5,11 @@ local React = _core.React
|
|
|
5
5
|
local Slot = _core.Slot
|
|
6
6
|
local useComboboxContext = TS.import(script, script.Parent, "context").useComboboxContext
|
|
7
7
|
local function toGuiObject(instance)
|
|
8
|
-
|
|
8
|
+
local _result = instance
|
|
9
|
+
if _result ~= nil then
|
|
10
|
+
_result = _result:IsA("GuiObject")
|
|
11
|
+
end
|
|
12
|
+
if not _result then
|
|
9
13
|
return nil
|
|
10
14
|
end
|
|
11
15
|
return instance
|
package/out/Combobox/types.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type ComboboxContextValue = {
|
|
|
19
19
|
value?: string;
|
|
20
20
|
setValue: ComboboxSetValue;
|
|
21
21
|
inputValue: string;
|
|
22
|
+
queryValue: string;
|
|
22
23
|
setInputValue: ComboboxSetInputValue;
|
|
23
24
|
syncInputFromValue: () => void;
|
|
24
25
|
disabled: boolean;
|
|
@@ -75,8 +76,9 @@ export type ComboboxContentProps = {
|
|
|
75
76
|
asChild?: boolean;
|
|
76
77
|
forceMount?: boolean;
|
|
77
78
|
placement?: PopperPlacement;
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
sideOffset?: number;
|
|
80
|
+
alignOffset?: number;
|
|
81
|
+
collisionPadding?: number;
|
|
80
82
|
onPointerDownOutside?: (event: LayerInteractEvent) => void;
|
|
81
83
|
onInteractOutside?: (event: LayerInteractEvent) => void;
|
|
82
84
|
children?: React.ReactNode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lattice-ui/combobox",
|
|
3
|
-
"version": "0.5.0-next.
|
|
3
|
+
"version": "0.5.0-next.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "out/init.luau",
|
|
6
6
|
"types": "out/index.d.ts",
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
"url": "https://github.com/astra-void/lattice-ui.git"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@lattice-ui/core": "0.5.0-next.
|
|
20
|
-
"@lattice-ui/layer": "0.5.0-next.
|
|
21
|
-
"@lattice-ui/
|
|
22
|
-
"@lattice-ui/
|
|
19
|
+
"@lattice-ui/core": "0.5.0-next.3",
|
|
20
|
+
"@lattice-ui/layer": "0.5.0-next.3",
|
|
21
|
+
"@lattice-ui/popper": "0.5.0-next.3",
|
|
22
|
+
"@lattice-ui/motion": "0.5.0-next.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@rbxts/react": "17.3.7-ts.1",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "rbxtsc -p tsconfig.json",
|
|
34
|
-
"lint": "
|
|
35
|
-
"lint:fix": "
|
|
34
|
+
"lint": "biome check src",
|
|
35
|
+
"lint:fix": "biome check src --write --unsafe",
|
|
36
36
|
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
37
37
|
"watch": "rbxtsc -p tsconfig.json -w"
|
|
38
38
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { composeRefs, getElementRef, React } from "@lattice-ui/core";
|
|
2
2
|
import type { LayerInteractEvent } from "@lattice-ui/layer";
|
|
3
3
|
import { DismissableLayer, Presence } from "@lattice-ui/layer";
|
|
4
|
-
import {
|
|
4
|
+
import { createCanvasGroupPopperEntranceRecipe, usePresenceMotionController } from "@lattice-ui/motion";
|
|
5
5
|
import type { PopperPlacement } from "@lattice-ui/popper";
|
|
6
6
|
import { usePopper } from "@lattice-ui/popper";
|
|
7
7
|
import { useComboboxContext } from "./context";
|
|
8
8
|
import type { ComboboxContentProps } from "./types";
|
|
9
9
|
|
|
10
10
|
const CONTENT_OFFSET = 6;
|
|
11
|
+
const HIDDEN_POSITION = UDim2.fromOffset(-9999, -9999);
|
|
12
|
+
|
|
13
|
+
type GuiPropBag = React.Attributes & Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
function toGuiPropBag(value: unknown): GuiPropBag {
|
|
16
|
+
return typeIs(value, "table") ? (value as GuiPropBag) : {};
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
function toGuiObject(instance: Instance | undefined) {
|
|
13
|
-
if (!instance
|
|
20
|
+
if (!instance?.IsA("GuiObject")) {
|
|
14
21
|
return undefined;
|
|
15
22
|
}
|
|
16
23
|
return instance;
|
|
@@ -20,8 +27,9 @@ function ComboboxContentImpl(props: {
|
|
|
20
27
|
motionPresent: boolean;
|
|
21
28
|
onExitComplete?: () => void;
|
|
22
29
|
placement?: PopperPlacement;
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
sideOffset?: number;
|
|
31
|
+
alignOffset?: number;
|
|
32
|
+
collisionPadding?: number;
|
|
25
33
|
forceMount?: boolean;
|
|
26
34
|
onPointerDownOutside?: (event: LayerInteractEvent) => void;
|
|
27
35
|
onInteractOutside?: (event: LayerInteractEvent) => void;
|
|
@@ -31,38 +39,54 @@ function ComboboxContentImpl(props: {
|
|
|
31
39
|
}) {
|
|
32
40
|
const comboboxContext = useComboboxContext();
|
|
33
41
|
const open = comboboxContext.open;
|
|
42
|
+
const shouldMeasure = open || props.motionPresent || props.onExitComplete !== undefined;
|
|
43
|
+
const contentBoundaryRef = React.useRef<GuiObject>();
|
|
34
44
|
|
|
35
45
|
const popper = usePopper({
|
|
36
46
|
anchorRef: comboboxContext.anchorRef,
|
|
37
47
|
contentRef: comboboxContext.contentRef,
|
|
48
|
+
alignOffset: props.alignOffset,
|
|
49
|
+
collisionPadding: props.collisionPadding,
|
|
50
|
+
sideOffset: props.sideOffset,
|
|
38
51
|
placement: props.placement,
|
|
39
|
-
|
|
40
|
-
padding: props.padding,
|
|
41
|
-
enabled: open,
|
|
52
|
+
enabled: shouldMeasure,
|
|
42
53
|
});
|
|
43
54
|
|
|
44
55
|
const defaultTransition = React.useMemo(
|
|
45
|
-
() =>
|
|
56
|
+
() => createCanvasGroupPopperEntranceRecipe(popper.placement, CONTENT_OFFSET),
|
|
46
57
|
[popper.placement],
|
|
47
58
|
);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
props.
|
|
52
|
-
|
|
59
|
+
const recipe = props.transition ?? defaultTransition;
|
|
60
|
+
|
|
61
|
+
const motion = usePresenceMotionController<GuiObject>({
|
|
62
|
+
present: props.motionPresent,
|
|
63
|
+
ready: popper.isPositioned,
|
|
64
|
+
forceMount: props.forceMount,
|
|
65
|
+
config: recipe,
|
|
66
|
+
onExitComplete: props.onExitComplete,
|
|
67
|
+
});
|
|
53
68
|
|
|
54
69
|
const setContentRef = React.useCallback(
|
|
55
70
|
(instance: Instance | undefined) => {
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
const guiObject = toGuiObject(instance);
|
|
72
|
+
comboboxContext.contentRef.current = guiObject;
|
|
73
|
+
contentBoundaryRef.current = guiObject;
|
|
74
|
+
motion.ref.current = guiObject;
|
|
58
75
|
},
|
|
59
|
-
[comboboxContext.contentRef,
|
|
76
|
+
[comboboxContext.contentRef, motion.ref],
|
|
60
77
|
);
|
|
61
78
|
|
|
62
79
|
const handleDismiss = React.useCallback(() => {
|
|
63
80
|
comboboxContext.setOpen(false);
|
|
64
81
|
}, [comboboxContext]);
|
|
65
|
-
|
|
82
|
+
|
|
83
|
+
const shouldRender = motion.mounted;
|
|
84
|
+
const contentVisible = shouldRender && (motion.present || motion.phase !== "exited");
|
|
85
|
+
const popperPosition = popper.isPositioned ? popper.position : HIDDEN_POSITION;
|
|
86
|
+
const popperContentSize = (popper as { contentSize?: Vector2 }).contentSize ?? new Vector2(0, 0);
|
|
87
|
+
const popperWrapperSize = popper.isPositioned
|
|
88
|
+
? UDim2.fromOffset(popperContentSize.X, popperContentSize.Y)
|
|
89
|
+
: UDim2.fromOffset(0, 0);
|
|
66
90
|
|
|
67
91
|
const contentNode = props.asChild ? (
|
|
68
92
|
(() => {
|
|
@@ -71,24 +95,38 @@ function ComboboxContentImpl(props: {
|
|
|
71
95
|
error("[ComboboxContent] `asChild` requires a child element.");
|
|
72
96
|
}
|
|
73
97
|
|
|
98
|
+
const childProps = toGuiPropBag((child as { props?: unknown }).props);
|
|
99
|
+
const childRef = getElementRef<Instance>(child);
|
|
100
|
+
|
|
74
101
|
return (
|
|
75
|
-
<
|
|
76
|
-
{
|
|
77
|
-
|
|
102
|
+
<canvasgroup
|
|
103
|
+
AutomaticSize={Enum.AutomaticSize.XY}
|
|
104
|
+
BackgroundTransparency={1}
|
|
105
|
+
BorderSizePixel={0}
|
|
106
|
+
Size={UDim2.fromOffset(0, 0)}
|
|
107
|
+
Visible={contentVisible}
|
|
108
|
+
ref={setContentRef as React.Ref<CanvasGroup>}
|
|
109
|
+
>
|
|
110
|
+
{React.cloneElement(child as React.ReactElement<GuiPropBag>, {
|
|
111
|
+
...childProps,
|
|
112
|
+
Position: UDim2.fromOffset(0, 0),
|
|
113
|
+
Visible: contentVisible,
|
|
114
|
+
ref: composeRefs(childRef),
|
|
115
|
+
})}
|
|
116
|
+
</canvasgroup>
|
|
78
117
|
);
|
|
79
118
|
})()
|
|
80
119
|
) : (
|
|
81
|
-
<
|
|
82
|
-
AnchorPoint={popper.anchorPoint}
|
|
120
|
+
<canvasgroup
|
|
83
121
|
AutomaticSize={Enum.AutomaticSize.XY}
|
|
84
122
|
BackgroundTransparency={1}
|
|
85
123
|
BorderSizePixel={0}
|
|
86
124
|
Size={UDim2.fromOffset(0, 0)}
|
|
87
|
-
Visible={
|
|
125
|
+
Visible={contentVisible}
|
|
88
126
|
ref={setContentRef}
|
|
89
127
|
>
|
|
90
128
|
{props.children}
|
|
91
|
-
</
|
|
129
|
+
</canvasgroup>
|
|
92
130
|
);
|
|
93
131
|
|
|
94
132
|
return (
|
|
@@ -99,12 +137,15 @@ function ComboboxContentImpl(props: {
|
|
|
99
137
|
onDismiss={handleDismiss}
|
|
100
138
|
onInteractOutside={props.onInteractOutside}
|
|
101
139
|
onPointerDownOutside={props.onPointerDownOutside}
|
|
140
|
+
contentBoundaryRef={contentBoundaryRef}
|
|
102
141
|
>
|
|
103
142
|
<frame
|
|
143
|
+
AnchorPoint={popper.anchorPoint}
|
|
104
144
|
BackgroundTransparency={1}
|
|
105
145
|
BorderSizePixel={0}
|
|
106
|
-
Position={
|
|
107
|
-
Size={
|
|
146
|
+
Position={popperPosition}
|
|
147
|
+
Size={popperWrapperSize}
|
|
148
|
+
Visible={shouldRender}
|
|
108
149
|
>
|
|
109
150
|
{contentNode}
|
|
110
151
|
</frame>
|
|
@@ -120,13 +161,14 @@ export function ComboboxContent(props: ComboboxContentProps) {
|
|
|
120
161
|
return (
|
|
121
162
|
<ComboboxContentImpl
|
|
122
163
|
asChild={props.asChild}
|
|
164
|
+
alignOffset={props.alignOffset}
|
|
165
|
+
collisionPadding={props.collisionPadding}
|
|
123
166
|
forceMount={props.forceMount}
|
|
124
167
|
motionPresent={open}
|
|
125
|
-
offset={props.offset}
|
|
126
168
|
onInteractOutside={props.onInteractOutside}
|
|
127
169
|
onPointerDownOutside={props.onPointerDownOutside}
|
|
128
|
-
padding={props.padding}
|
|
129
170
|
placement={props.placement}
|
|
171
|
+
sideOffset={props.sideOffset}
|
|
130
172
|
transition={props.transition}
|
|
131
173
|
>
|
|
132
174
|
{props.children}
|
|
@@ -140,14 +182,15 @@ export function ComboboxContent(props: ComboboxContentProps) {
|
|
|
140
182
|
render={(state) => (
|
|
141
183
|
<ComboboxContentImpl
|
|
142
184
|
asChild={props.asChild}
|
|
185
|
+
alignOffset={props.alignOffset}
|
|
186
|
+
collisionPadding={props.collisionPadding}
|
|
143
187
|
forceMount={props.forceMount}
|
|
144
188
|
motionPresent={state.isPresent}
|
|
145
|
-
offset={props.offset}
|
|
146
189
|
onExitComplete={state.onExitComplete}
|
|
147
190
|
onInteractOutside={props.onInteractOutside}
|
|
148
191
|
onPointerDownOutside={props.onPointerDownOutside}
|
|
149
|
-
padding={props.padding}
|
|
150
192
|
placement={props.placement}
|
|
193
|
+
sideOffset={props.sideOffset}
|
|
151
194
|
transition={props.transition}
|
|
152
195
|
>
|
|
153
196
|
{props.children}
|
|
@@ -2,8 +2,10 @@ import { React, Slot } from "@lattice-ui/core";
|
|
|
2
2
|
import { useComboboxContext } from "./context";
|
|
3
3
|
import type { ComboboxInputProps } from "./types";
|
|
4
4
|
|
|
5
|
+
const UserInputService = game.GetService("UserInputService");
|
|
6
|
+
|
|
5
7
|
function toTextBox(instance: Instance | undefined) {
|
|
6
|
-
if (!instance
|
|
8
|
+
if (!instance?.IsA("TextBox")) {
|
|
7
9
|
return undefined;
|
|
8
10
|
}
|
|
9
11
|
|
|
@@ -43,6 +45,10 @@ export function ComboboxInput(props: ComboboxInputProps) {
|
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
if (UserInputService.GetFocusedTextBox() !== textBox) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
if (disabled || readOnly) {
|
|
47
53
|
if (textBox.Text !== lastInputValueRef.current) {
|
|
48
54
|
textBox.Text = lastInputValueRef.current;
|
|
@@ -7,10 +7,10 @@ let nextItemOrder = 0;
|
|
|
7
7
|
|
|
8
8
|
export function ComboboxItem(props: ComboboxItemProps) {
|
|
9
9
|
const comboboxContext = useComboboxContext();
|
|
10
|
-
const
|
|
10
|
+
const textValue = props.textValue ?? props.value;
|
|
11
|
+
const itemQueryMatch = comboboxContext.filterFn(textValue, comboboxContext.queryValue);
|
|
11
12
|
const disabled = comboboxContext.disabled || props.disabled === true;
|
|
12
13
|
const interactionDisabled = disabled || !itemQueryMatch;
|
|
13
|
-
const textValue = props.textValue ?? props.value;
|
|
14
14
|
|
|
15
15
|
const disabledRef = React.useRef(disabled);
|
|
16
16
|
const textValueRef = React.useRef(textValue);
|
|
@@ -39,6 +39,7 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
39
39
|
defaultValue: props.defaultInputValue ?? "",
|
|
40
40
|
onChange: props.onInputValueChange,
|
|
41
41
|
});
|
|
42
|
+
const [visibleQueryValue, setVisibleQueryValue] = React.useState(inputValue);
|
|
42
43
|
|
|
43
44
|
const disabled = props.disabled === true;
|
|
44
45
|
const readOnly = props.readOnly === true;
|
|
@@ -52,6 +53,7 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
52
53
|
|
|
53
54
|
const itemEntriesRef = React.useRef<Array<ComboboxItemRegistration>>([]);
|
|
54
55
|
const itemTextCacheRef = React.useRef<Record<string, string>>({});
|
|
56
|
+
const programmaticInputValueRef = React.useRef<string | undefined>();
|
|
55
57
|
const [registryRevision, setRegistryRevision] = React.useState(0);
|
|
56
58
|
|
|
57
59
|
const registerItem = React.useCallback((item: ComboboxItemRegistration) => {
|
|
@@ -90,6 +92,7 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
90
92
|
|
|
91
93
|
const syncInputFromValue = React.useCallback(() => {
|
|
92
94
|
const nextInputValue = value !== undefined ? (getItemText(value) ?? "") : "";
|
|
95
|
+
programmaticInputValueRef.current = nextInputValue;
|
|
93
96
|
setInputValueState(nextInputValue);
|
|
94
97
|
}, [getItemText, setInputValueState, value]);
|
|
95
98
|
|
|
@@ -111,12 +114,13 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
const selected = resolveOrderedItems().find((item) => item.value === nextValue);
|
|
114
|
-
if (selected
|
|
117
|
+
if (selected?.getDisabled()) {
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
setValueState(nextValue);
|
|
119
122
|
const nextInputValue = getItemText(nextValue) ?? nextValue;
|
|
123
|
+
programmaticInputValueRef.current = nextInputValue;
|
|
120
124
|
setInputValueState(nextInputValue);
|
|
121
125
|
},
|
|
122
126
|
[disabled, getItemText, resolveOrderedItems, setInputValueState, setValueState],
|
|
@@ -128,6 +132,16 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
128
132
|
return;
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
if (programmaticInputValueRef.current !== undefined && nextInputValue === programmaticInputValueRef.current) {
|
|
136
|
+
programmaticInputValueRef.current = undefined;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
programmaticInputValueRef.current = undefined;
|
|
141
|
+
setVisibleQueryValue((currentQueryValue) =>
|
|
142
|
+
currentQueryValue === nextInputValue ? currentQueryValue : nextInputValue,
|
|
143
|
+
);
|
|
144
|
+
|
|
131
145
|
if (nextInputValue === inputValue) {
|
|
132
146
|
return;
|
|
133
147
|
}
|
|
@@ -160,7 +174,10 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
syncInputFromValue();
|
|
163
|
-
|
|
177
|
+
setVisibleQueryValue((currentQueryValue) => (currentQueryValue === inputValue ? currentQueryValue : inputValue));
|
|
178
|
+
}, [inputValue, open, syncInputFromValue, value]);
|
|
179
|
+
|
|
180
|
+
const queryValue = open ? visibleQueryValue : inputValue;
|
|
164
181
|
|
|
165
182
|
const contextValue = React.useMemo(
|
|
166
183
|
() => ({
|
|
@@ -169,6 +186,7 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
169
186
|
value,
|
|
170
187
|
setValue,
|
|
171
188
|
inputValue,
|
|
189
|
+
queryValue,
|
|
172
190
|
setInputValue,
|
|
173
191
|
syncInputFromValue,
|
|
174
192
|
disabled,
|
|
@@ -188,6 +206,7 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
188
206
|
getItemText,
|
|
189
207
|
inputValue,
|
|
190
208
|
open,
|
|
209
|
+
queryValue,
|
|
191
210
|
readOnly,
|
|
192
211
|
registerItem,
|
|
193
212
|
required,
|
|
@@ -197,6 +216,7 @@ export function ComboboxRoot(props: ComboboxProps) {
|
|
|
197
216
|
setValue,
|
|
198
217
|
syncInputFromValue,
|
|
199
218
|
value,
|
|
219
|
+
visibleQueryValue,
|
|
200
220
|
],
|
|
201
221
|
);
|
|
202
222
|
|
|
@@ -3,7 +3,7 @@ import { useComboboxContext } from "./context";
|
|
|
3
3
|
import type { ComboboxTriggerProps } from "./types";
|
|
4
4
|
|
|
5
5
|
function toGuiObject(instance: Instance | undefined) {
|
|
6
|
-
if (!instance
|
|
6
|
+
if (!instance?.IsA("GuiObject")) {
|
|
7
7
|
return undefined;
|
|
8
8
|
}
|
|
9
9
|
|
package/src/Combobox/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type ComboboxContextValue = {
|
|
|
23
23
|
value?: string;
|
|
24
24
|
setValue: ComboboxSetValue;
|
|
25
25
|
inputValue: string;
|
|
26
|
+
queryValue: string;
|
|
26
27
|
setInputValue: ComboboxSetInputValue;
|
|
27
28
|
syncInputFromValue: () => void;
|
|
28
29
|
disabled: boolean;
|
|
@@ -85,8 +86,9 @@ export type ComboboxContentProps = {
|
|
|
85
86
|
asChild?: boolean;
|
|
86
87
|
forceMount?: boolean;
|
|
87
88
|
placement?: PopperPlacement;
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
sideOffset?: number;
|
|
90
|
+
alignOffset?: number;
|
|
91
|
+
collisionPadding?: number;
|
|
90
92
|
onPointerDownOutside?: (event: LayerInteractEvent) => void;
|
|
91
93
|
onInteractOutside?: (event: LayerInteractEvent) => void;
|
|
92
94
|
children?: React.ReactNode;
|