@lattice-ui/combobox 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+
2
+ > @lattice-ui/combobox@0.4.1 build C:\Users\retur\OneDrive\Desktop\Workspace\rojo\lattice-ui\packages\combobox
3
+ > rbxtsc -p tsconfig.json
4
+
@@ -0,0 +1,4 @@
1
+
2
+ > @lattice-ui/combobox@0.4.1 typecheck C:\Users\retur\OneDrive\Desktop\Workspace\rojo\lattice-ui\packages\combobox
3
+ > tsc -p tsconfig.typecheck.json
4
+
@@ -3,7 +3,6 @@ local TS = _G[script]
3
3
  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
- local RovingFocusGroup = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusGroup
7
6
  local _layer = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out)
8
7
  local DismissableLayer = _layer.DismissableLayer
9
8
  local Presence = _layer.Presence
@@ -18,7 +17,7 @@ end
18
17
  local function ComboboxContentImpl(props)
19
18
  local comboboxContext = useComboboxContext()
20
19
  local popper = usePopper({
21
- anchorRef = comboboxContext.triggerRef,
20
+ anchorRef = comboboxContext.anchorRef,
22
21
  contentRef = comboboxContext.contentRef,
23
22
  placement = props.placement,
24
23
  offset = props.offset,
@@ -50,17 +49,12 @@ local function ComboboxContentImpl(props)
50
49
  }, props.children))
51
50
  return React.createElement(DismissableLayer, {
52
51
  enabled = props.enabled,
52
+ insideRefs = { comboboxContext.triggerRef, comboboxContext.inputRef },
53
53
  modal = false,
54
54
  onDismiss = props.onDismiss,
55
- onEscapeKeyDown = props.onEscapeKeyDown,
56
55
  onInteractOutside = props.onInteractOutside,
57
56
  onPointerDownOutside = props.onPointerDownOutside,
58
- }, React.createElement(RovingFocusGroup, {
59
- active = props.enabled,
60
- autoFocus = "first",
61
- loop = comboboxContext.loop,
62
- orientation = "vertical",
63
- }, contentNode))
57
+ }, contentNode)
64
58
  end
65
59
  local function ComboboxContent(props)
66
60
  local comboboxContext = useComboboxContext()
@@ -78,7 +72,6 @@ local function ComboboxContent(props)
78
72
  enabled = open,
79
73
  offset = props.offset,
80
74
  onDismiss = handleDismiss,
81
- onEscapeKeyDown = props.onEscapeKeyDown,
82
75
  onInteractOutside = props.onInteractOutside,
83
76
  onPointerDownOutside = props.onPointerDownOutside,
84
77
  padding = props.padding,
@@ -95,7 +88,6 @@ local function ComboboxContent(props)
95
88
  enabled = state.isPresent,
96
89
  offset = props.offset,
97
90
  onDismiss = handleDismiss,
98
- onEscapeKeyDown = props.onEscapeKeyDown,
99
91
  onInteractOutside = props.onInteractOutside,
100
92
  onPointerDownOutside = props.onPointerDownOutside,
101
93
  padding = props.padding,
@@ -15,9 +15,21 @@ local function ComboboxInput(props)
15
15
  local disabled = comboboxContext.disabled or props.disabled == true
16
16
  local readOnly = comboboxContext.readOnly or props.readOnly == true
17
17
  local setInputRef = React.useCallback(function(instance)
18
- comboboxContext.inputRef.current = toTextBox(instance)
19
- end, { comboboxContext.inputRef })
18
+ local previousInput = comboboxContext.inputRef.current
19
+ local nextInput = toTextBox(instance)
20
+ comboboxContext.inputRef.current = nextInput
21
+ if nextInput then
22
+ comboboxContext.anchorRef.current = nextInput
23
+ return nil
24
+ end
25
+ if comboboxContext.anchorRef.current == previousInput then
26
+ comboboxContext.anchorRef.current = comboboxContext.triggerRef.current
27
+ end
28
+ end, { comboboxContext.anchorRef, comboboxContext.inputRef, comboboxContext.triggerRef })
20
29
  local handleTextChanged = React.useCallback(function(textBox)
30
+ if textBox.Text == comboboxContext.inputValue then
31
+ return nil
32
+ end
21
33
  if disabled or readOnly then
22
34
  if textBox.Text ~= comboboxContext.inputValue then
23
35
  textBox.Text = comboboxContext.inputValue
@@ -26,19 +38,6 @@ local function ComboboxInput(props)
26
38
  end
27
39
  comboboxContext.setInputValue(textBox.Text)
28
40
  end, { comboboxContext, disabled, readOnly })
29
- local handleFocusLost = React.useCallback(function()
30
- comboboxContext.setOpen(false)
31
- comboboxContext.syncInputFromValue()
32
- end, { comboboxContext })
33
- local handleInputBegan = React.useCallback(function(_rbx, inputObject)
34
- if disabled then
35
- return nil
36
- end
37
- local keyCode = inputObject.KeyCode
38
- if keyCode == Enum.KeyCode.Down or keyCode == Enum.KeyCode.Up then
39
- comboboxContext.setOpen(true)
40
- end
41
- end, { comboboxContext, disabled })
42
41
  local _object = {
43
42
  Active = not disabled,
44
43
  ClearTextOnFocus = false,
@@ -55,10 +54,6 @@ local function ComboboxInput(props)
55
54
  _object.Change = {
56
55
  Text = handleTextChanged,
57
56
  }
58
- _object.Event = {
59
- FocusLost = handleFocusLost,
60
- InputBegan = handleInputBegan,
61
- }
62
57
  _object.ref = setInputRef
63
58
  local sharedProps = _object
64
59
  if props.asChild then
@@ -3,25 +3,18 @@ local TS = _G[script]
3
3
  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
- local RovingFocusItem = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusItem
7
6
  local useComboboxContext = TS.import(script, script.Parent, "context").useComboboxContext
8
7
  local nextItemId = 0
9
8
  local nextItemOrder = 0
10
- local function toGuiObject(instance)
11
- if not instance or not instance:IsA("GuiObject") then
12
- return nil
13
- end
14
- return instance
15
- end
16
9
  local function ComboboxItem(props)
17
10
  local comboboxContext = useComboboxContext()
18
- local itemRef = React.useRef()
19
11
  local _condition = props.textValue
20
12
  if _condition == nil then
21
13
  _condition = props.value
22
14
  end
23
15
  local itemQueryMatch = comboboxContext.filterFn(_condition, comboboxContext.inputValue)
24
- local disabled = comboboxContext.disabled or props.disabled == true or not itemQueryMatch
16
+ local disabled = comboboxContext.disabled or props.disabled == true
17
+ local interactionDisabled = disabled or not itemQueryMatch
25
18
  local _condition_1 = props.textValue
26
19
  if _condition_1 == nil then
27
20
  _condition_1 = props.value
@@ -50,9 +43,6 @@ local function ComboboxItem(props)
50
43
  id = itemIdRef.current,
51
44
  value = props.value,
52
45
  order = itemOrderRef.current,
53
- getNode = function()
54
- return itemRef.current
55
- end,
56
46
  getDisabled = function()
57
47
  return disabledRef.current
58
48
  end,
@@ -61,18 +51,15 @@ local function ComboboxItem(props)
61
51
  end,
62
52
  })
63
53
  end, { comboboxContext, props.value })
64
- local setItemRef = React.useCallback(function(instance)
65
- itemRef.current = toGuiObject(instance)
66
- end, {})
67
54
  local handleSelect = React.useCallback(function()
68
- if disabled then
55
+ if interactionDisabled then
69
56
  return nil
70
57
  end
71
58
  comboboxContext.setValue(props.value)
72
59
  comboboxContext.setOpen(false)
73
- end, { comboboxContext, disabled, props.value })
60
+ end, { comboboxContext, interactionDisabled, props.value })
74
61
  local handleInputBegan = React.useCallback(function(_rbx, inputObject)
75
- if disabled then
62
+ if interactionDisabled then
76
63
  return nil
77
64
  end
78
65
  local keyCode = inputObject.KeyCode
@@ -81,7 +68,7 @@ local function ComboboxItem(props)
81
68
  end
82
69
  comboboxContext.setValue(props.value)
83
70
  comboboxContext.setOpen(false)
84
- end, { comboboxContext, disabled, props.value })
71
+ end, { comboboxContext, interactionDisabled, props.value })
85
72
  local eventHandlers = React.useMemo(function()
86
73
  return {
87
74
  Activated = handleSelect,
@@ -93,38 +80,30 @@ local function ComboboxItem(props)
93
80
  if not child then
94
81
  error("[ComboboxItem] `asChild` requires a child element.")
95
82
  end
96
- return React.createElement(RovingFocusItem, {
97
- asChild = true,
98
- disabled = disabled,
99
- }, React.createElement(Slot, {
100
- Active = not disabled,
83
+ return React.createElement(Slot, {
84
+ Active = not interactionDisabled,
101
85
  Event = eventHandlers,
102
- Selectable = not disabled,
86
+ Selectable = false,
103
87
  Visible = itemQueryMatch,
104
- ref = setItemRef,
105
- }, child))
88
+ }, child)
106
89
  end
107
- return React.createElement(RovingFocusItem, {
108
- asChild = true,
109
- disabled = disabled,
110
- }, React.createElement("textbutton", {
111
- Active = not disabled,
90
+ return React.createElement("textbutton", {
91
+ Active = not interactionDisabled,
112
92
  AutoButtonColor = false,
113
93
  BackgroundColor3 = Color3.fromRGB(47, 53, 68),
114
94
  BorderSizePixel = 0,
115
95
  Event = eventHandlers,
116
- Selectable = not disabled,
96
+ Selectable = false,
117
97
  Size = UDim2.fromOffset(220, 32),
118
98
  Text = textValue,
119
- TextColor3 = if disabled then Color3.fromRGB(134, 141, 156) else Color3.fromRGB(234, 239, 247),
99
+ TextColor3 = if interactionDisabled then Color3.fromRGB(134, 141, 156) else Color3.fromRGB(234, 239, 247),
120
100
  TextSize = 15,
121
101
  TextXAlignment = Enum.TextXAlignment.Left,
122
102
  Visible = itemQueryMatch,
123
- ref = setItemRef,
124
103
  }, React.createElement("uipadding", {
125
104
  PaddingLeft = UDim.new(0, 10),
126
105
  PaddingRight = UDim.new(0, 10),
127
- }), props.children))
106
+ }), props.children)
128
107
  end
129
108
  return {
130
109
  ComboboxItem = ComboboxItem,
@@ -6,7 +6,6 @@ local useControllableState = _core.useControllableState
6
6
  local ComboboxContextProvider = TS.import(script, script.Parent, "context").ComboboxContextProvider
7
7
  local _logic = TS.import(script, script.Parent, "logic")
8
8
  local defaultComboboxFilter = _logic.defaultComboboxFilter
9
- local resolveComboboxInputValue = _logic.resolveComboboxInputValue
10
9
  local resolveForcedComboboxValue = _logic.resolveForcedComboboxValue
11
10
  local function getOrderedItems(items)
12
11
  local _array = {}
@@ -78,21 +77,19 @@ local function ComboboxRoot(props)
78
77
  local disabled = props.disabled == true
79
78
  local readOnly = props.readOnly == true
80
79
  local required = props.required == true
81
- local _condition_2 = props.loop
82
- if _condition_2 == nil then
83
- _condition_2 = true
84
- end
85
- local loop = _condition_2
86
80
  local filterFn = props.filterFn or defaultComboboxFilter
81
+ local anchorRef = React.useRef()
87
82
  local triggerRef = React.useRef()
88
83
  local inputRef = React.useRef()
89
84
  local contentRef = React.useRef()
90
85
  local itemEntriesRef = React.useRef({})
86
+ local itemTextCacheRef = React.useRef({})
91
87
  local registryRevision, setRegistryRevision = React.useState(0)
92
88
  local registerItem = React.useCallback(function(item)
93
89
  local _current = itemEntriesRef.current
94
90
  local _item = item
95
91
  table.insert(_current, _item)
92
+ itemTextCacheRef.current[item.value] = item.getTextValue()
96
93
  setRegistryRevision(function(revision)
97
94
  return revision + 1
98
95
  end)
@@ -137,25 +134,33 @@ local function ComboboxRoot(props)
137
134
  end
138
135
  -- ▲ ReadonlyArray.find ▲
139
136
  local selected = _result
140
- local _result_1 = selected
141
- if _result_1 ~= nil then
142
- _result_1 = _result_1.getTextValue()
137
+ if selected then
138
+ local textValue = selected.getTextValue()
139
+ itemTextCacheRef.current[candidateValue] = textValue
140
+ return textValue
143
141
  end
144
- return _result_1
142
+ return itemTextCacheRef.current[candidateValue]
145
143
  end, { resolveOrderedItems })
146
144
  local syncInputFromValue = React.useCallback(function()
147
- local nextInputValue = resolveComboboxInputValue(value, toOptions(resolveOrderedItems()), "")
145
+ local _result
146
+ if value ~= nil then
147
+ local _condition_2 = getItemText(value)
148
+ if _condition_2 == nil then
149
+ _condition_2 = ""
150
+ end
151
+ _result = _condition_2
152
+ else
153
+ _result = ""
154
+ end
155
+ local nextInputValue = _result
148
156
  setInputValueState(nextInputValue)
149
- end, { resolveOrderedItems, setInputValueState, value })
157
+ end, { getItemText, setInputValueState, value })
150
158
  local setOpen = React.useCallback(function(nextOpen)
151
159
  if disabled and nextOpen then
152
160
  return nil
153
161
  end
154
162
  setOpenState(nextOpen)
155
- if not nextOpen then
156
- syncInputFromValue()
157
- end
158
- end, { disabled, setOpenState, syncInputFromValue })
163
+ end, { disabled, setOpenState })
159
164
  local setValue = React.useCallback(function(nextValue)
160
165
  if disabled then
161
166
  return nil
@@ -178,45 +183,36 @@ local function ComboboxRoot(props)
178
183
  return nil
179
184
  end
180
185
  setValueState(nextValue)
181
- local _condition_3 = getItemText(nextValue)
182
- if _condition_3 == nil then
183
- _condition_3 = nextValue
186
+ local _condition_2 = getItemText(nextValue)
187
+ if _condition_2 == nil then
188
+ _condition_2 = nextValue
184
189
  end
185
- local nextInputValue = _condition_3
190
+ local nextInputValue = _condition_2
186
191
  setInputValueState(nextInputValue)
187
192
  end, { disabled, getItemText, resolveOrderedItems, setInputValueState, setValueState })
188
193
  local setInputValue = React.useCallback(function(nextInputValue)
189
194
  if disabled or readOnly then
190
195
  return nil
191
196
  end
197
+ if nextInputValue == inputValue then
198
+ return nil
199
+ end
192
200
  setInputValueState(nextInputValue)
193
201
  setOpenState(true)
194
- end, { disabled, readOnly, setInputValueState, setOpenState })
195
- local getFilteredItems = React.useCallback(function()
196
- local query = inputValue
197
- local _exp = resolveOrderedItems()
198
- -- ▼ ReadonlyArray.filter ▼
199
- local _newValue = {}
200
- local _callback = function(item)
201
- return filterFn(item.getTextValue(), query)
202
- end
203
- local _length = 0
204
- for _k, _v in _exp do
205
- if _callback(_v, _k - 1, _exp) == true then
206
- _length += 1
207
- _newValue[_length] = _v
208
- end
209
- end
210
- -- ▲ ReadonlyArray.filter ▲
211
- return _newValue
212
- end, { filterFn, inputValue, resolveOrderedItems })
202
+ end, { disabled, inputValue, readOnly, setInputValueState, setOpenState })
213
203
  React.useEffect(function()
204
+ if not open then
205
+ return nil
206
+ end
214
207
  local orderedItems = resolveOrderedItems()
208
+ if #orderedItems == 0 then
209
+ return nil
210
+ end
215
211
  local nextValue = resolveForcedComboboxValue(value, toOptions(orderedItems))
216
- if nextValue ~= value then
212
+ if nextValue ~= nil and nextValue ~= value then
217
213
  setValueState(nextValue)
218
214
  end
219
- end, { registryRevision, resolveOrderedItems, setValueState, value })
215
+ end, { open, registryRevision, resolveOrderedItems, setValueState, value })
220
216
  React.useEffect(function()
221
217
  if open then
222
218
  return nil
@@ -235,17 +231,15 @@ local function ComboboxRoot(props)
235
231
  disabled = disabled,
236
232
  readOnly = readOnly,
237
233
  required = required,
238
- loop = loop,
239
234
  filterFn = filterFn,
235
+ anchorRef = anchorRef,
240
236
  triggerRef = triggerRef,
241
237
  inputRef = inputRef,
242
238
  contentRef = contentRef,
243
239
  registerItem = registerItem,
244
- getOrderedItems = resolveOrderedItems,
245
- getFilteredItems = getFilteredItems,
246
240
  getItemText = getItemText,
247
241
  }
248
- end, { disabled, filterFn, getFilteredItems, getItemText, inputValue, loop, open, readOnly, registerItem, required, resolveOrderedItems, setInputValue, setOpen, setValue, syncInputFromValue, value })
242
+ end, { disabled, filterFn, getItemText, inputValue, open, readOnly, registerItem, required, resolveOrderedItems, setInputValue, setOpen, setValue, syncInputFromValue, value })
249
243
  return React.createElement(ComboboxContextProvider, {
250
244
  value = contextValue,
251
245
  }, props.children)
@@ -14,8 +14,20 @@ local function ComboboxTrigger(props)
14
14
  local comboboxContext = useComboboxContext()
15
15
  local disabled = comboboxContext.disabled or props.disabled == true
16
16
  local setTriggerRef = React.useCallback(function(instance)
17
- comboboxContext.triggerRef.current = toGuiObject(instance)
18
- end, { comboboxContext.triggerRef })
17
+ local previousTrigger = comboboxContext.triggerRef.current
18
+ local nextTrigger = toGuiObject(instance)
19
+ comboboxContext.triggerRef.current = nextTrigger
20
+ if comboboxContext.inputRef.current then
21
+ return nil
22
+ end
23
+ if nextTrigger then
24
+ comboboxContext.anchorRef.current = nextTrigger
25
+ return nil
26
+ end
27
+ if comboboxContext.anchorRef.current == previousTrigger then
28
+ comboboxContext.anchorRef.current = nil
29
+ end
30
+ end, { comboboxContext.anchorRef, comboboxContext.inputRef, comboboxContext.triggerRef })
19
31
  local handleActivated = React.useCallback(function()
20
32
  if disabled then
21
33
  return nil
@@ -29,10 +41,6 @@ local function ComboboxTrigger(props)
29
41
  local keyCode = inputObject.KeyCode
30
42
  if keyCode == Enum.KeyCode.Return or keyCode == Enum.KeyCode.Space then
31
43
  comboboxContext.setOpen(not comboboxContext.open)
32
- return nil
33
- end
34
- if keyCode == Enum.KeyCode.Down or keyCode == Enum.KeyCode.Up then
35
- comboboxContext.setOpen(true)
36
44
  end
37
45
  end, { comboboxContext, disabled })
38
46
  local eventHandlers = React.useMemo(function()
@@ -49,7 +57,7 @@ local function ComboboxTrigger(props)
49
57
  return React.createElement(Slot, {
50
58
  Active = not disabled,
51
59
  Event = eventHandlers,
52
- Selectable = not disabled,
60
+ Selectable = false,
53
61
  ref = setTriggerRef,
54
62
  }, child)
55
63
  end
@@ -59,7 +67,7 @@ local function ComboboxTrigger(props)
59
67
  BackgroundColor3 = Color3.fromRGB(41, 48, 63),
60
68
  BorderSizePixel = 0,
61
69
  Event = eventHandlers,
62
- Selectable = not disabled,
70
+ Selectable = false,
63
71
  Size = UDim2.fromOffset(220, 36),
64
72
  Text = "Combobox",
65
73
  TextColor3 = if disabled then Color3.fromRGB(140, 148, 164) else Color3.fromRGB(235, 241, 248),
@@ -9,7 +9,6 @@ export type ComboboxItemRegistration = {
9
9
  id: number;
10
10
  value: string;
11
11
  order: number;
12
- getNode: () => GuiObject | undefined;
13
12
  getDisabled: () => boolean;
14
13
  getTextValue: () => string;
15
14
  };
@@ -24,14 +23,12 @@ export type ComboboxContextValue = {
24
23
  disabled: boolean;
25
24
  readOnly: boolean;
26
25
  required: boolean;
27
- loop: boolean;
28
26
  filterFn: ComboboxFilterFn;
27
+ anchorRef: React.MutableRefObject<GuiObject | undefined>;
29
28
  triggerRef: React.MutableRefObject<GuiObject | undefined>;
30
29
  inputRef: React.MutableRefObject<TextBox | undefined>;
31
30
  contentRef: React.MutableRefObject<GuiObject | undefined>;
32
31
  registerItem: (item: ComboboxItemRegistration) => () => void;
33
- getOrderedItems: () => Array<ComboboxItemRegistration>;
34
- getFilteredItems: () => Array<ComboboxItemRegistration>;
35
32
  getItemText: (value: string) => string | undefined;
36
33
  };
37
34
  export type ComboboxProps = {
@@ -47,7 +44,6 @@ export type ComboboxProps = {
47
44
  disabled?: boolean;
48
45
  readOnly?: boolean;
49
46
  required?: boolean;
50
- loop?: boolean;
51
47
  filterFn?: ComboboxFilterFn;
52
48
  children?: React.ReactNode;
53
49
  };
@@ -79,7 +75,6 @@ export type ComboboxContentProps = {
79
75
  placement?: PopperPlacement;
80
76
  offset?: Vector2;
81
77
  padding?: number;
82
- onEscapeKeyDown?: (event: LayerInteractEvent) => void;
83
78
  onPointerDownOutside?: (event: LayerInteractEvent) => void;
84
79
  onInteractOutside?: (event: LayerInteractEvent) => void;
85
80
  children?: React.ReactNode;
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@lattice-ui/combobox",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "main": "out/init.luau",
6
6
  "types": "out/index.d.ts",
7
7
  "dependencies": {
8
- "@lattice-ui/core": "0.3.2",
9
- "@lattice-ui/layer": "0.3.2",
10
- "@lattice-ui/focus": "0.3.2",
11
- "@lattice-ui/popper": "0.3.2"
8
+ "@lattice-ui/core": "0.4.1",
9
+ "@lattice-ui/layer": "0.4.1",
10
+ "@lattice-ui/popper": "0.4.1"
12
11
  },
13
12
  "devDependencies": {
14
13
  "@rbxts/react": "17.3.7-ts.1",
@@ -1,5 +1,4 @@
1
1
  import { React, Slot } from "@lattice-ui/core";
2
- import { RovingFocusGroup } from "@lattice-ui/focus";
3
2
  import { DismissableLayer, Presence } from "@lattice-ui/layer";
4
3
  import { usePopper } from "@lattice-ui/popper";
5
4
  import { useComboboxContext } from "./context";
@@ -13,7 +12,7 @@ type ComboboxContentImplProps = {
13
12
  placement?: ComboboxContentProps["placement"];
14
13
  offset?: ComboboxContentProps["offset"];
15
14
  padding?: ComboboxContentProps["padding"];
16
- } & Pick<ComboboxContentProps, "children" | "onEscapeKeyDown" | "onInteractOutside" | "onPointerDownOutside">;
15
+ } & Pick<ComboboxContentProps, "children" | "onInteractOutside" | "onPointerDownOutside">;
17
16
 
18
17
  function toGuiObject(instance: Instance | undefined) {
19
18
  if (!instance || !instance.IsA("GuiObject")) {
@@ -27,7 +26,7 @@ function ComboboxContentImpl(props: ComboboxContentImplProps) {
27
26
  const comboboxContext = useComboboxContext();
28
27
 
29
28
  const popper = usePopper({
30
- anchorRef: comboboxContext.triggerRef,
29
+ anchorRef: comboboxContext.anchorRef,
31
30
  contentRef: comboboxContext.contentRef,
32
31
  placement: props.placement,
33
32
  offset: props.offset,
@@ -72,15 +71,13 @@ function ComboboxContentImpl(props: ComboboxContentImplProps) {
72
71
  return (
73
72
  <DismissableLayer
74
73
  enabled={props.enabled}
74
+ insideRefs={[comboboxContext.triggerRef, comboboxContext.inputRef]}
75
75
  modal={false}
76
76
  onDismiss={props.onDismiss}
77
- onEscapeKeyDown={props.onEscapeKeyDown}
78
77
  onInteractOutside={props.onInteractOutside}
79
78
  onPointerDownOutside={props.onPointerDownOutside}
80
79
  >
81
- <RovingFocusGroup active={props.enabled} autoFocus="first" loop={comboboxContext.loop} orientation="vertical">
82
- {contentNode}
83
- </RovingFocusGroup>
80
+ {contentNode}
84
81
  </DismissableLayer>
85
82
  );
86
83
  }
@@ -105,7 +102,6 @@ export function ComboboxContent(props: ComboboxContentProps) {
105
102
  enabled={open}
106
103
  offset={props.offset}
107
104
  onDismiss={handleDismiss}
108
- onEscapeKeyDown={props.onEscapeKeyDown}
109
105
  onInteractOutside={props.onInteractOutside}
110
106
  onPointerDownOutside={props.onPointerDownOutside}
111
107
  padding={props.padding}
@@ -127,7 +123,6 @@ export function ComboboxContent(props: ComboboxContentProps) {
127
123
  enabled={state.isPresent}
128
124
  offset={props.offset}
129
125
  onDismiss={handleDismiss}
130
- onEscapeKeyDown={props.onEscapeKeyDown}
131
126
  onInteractOutside={props.onInteractOutside}
132
127
  onPointerDownOutside={props.onPointerDownOutside}
133
128
  padding={props.padding}
@@ -17,13 +17,29 @@ export function ComboboxInput(props: ComboboxInputProps) {
17
17
 
18
18
  const setInputRef = React.useCallback(
19
19
  (instance: Instance | undefined) => {
20
- comboboxContext.inputRef.current = toTextBox(instance);
20
+ const previousInput = comboboxContext.inputRef.current;
21
+ const nextInput = toTextBox(instance);
22
+
23
+ comboboxContext.inputRef.current = nextInput;
24
+
25
+ if (nextInput) {
26
+ comboboxContext.anchorRef.current = nextInput;
27
+ return;
28
+ }
29
+
30
+ if (comboboxContext.anchorRef.current === previousInput) {
31
+ comboboxContext.anchorRef.current = comboboxContext.triggerRef.current;
32
+ }
21
33
  },
22
- [comboboxContext.inputRef],
34
+ [comboboxContext.anchorRef, comboboxContext.inputRef, comboboxContext.triggerRef],
23
35
  );
24
36
 
25
37
  const handleTextChanged = React.useCallback(
26
38
  (textBox: TextBox) => {
39
+ if (textBox.Text === comboboxContext.inputValue) {
40
+ return;
41
+ }
42
+
27
43
  if (disabled || readOnly) {
28
44
  if (textBox.Text !== comboboxContext.inputValue) {
29
45
  textBox.Text = comboboxContext.inputValue;
@@ -37,25 +53,6 @@ export function ComboboxInput(props: ComboboxInputProps) {
37
53
  [comboboxContext, disabled, readOnly],
38
54
  );
39
55
 
40
- const handleFocusLost = React.useCallback(() => {
41
- comboboxContext.setOpen(false);
42
- comboboxContext.syncInputFromValue();
43
- }, [comboboxContext]);
44
-
45
- const handleInputBegan = React.useCallback(
46
- (_rbx: GuiObject, inputObject: InputObject) => {
47
- if (disabled) {
48
- return;
49
- }
50
-
51
- const keyCode = inputObject.KeyCode;
52
- if (keyCode === Enum.KeyCode.Down || keyCode === Enum.KeyCode.Up) {
53
- comboboxContext.setOpen(true);
54
- }
55
- },
56
- [comboboxContext, disabled],
57
- );
58
-
59
56
  const sharedProps = {
60
57
  Active: !disabled,
61
58
  ClearTextOnFocus: false,
@@ -66,10 +63,6 @@ export function ComboboxInput(props: ComboboxInputProps) {
66
63
  Change: {
67
64
  Text: handleTextChanged,
68
65
  },
69
- Event: {
70
- FocusLost: handleFocusLost,
71
- InputBegan: handleInputBegan,
72
- },
73
66
  ref: setInputRef,
74
67
  };
75
68
 
@@ -1,25 +1,15 @@
1
1
  import { React, Slot } from "@lattice-ui/core";
2
- import { RovingFocusItem } from "@lattice-ui/focus";
3
2
  import { useComboboxContext } from "./context";
4
3
  import type { ComboboxItemProps } from "./types";
5
4
 
6
5
  let nextItemId = 0;
7
6
  let nextItemOrder = 0;
8
7
 
9
- function toGuiObject(instance: Instance | undefined) {
10
- if (!instance || !instance.IsA("GuiObject")) {
11
- return undefined;
12
- }
13
-
14
- return instance;
15
- }
16
-
17
8
  export function ComboboxItem(props: ComboboxItemProps) {
18
9
  const comboboxContext = useComboboxContext();
19
- const itemRef = React.useRef<GuiObject>();
20
-
21
10
  const itemQueryMatch = comboboxContext.filterFn(props.textValue ?? props.value, comboboxContext.inputValue);
22
- const disabled = comboboxContext.disabled || props.disabled === true || !itemQueryMatch;
11
+ const disabled = comboboxContext.disabled || props.disabled === true;
12
+ const interactionDisabled = disabled || !itemQueryMatch;
23
13
  const textValue = props.textValue ?? props.value;
24
14
 
25
15
  const disabledRef = React.useRef(disabled);
@@ -50,28 +40,23 @@ export function ComboboxItem(props: ComboboxItemProps) {
50
40
  id: itemIdRef.current,
51
41
  value: props.value,
52
42
  order: itemOrderRef.current,
53
- getNode: () => itemRef.current,
54
43
  getDisabled: () => disabledRef.current,
55
44
  getTextValue: () => textValueRef.current,
56
45
  });
57
46
  }, [comboboxContext, props.value]);
58
47
 
59
- const setItemRef = React.useCallback((instance: Instance | undefined) => {
60
- itemRef.current = toGuiObject(instance);
61
- }, []);
62
-
63
48
  const handleSelect = React.useCallback(() => {
64
- if (disabled) {
49
+ if (interactionDisabled) {
65
50
  return;
66
51
  }
67
52
 
68
53
  comboboxContext.setValue(props.value);
69
54
  comboboxContext.setOpen(false);
70
- }, [comboboxContext, disabled, props.value]);
55
+ }, [comboboxContext, interactionDisabled, props.value]);
71
56
 
72
57
  const handleInputBegan = React.useCallback(
73
58
  (_rbx: GuiObject, inputObject: InputObject) => {
74
- if (disabled) {
59
+ if (interactionDisabled) {
75
60
  return;
76
61
  }
77
62
 
@@ -83,7 +68,7 @@ export function ComboboxItem(props: ComboboxItemProps) {
83
68
  comboboxContext.setValue(props.value);
84
69
  comboboxContext.setOpen(false);
85
70
  },
86
- [comboboxContext, disabled, props.value],
71
+ [comboboxContext, interactionDisabled, props.value],
87
72
  );
88
73
 
89
74
  const eventHandlers = React.useMemo(
@@ -101,34 +86,29 @@ export function ComboboxItem(props: ComboboxItemProps) {
101
86
  }
102
87
 
103
88
  return (
104
- <RovingFocusItem asChild disabled={disabled}>
105
- <Slot Active={!disabled} Event={eventHandlers} Selectable={!disabled} Visible={itemQueryMatch} ref={setItemRef}>
106
- {child}
107
- </Slot>
108
- </RovingFocusItem>
89
+ <Slot Active={!interactionDisabled} Event={eventHandlers} Selectable={false} Visible={itemQueryMatch}>
90
+ {child}
91
+ </Slot>
109
92
  );
110
93
  }
111
94
 
112
95
  return (
113
- <RovingFocusItem asChild disabled={disabled}>
114
- <textbutton
115
- Active={!disabled}
116
- AutoButtonColor={false}
117
- BackgroundColor3={Color3.fromRGB(47, 53, 68)}
118
- BorderSizePixel={0}
119
- Event={eventHandlers}
120
- Selectable={!disabled}
121
- Size={UDim2.fromOffset(220, 32)}
122
- Text={textValue}
123
- TextColor3={disabled ? Color3.fromRGB(134, 141, 156) : Color3.fromRGB(234, 239, 247)}
124
- TextSize={15}
125
- TextXAlignment={Enum.TextXAlignment.Left}
126
- Visible={itemQueryMatch}
127
- ref={setItemRef}
128
- >
129
- <uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 10)} />
130
- {props.children}
131
- </textbutton>
132
- </RovingFocusItem>
96
+ <textbutton
97
+ Active={!interactionDisabled}
98
+ AutoButtonColor={false}
99
+ BackgroundColor3={Color3.fromRGB(47, 53, 68)}
100
+ BorderSizePixel={0}
101
+ Event={eventHandlers}
102
+ Selectable={false}
103
+ Size={UDim2.fromOffset(220, 32)}
104
+ Text={textValue}
105
+ TextColor3={interactionDisabled ? Color3.fromRGB(134, 141, 156) : Color3.fromRGB(234, 239, 247)}
106
+ TextSize={15}
107
+ TextXAlignment={Enum.TextXAlignment.Left}
108
+ Visible={itemQueryMatch}
109
+ >
110
+ <uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 10)} />
111
+ {props.children}
112
+ </textbutton>
133
113
  );
134
114
  }
@@ -1,11 +1,6 @@
1
1
  import { React, useControllableState } from "@lattice-ui/core";
2
2
  import { ComboboxContextProvider } from "./context";
3
- import {
4
- type ComboboxOption,
5
- defaultComboboxFilter,
6
- resolveComboboxInputValue,
7
- resolveForcedComboboxValue,
8
- } from "./logic";
3
+ import { type ComboboxOption, defaultComboboxFilter, resolveForcedComboboxValue } from "./logic";
9
4
  import type { ComboboxItemRegistration, ComboboxProps } from "./types";
10
5
 
11
6
  function getOrderedItems(items: Array<ComboboxItemRegistration>) {
@@ -48,18 +43,20 @@ export function ComboboxRoot(props: ComboboxProps) {
48
43
  const disabled = props.disabled === true;
49
44
  const readOnly = props.readOnly === true;
50
45
  const required = props.required === true;
51
- const loop = props.loop ?? true;
52
46
  const filterFn = props.filterFn ?? defaultComboboxFilter;
53
47
 
48
+ const anchorRef = React.useRef<GuiObject>();
54
49
  const triggerRef = React.useRef<GuiObject>();
55
50
  const inputRef = React.useRef<TextBox>();
56
51
  const contentRef = React.useRef<GuiObject>();
57
52
 
58
53
  const itemEntriesRef = React.useRef<Array<ComboboxItemRegistration>>([]);
54
+ const itemTextCacheRef = React.useRef<Record<string, string>>({});
59
55
  const [registryRevision, setRegistryRevision] = React.useState(0);
60
56
 
61
57
  const registerItem = React.useCallback((item: ComboboxItemRegistration) => {
62
58
  itemEntriesRef.current.push(item);
59
+ itemTextCacheRef.current[item.value] = item.getTextValue();
63
60
  setRegistryRevision((revision) => revision + 1);
64
61
 
65
62
  return () => {
@@ -78,15 +75,21 @@ export function ComboboxRoot(props: ComboboxProps) {
78
75
  const getItemText = React.useCallback(
79
76
  (candidateValue: string) => {
80
77
  const selected = resolveOrderedItems().find((item) => item.value === candidateValue);
81
- return selected?.getTextValue();
78
+ if (selected) {
79
+ const textValue = selected.getTextValue();
80
+ itemTextCacheRef.current[candidateValue] = textValue;
81
+ return textValue;
82
+ }
83
+
84
+ return itemTextCacheRef.current[candidateValue];
82
85
  },
83
86
  [resolveOrderedItems],
84
87
  );
85
88
 
86
89
  const syncInputFromValue = React.useCallback(() => {
87
- const nextInputValue = resolveComboboxInputValue(value, toOptions(resolveOrderedItems()), "");
90
+ const nextInputValue = value !== undefined ? (getItemText(value) ?? "") : "";
88
91
  setInputValueState(nextInputValue);
89
- }, [resolveOrderedItems, setInputValueState, value]);
92
+ }, [getItemText, setInputValueState, value]);
90
93
 
91
94
  const setOpen = React.useCallback(
92
95
  (nextOpen: boolean) => {
@@ -95,12 +98,8 @@ export function ComboboxRoot(props: ComboboxProps) {
95
98
  }
96
99
 
97
100
  setOpenState(nextOpen);
98
-
99
- if (!nextOpen) {
100
- syncInputFromValue();
101
- }
102
101
  },
103
- [disabled, setOpenState, syncInputFromValue],
102
+ [disabled, setOpenState],
104
103
  );
105
104
 
106
105
  const setValue = React.useCallback(
@@ -127,24 +126,31 @@ export function ComboboxRoot(props: ComboboxProps) {
127
126
  return;
128
127
  }
129
128
 
129
+ if (nextInputValue === inputValue) {
130
+ return;
131
+ }
132
+
130
133
  setInputValueState(nextInputValue);
131
134
  setOpenState(true);
132
135
  },
133
- [disabled, readOnly, setInputValueState, setOpenState],
136
+ [disabled, inputValue, readOnly, setInputValueState, setOpenState],
134
137
  );
135
138
 
136
- const getFilteredItems = React.useCallback(() => {
137
- const query = inputValue;
138
- return resolveOrderedItems().filter((item) => filterFn(item.getTextValue(), query));
139
- }, [filterFn, inputValue, resolveOrderedItems]);
140
-
141
139
  React.useEffect(() => {
140
+ if (!open) {
141
+ return;
142
+ }
143
+
142
144
  const orderedItems = resolveOrderedItems();
145
+ if (orderedItems.size() === 0) {
146
+ return;
147
+ }
148
+
143
149
  const nextValue = resolveForcedComboboxValue(value, toOptions(orderedItems));
144
- if (nextValue !== value) {
150
+ if (nextValue !== undefined && nextValue !== value) {
145
151
  setValueState(nextValue);
146
152
  }
147
- }, [registryRevision, resolveOrderedItems, setValueState, value]);
153
+ }, [open, registryRevision, resolveOrderedItems, setValueState, value]);
148
154
 
149
155
  React.useEffect(() => {
150
156
  if (open) {
@@ -166,23 +172,19 @@ export function ComboboxRoot(props: ComboboxProps) {
166
172
  disabled,
167
173
  readOnly,
168
174
  required,
169
- loop,
170
175
  filterFn,
176
+ anchorRef,
171
177
  triggerRef,
172
178
  inputRef,
173
179
  contentRef,
174
180
  registerItem,
175
- getOrderedItems: resolveOrderedItems,
176
- getFilteredItems,
177
181
  getItemText,
178
182
  }),
179
183
  [
180
184
  disabled,
181
185
  filterFn,
182
- getFilteredItems,
183
186
  getItemText,
184
187
  inputValue,
185
- loop,
186
188
  open,
187
189
  readOnly,
188
190
  registerItem,
@@ -16,9 +16,25 @@ export function ComboboxTrigger(props: ComboboxTriggerProps) {
16
16
 
17
17
  const setTriggerRef = React.useCallback(
18
18
  (instance: Instance | undefined) => {
19
- comboboxContext.triggerRef.current = toGuiObject(instance);
19
+ const previousTrigger = comboboxContext.triggerRef.current;
20
+ const nextTrigger = toGuiObject(instance);
21
+
22
+ comboboxContext.triggerRef.current = nextTrigger;
23
+
24
+ if (comboboxContext.inputRef.current) {
25
+ return;
26
+ }
27
+
28
+ if (nextTrigger) {
29
+ comboboxContext.anchorRef.current = nextTrigger;
30
+ return;
31
+ }
32
+
33
+ if (comboboxContext.anchorRef.current === previousTrigger) {
34
+ comboboxContext.anchorRef.current = undefined;
35
+ }
20
36
  },
21
- [comboboxContext.triggerRef],
37
+ [comboboxContext.anchorRef, comboboxContext.inputRef, comboboxContext.triggerRef],
22
38
  );
23
39
 
24
40
  const handleActivated = React.useCallback(() => {
@@ -38,11 +54,6 @@ export function ComboboxTrigger(props: ComboboxTriggerProps) {
38
54
  const keyCode = inputObject.KeyCode;
39
55
  if (keyCode === Enum.KeyCode.Return || keyCode === Enum.KeyCode.Space) {
40
56
  comboboxContext.setOpen(!comboboxContext.open);
41
- return;
42
- }
43
-
44
- if (keyCode === Enum.KeyCode.Down || keyCode === Enum.KeyCode.Up) {
45
- comboboxContext.setOpen(true);
46
57
  }
47
58
  },
48
59
  [comboboxContext, disabled],
@@ -63,7 +74,7 @@ export function ComboboxTrigger(props: ComboboxTriggerProps) {
63
74
  }
64
75
 
65
76
  return (
66
- <Slot Active={!disabled} Event={eventHandlers} Selectable={!disabled} ref={setTriggerRef}>
77
+ <Slot Active={!disabled} Event={eventHandlers} Selectable={false} ref={setTriggerRef}>
67
78
  {child}
68
79
  </Slot>
69
80
  );
@@ -76,7 +87,7 @@ export function ComboboxTrigger(props: ComboboxTriggerProps) {
76
87
  BackgroundColor3={Color3.fromRGB(41, 48, 63)}
77
88
  BorderSizePixel={0}
78
89
  Event={eventHandlers}
79
- Selectable={!disabled}
90
+ Selectable={false}
80
91
  Size={UDim2.fromOffset(220, 36)}
81
92
  Text="Combobox"
82
93
  TextColor3={disabled ? Color3.fromRGB(140, 148, 164) : Color3.fromRGB(235, 241, 248)}
@@ -12,7 +12,6 @@ export type ComboboxItemRegistration = {
12
12
  id: number;
13
13
  value: string;
14
14
  order: number;
15
- getNode: () => GuiObject | undefined;
16
15
  getDisabled: () => boolean;
17
16
  getTextValue: () => string;
18
17
  };
@@ -28,14 +27,12 @@ export type ComboboxContextValue = {
28
27
  disabled: boolean;
29
28
  readOnly: boolean;
30
29
  required: boolean;
31
- loop: boolean;
32
30
  filterFn: ComboboxFilterFn;
31
+ anchorRef: React.MutableRefObject<GuiObject | undefined>;
33
32
  triggerRef: React.MutableRefObject<GuiObject | undefined>;
34
33
  inputRef: React.MutableRefObject<TextBox | undefined>;
35
34
  contentRef: React.MutableRefObject<GuiObject | undefined>;
36
35
  registerItem: (item: ComboboxItemRegistration) => () => void;
37
- getOrderedItems: () => Array<ComboboxItemRegistration>;
38
- getFilteredItems: () => Array<ComboboxItemRegistration>;
39
36
  getItemText: (value: string) => string | undefined;
40
37
  };
41
38
 
@@ -52,7 +49,6 @@ export type ComboboxProps = {
52
49
  disabled?: boolean;
53
50
  readOnly?: boolean;
54
51
  required?: boolean;
55
- loop?: boolean;
56
52
  filterFn?: ComboboxFilterFn;
57
53
  children?: React.ReactNode;
58
54
  };
@@ -89,7 +85,6 @@ export type ComboboxContentProps = {
89
85
  placement?: PopperPlacement;
90
86
  offset?: Vector2;
91
87
  padding?: number;
92
- onEscapeKeyDown?: (event: LayerInteractEvent) => void;
93
88
  onPointerDownOutside?: (event: LayerInteractEvent) => void;
94
89
  onInteractOutside?: (event: LayerInteractEvent) => void;
95
90
  children?: React.ReactNode;