@lattice-ui/core 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.
- package/out/focus/context.d.ts +14 -0
- package/out/focus/context.luau +27 -0
- package/out/focus/env.d.ts +1 -0
- package/out/focus/env.luau +5 -0
- package/out/focus/focusManager.d.ts +44 -0
- package/out/focus/focusManager.luau +727 -0
- package/out/focus/useFocusNode.d.ts +10 -0
- package/out/focus/useFocusNode.luau +65 -0
- package/out/index.d.ts +4 -0
- package/out/init.luau +12 -0
- package/out/orderedSelection.d.ts +16 -0
- package/out/orderedSelection.luau +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type ReactType from "@rbxts/react";
|
|
2
|
+
type FocusScopeProviderProps = {
|
|
3
|
+
scopeId?: number;
|
|
4
|
+
children?: ReactType.ReactNode;
|
|
5
|
+
};
|
|
6
|
+
type FocusLayerProviderProps = {
|
|
7
|
+
layerOrder?: number;
|
|
8
|
+
children?: ReactType.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
export declare function FocusScopeProvider(props: FocusScopeProviderProps): ReactType.JSX.Element;
|
|
11
|
+
export declare function useFocusScopeId(): number | undefined;
|
|
12
|
+
export declare function FocusLayerProvider(props: FocusLayerProviderProps): ReactType.JSX.Element;
|
|
13
|
+
export declare function useFocusLayerOrder(): number | undefined;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local React = TS.import(script, script.Parent.Parent, "react").default
|
|
4
|
+
local FocusScopeIdContext = React.createContext(nil)
|
|
5
|
+
local FocusLayerOrderContext = React.createContext(nil)
|
|
6
|
+
local function FocusScopeProvider(props)
|
|
7
|
+
return React.createElement(FocusScopeIdContext.Provider, {
|
|
8
|
+
value = props.scopeId,
|
|
9
|
+
}, props.children)
|
|
10
|
+
end
|
|
11
|
+
local function useFocusScopeId()
|
|
12
|
+
return React.useContext(FocusScopeIdContext)
|
|
13
|
+
end
|
|
14
|
+
local function FocusLayerProvider(props)
|
|
15
|
+
return React.createElement(FocusLayerOrderContext.Provider, {
|
|
16
|
+
value = props.layerOrder,
|
|
17
|
+
}, props.children)
|
|
18
|
+
end
|
|
19
|
+
local function useFocusLayerOrder()
|
|
20
|
+
return React.useContext(FocusLayerOrderContext)
|
|
21
|
+
end
|
|
22
|
+
return {
|
|
23
|
+
FocusScopeProvider = FocusScopeProvider,
|
|
24
|
+
useFocusScopeId = useFocusScopeId,
|
|
25
|
+
FocusLayerProvider = FocusLayerProvider,
|
|
26
|
+
useFocusLayerOrder = useFocusLayerOrder,
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const GuiService: GuiService;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type FocusRestoreSnapshot = {
|
|
2
|
+
nodeId?: number;
|
|
3
|
+
};
|
|
4
|
+
export type FocusNodeRecord = {
|
|
5
|
+
id: number;
|
|
6
|
+
scopeId?: number;
|
|
7
|
+
implicit: boolean;
|
|
8
|
+
order: number;
|
|
9
|
+
getGuiObject: () => GuiObject | undefined;
|
|
10
|
+
getDisabled: () => boolean;
|
|
11
|
+
getVisible: () => boolean | undefined;
|
|
12
|
+
getSyncToRoblox: () => boolean;
|
|
13
|
+
};
|
|
14
|
+
export type RegisterFocusNodeParams = {
|
|
15
|
+
scopeId?: number;
|
|
16
|
+
getGuiObject: () => GuiObject | undefined;
|
|
17
|
+
getDisabled?: () => boolean;
|
|
18
|
+
getVisible?: () => boolean | undefined;
|
|
19
|
+
getSyncToRoblox?: () => boolean;
|
|
20
|
+
};
|
|
21
|
+
export type RegisterFocusScopeParams = {
|
|
22
|
+
parentScopeId?: number;
|
|
23
|
+
getRoot: () => GuiObject | undefined;
|
|
24
|
+
getActive: () => boolean;
|
|
25
|
+
getTrapped: () => boolean;
|
|
26
|
+
getRestoreFocus?: () => boolean;
|
|
27
|
+
getLayerOrder?: () => number | undefined;
|
|
28
|
+
};
|
|
29
|
+
export declare function registerFocusNode(params: RegisterFocusNodeParams): number;
|
|
30
|
+
export declare function createFocusScopeId(): number;
|
|
31
|
+
export declare function unregisterFocusNode(nodeId: number): void;
|
|
32
|
+
export declare function registerFocusScope(scopeId: number, params: RegisterFocusScopeParams): number;
|
|
33
|
+
export declare function syncFocusScope(scopeId: number): void;
|
|
34
|
+
export declare function unregisterFocusScope(scopeId: number): void;
|
|
35
|
+
export declare function retainExternalFocusBridge(): void;
|
|
36
|
+
export declare function releaseExternalFocusBridge(): void;
|
|
37
|
+
export declare function canFocusNode(nodeId: number): boolean;
|
|
38
|
+
export declare function focusNode(nodeId: number): GuiObject | undefined;
|
|
39
|
+
export declare function focusGuiObject(guiObject: GuiObject | undefined): GuiObject | undefined;
|
|
40
|
+
export declare function clearFocus(): void;
|
|
41
|
+
export declare function getFocusedNode(): FocusNodeRecord | undefined;
|
|
42
|
+
export declare function getFocusedGuiObject(): GuiObject | undefined;
|
|
43
|
+
export declare function captureRestoreSnapshot(): FocusRestoreSnapshot;
|
|
44
|
+
export declare function restoreSnapshot(snapshot: FocusRestoreSnapshot | undefined): GuiObject | undefined;
|
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local GuiService = TS.import(script, script.Parent, "env").GuiService
|
|
4
|
+
local focusNodes = {}
|
|
5
|
+
local focusScopes = {}
|
|
6
|
+
local nextFocusNodeId = 0
|
|
7
|
+
local nextFocusScopeId = 0
|
|
8
|
+
local nextFocusOrder = 0
|
|
9
|
+
local currentFocusedNodeId
|
|
10
|
+
local externalSelectionConsumerCount = 0
|
|
11
|
+
local selectedObjectConnection
|
|
12
|
+
local bridgeWriteDepth = 0
|
|
13
|
+
local function isLiveGuiObject(guiObject)
|
|
14
|
+
return guiObject ~= nil and guiObject.Parent ~= nil
|
|
15
|
+
end
|
|
16
|
+
local function isEffectivelyVisible(guiObject)
|
|
17
|
+
if not isLiveGuiObject(guiObject) or not guiObject.Visible then
|
|
18
|
+
return false
|
|
19
|
+
end
|
|
20
|
+
local ancestor = guiObject.Parent
|
|
21
|
+
while ancestor ~= nil do
|
|
22
|
+
if ancestor:IsA("GuiObject") and not ancestor.Visible then
|
|
23
|
+
return false
|
|
24
|
+
end
|
|
25
|
+
if ancestor:IsA("LayerCollector") and not ancestor.Enabled then
|
|
26
|
+
return false
|
|
27
|
+
end
|
|
28
|
+
ancestor = ancestor.Parent
|
|
29
|
+
end
|
|
30
|
+
return true
|
|
31
|
+
end
|
|
32
|
+
local function isInsideRoot(scopeRoot, guiObject)
|
|
33
|
+
if not isLiveGuiObject(scopeRoot) or not isLiveGuiObject(guiObject) then
|
|
34
|
+
return false
|
|
35
|
+
end
|
|
36
|
+
return guiObject == scopeRoot or guiObject:IsDescendantOf(scopeRoot)
|
|
37
|
+
end
|
|
38
|
+
local function isRawGuiObjectFocusable(guiObject)
|
|
39
|
+
return isLiveGuiObject(guiObject) and isEffectivelyVisible(guiObject) and guiObject.Selectable
|
|
40
|
+
end
|
|
41
|
+
local function findFocusNodeIndex(nodeId)
|
|
42
|
+
-- ▼ ReadonlyArray.findIndex ▼
|
|
43
|
+
local _callback = function(entry)
|
|
44
|
+
return entry.id == nodeId
|
|
45
|
+
end
|
|
46
|
+
local _result = -1
|
|
47
|
+
for _i, _v in focusNodes do
|
|
48
|
+
if _callback(_v, _i - 1, focusNodes) == true then
|
|
49
|
+
_result = _i - 1
|
|
50
|
+
break
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
-- ▲ ReadonlyArray.findIndex ▲
|
|
54
|
+
return _result
|
|
55
|
+
end
|
|
56
|
+
local function findFocusScopeIndex(scopeId)
|
|
57
|
+
-- ▼ ReadonlyArray.findIndex ▼
|
|
58
|
+
local _callback = function(entry)
|
|
59
|
+
return entry.id == scopeId
|
|
60
|
+
end
|
|
61
|
+
local _result = -1
|
|
62
|
+
for _i, _v in focusScopes do
|
|
63
|
+
if _callback(_v, _i - 1, focusScopes) == true then
|
|
64
|
+
_result = _i - 1
|
|
65
|
+
break
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
-- ▲ ReadonlyArray.findIndex ▲
|
|
69
|
+
return _result
|
|
70
|
+
end
|
|
71
|
+
local function getFocusNodeRecord(nodeId)
|
|
72
|
+
if nodeId == nil then
|
|
73
|
+
return nil
|
|
74
|
+
end
|
|
75
|
+
local nodeIndex = findFocusNodeIndex(nodeId)
|
|
76
|
+
if nodeIndex < 0 then
|
|
77
|
+
return nil
|
|
78
|
+
end
|
|
79
|
+
return focusNodes[nodeIndex + 1]
|
|
80
|
+
end
|
|
81
|
+
local function getFocusScopeRecord(scopeId)
|
|
82
|
+
if scopeId == nil then
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
local scopeIndex = findFocusScopeIndex(scopeId)
|
|
86
|
+
if scopeIndex < 0 then
|
|
87
|
+
return nil
|
|
88
|
+
end
|
|
89
|
+
return focusScopes[scopeIndex + 1]
|
|
90
|
+
end
|
|
91
|
+
local function getNodeOwningScopeId(record, guiObject)
|
|
92
|
+
if record.scopeId ~= nil then
|
|
93
|
+
return record.scopeId
|
|
94
|
+
end
|
|
95
|
+
if not isLiveGuiObject(guiObject) then
|
|
96
|
+
return nil
|
|
97
|
+
end
|
|
98
|
+
local bestScopeId
|
|
99
|
+
local bestLayerOrder = -1
|
|
100
|
+
local bestScopeOrder = -1
|
|
101
|
+
for _, scopeRecord in focusScopes do
|
|
102
|
+
local scopeRoot = scopeRecord.getRoot()
|
|
103
|
+
if not isInsideRoot(scopeRoot, guiObject) then
|
|
104
|
+
continue
|
|
105
|
+
end
|
|
106
|
+
local _condition = scopeRecord.getLayerOrder()
|
|
107
|
+
if _condition == nil then
|
|
108
|
+
_condition = 0
|
|
109
|
+
end
|
|
110
|
+
local layerOrder = _condition
|
|
111
|
+
if layerOrder > bestLayerOrder or (layerOrder == bestLayerOrder and scopeRecord.order > bestScopeOrder) then
|
|
112
|
+
bestScopeId = scopeRecord.id
|
|
113
|
+
bestLayerOrder = layerOrder
|
|
114
|
+
bestScopeOrder = scopeRecord.order
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
return bestScopeId
|
|
118
|
+
end
|
|
119
|
+
local function isNodeInsideInactiveScope(record, guiObject)
|
|
120
|
+
local owningScopeId = getNodeOwningScopeId(record, guiObject)
|
|
121
|
+
local owningScope = getFocusScopeRecord(owningScopeId)
|
|
122
|
+
return owningScope ~= nil and not owningScope.getActive()
|
|
123
|
+
end
|
|
124
|
+
local function getTopTrappedScope(excludingScopeId)
|
|
125
|
+
local bestScope
|
|
126
|
+
for _, scopeRecord in focusScopes do
|
|
127
|
+
if scopeRecord.id == excludingScopeId or not scopeRecord.getActive() or not scopeRecord.getTrapped() then
|
|
128
|
+
continue
|
|
129
|
+
end
|
|
130
|
+
local scopeRoot = scopeRecord.getRoot()
|
|
131
|
+
if not isLiveGuiObject(scopeRoot) then
|
|
132
|
+
continue
|
|
133
|
+
end
|
|
134
|
+
if not bestScope then
|
|
135
|
+
bestScope = scopeRecord
|
|
136
|
+
continue
|
|
137
|
+
end
|
|
138
|
+
local _condition = bestScope.getLayerOrder()
|
|
139
|
+
if _condition == nil then
|
|
140
|
+
_condition = 0
|
|
141
|
+
end
|
|
142
|
+
local bestLayerOrder = _condition
|
|
143
|
+
local _condition_1 = scopeRecord.getLayerOrder()
|
|
144
|
+
if _condition_1 == nil then
|
|
145
|
+
_condition_1 = 0
|
|
146
|
+
end
|
|
147
|
+
local nextLayerOrder = _condition_1
|
|
148
|
+
if nextLayerOrder > bestLayerOrder or (nextLayerOrder == bestLayerOrder and scopeRecord.order > bestScope.order) then
|
|
149
|
+
bestScope = scopeRecord
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
return bestScope
|
|
153
|
+
end
|
|
154
|
+
local function getResolvedFocusNode(nodeId, options)
|
|
155
|
+
local record = getFocusNodeRecord(nodeId)
|
|
156
|
+
if not record then
|
|
157
|
+
return nil
|
|
158
|
+
end
|
|
159
|
+
local guiObject = record.getGuiObject()
|
|
160
|
+
if not isLiveGuiObject(guiObject) then
|
|
161
|
+
return nil
|
|
162
|
+
end
|
|
163
|
+
if record.getDisabled() then
|
|
164
|
+
return nil
|
|
165
|
+
end
|
|
166
|
+
local explicitVisible = record.getVisible()
|
|
167
|
+
if explicitVisible == false or not isEffectivelyVisible(guiObject) then
|
|
168
|
+
return nil
|
|
169
|
+
end
|
|
170
|
+
if isNodeInsideInactiveScope(record, guiObject) then
|
|
171
|
+
return nil
|
|
172
|
+
end
|
|
173
|
+
local _result = options
|
|
174
|
+
if _result ~= nil then
|
|
175
|
+
_result = _result.trapScopeOverride
|
|
176
|
+
end
|
|
177
|
+
local _condition = _result
|
|
178
|
+
if _condition == nil then
|
|
179
|
+
_condition = getTopTrappedScope()
|
|
180
|
+
end
|
|
181
|
+
local trappedScope = _condition
|
|
182
|
+
if trappedScope and not isInsideRoot(trappedScope.getRoot(), guiObject) then
|
|
183
|
+
return nil
|
|
184
|
+
end
|
|
185
|
+
return {
|
|
186
|
+
record = record,
|
|
187
|
+
guiObject = guiObject,
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
local function canSyncNodeToRoblox(resolvedNode)
|
|
191
|
+
return resolvedNode.record.getSyncToRoblox() and resolvedNode.guiObject.Selectable
|
|
192
|
+
end
|
|
193
|
+
local function withBridgeWrite(callback)
|
|
194
|
+
bridgeWriteDepth += 1
|
|
195
|
+
local result = callback()
|
|
196
|
+
bridgeWriteDepth -= 1
|
|
197
|
+
return result
|
|
198
|
+
end
|
|
199
|
+
local function syncRobloxSelection()
|
|
200
|
+
local resolvedFocusedNode = if currentFocusedNodeId ~= nil then getResolvedFocusNode(currentFocusedNodeId) else nil
|
|
201
|
+
local nextSelectedObject = if resolvedFocusedNode and canSyncNodeToRoblox(resolvedFocusedNode) then resolvedFocusedNode.guiObject else nil
|
|
202
|
+
if GuiService.SelectedObject == nextSelectedObject then
|
|
203
|
+
return nil
|
|
204
|
+
end
|
|
205
|
+
withBridgeWrite(function()
|
|
206
|
+
GuiService.SelectedObject = nextSelectedObject
|
|
207
|
+
end)
|
|
208
|
+
end
|
|
209
|
+
local function updateScopeLastFocusedNode(nodeId, guiObject)
|
|
210
|
+
for _, scopeRecord in focusScopes do
|
|
211
|
+
if not scopeRecord.getActive() then
|
|
212
|
+
continue
|
|
213
|
+
end
|
|
214
|
+
if isInsideRoot(scopeRecord.getRoot(), guiObject) then
|
|
215
|
+
scopeRecord.lastFocusedNodeId = nodeId
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
local function setCurrentFocusedNode(nodeId)
|
|
220
|
+
currentFocusedNodeId = nodeId
|
|
221
|
+
local resolvedNode = if nodeId ~= nil then getResolvedFocusNode(nodeId) else nil
|
|
222
|
+
if resolvedNode then
|
|
223
|
+
updateScopeLastFocusedNode(resolvedNode.record.id, resolvedNode.guiObject)
|
|
224
|
+
end
|
|
225
|
+
syncRobloxSelection()
|
|
226
|
+
local _result = resolvedNode
|
|
227
|
+
if _result ~= nil then
|
|
228
|
+
_result = _result.guiObject
|
|
229
|
+
end
|
|
230
|
+
return _result
|
|
231
|
+
end
|
|
232
|
+
local function findExistingFocusNodeByGuiObject(guiObject)
|
|
233
|
+
if not isLiveGuiObject(guiObject) then
|
|
234
|
+
return nil
|
|
235
|
+
end
|
|
236
|
+
for index = #focusNodes - 1, 0, -1 do
|
|
237
|
+
local nodeRecord = focusNodes[index + 1]
|
|
238
|
+
if nodeRecord.getGuiObject() == guiObject then
|
|
239
|
+
return nodeRecord
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
return nil
|
|
243
|
+
end
|
|
244
|
+
local function registerImplicitFocusNode(guiObject)
|
|
245
|
+
local existingNode = findExistingFocusNodeByGuiObject(guiObject)
|
|
246
|
+
if existingNode then
|
|
247
|
+
return existingNode
|
|
248
|
+
end
|
|
249
|
+
nextFocusNodeId += 1
|
|
250
|
+
nextFocusOrder += 1
|
|
251
|
+
local nodeRecord = {
|
|
252
|
+
id = nextFocusNodeId,
|
|
253
|
+
scopeId = nil,
|
|
254
|
+
implicit = true,
|
|
255
|
+
order = nextFocusOrder,
|
|
256
|
+
getGuiObject = function()
|
|
257
|
+
return guiObject
|
|
258
|
+
end,
|
|
259
|
+
getDisabled = function()
|
|
260
|
+
return false
|
|
261
|
+
end,
|
|
262
|
+
getVisible = function()
|
|
263
|
+
return nil
|
|
264
|
+
end,
|
|
265
|
+
getSyncToRoblox = function()
|
|
266
|
+
return true
|
|
267
|
+
end,
|
|
268
|
+
}
|
|
269
|
+
table.insert(focusNodes, nodeRecord)
|
|
270
|
+
return nodeRecord
|
|
271
|
+
end
|
|
272
|
+
local function resolveFocusNodeByGuiObject(guiObject, options)
|
|
273
|
+
if not isLiveGuiObject(guiObject) then
|
|
274
|
+
return nil
|
|
275
|
+
end
|
|
276
|
+
local existingNode = findExistingFocusNodeByGuiObject(guiObject)
|
|
277
|
+
if existingNode then
|
|
278
|
+
return getResolvedFocusNode(existingNode.id, options)
|
|
279
|
+
end
|
|
280
|
+
local _result = options
|
|
281
|
+
if _result ~= nil then
|
|
282
|
+
_result = _result.allowImplicit
|
|
283
|
+
end
|
|
284
|
+
local _condition = not _result
|
|
285
|
+
if not _condition then
|
|
286
|
+
_condition = not isRawGuiObjectFocusable(guiObject)
|
|
287
|
+
end
|
|
288
|
+
if _condition then
|
|
289
|
+
return nil
|
|
290
|
+
end
|
|
291
|
+
local implicitNode = registerImplicitFocusNode(guiObject)
|
|
292
|
+
return getResolvedFocusNode(implicitNode.id, options)
|
|
293
|
+
end
|
|
294
|
+
local function findFirstRegisteredNodeInScope(scopeRecord)
|
|
295
|
+
for _, nodeRecord in focusNodes do
|
|
296
|
+
local guiObject = nodeRecord.getGuiObject()
|
|
297
|
+
if not isInsideRoot(scopeRecord.getRoot(), guiObject) then
|
|
298
|
+
continue
|
|
299
|
+
end
|
|
300
|
+
local resolvedNode = getResolvedFocusNode(nodeRecord.id, {
|
|
301
|
+
trapScopeOverride = scopeRecord,
|
|
302
|
+
})
|
|
303
|
+
if resolvedNode then
|
|
304
|
+
return resolvedNode
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
return nil
|
|
308
|
+
end
|
|
309
|
+
local function findFirstFocusableDescendantInScope(scopeRecord)
|
|
310
|
+
local scopeRoot = scopeRecord.getRoot()
|
|
311
|
+
if not isLiveGuiObject(scopeRoot) then
|
|
312
|
+
return nil
|
|
313
|
+
end
|
|
314
|
+
local rootNode = resolveFocusNodeByGuiObject(scopeRoot, {
|
|
315
|
+
allowImplicit = true,
|
|
316
|
+
trapScopeOverride = scopeRecord,
|
|
317
|
+
})
|
|
318
|
+
if rootNode then
|
|
319
|
+
return rootNode
|
|
320
|
+
end
|
|
321
|
+
for _, descendant in scopeRoot:GetDescendants() do
|
|
322
|
+
if not descendant:IsA("GuiObject") then
|
|
323
|
+
continue
|
|
324
|
+
end
|
|
325
|
+
local resolvedNode = resolveFocusNodeByGuiObject(descendant, {
|
|
326
|
+
allowImplicit = true,
|
|
327
|
+
trapScopeOverride = scopeRecord,
|
|
328
|
+
})
|
|
329
|
+
if resolvedNode then
|
|
330
|
+
return resolvedNode
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
return nil
|
|
334
|
+
end
|
|
335
|
+
local function getBestScopeFallbackNode(scopeRecord)
|
|
336
|
+
local lastFocusedNodeId = scopeRecord.lastFocusedNodeId
|
|
337
|
+
if lastFocusedNodeId ~= nil then
|
|
338
|
+
local lastFocusedNode = getResolvedFocusNode(lastFocusedNodeId, {
|
|
339
|
+
trapScopeOverride = scopeRecord,
|
|
340
|
+
})
|
|
341
|
+
if lastFocusedNode and isInsideRoot(scopeRecord.getRoot(), lastFocusedNode.guiObject) then
|
|
342
|
+
return lastFocusedNode
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
return findFirstRegisteredNodeInScope(scopeRecord) or findFirstFocusableDescendantInScope(scopeRecord)
|
|
346
|
+
end
|
|
347
|
+
local function enforceTrappedFocus(excludingScopeId)
|
|
348
|
+
local trappedScope = getTopTrappedScope(excludingScopeId)
|
|
349
|
+
if not trappedScope then
|
|
350
|
+
syncRobloxSelection()
|
|
351
|
+
return nil
|
|
352
|
+
end
|
|
353
|
+
local currentFocusedNode = if currentFocusedNodeId ~= nil then getResolvedFocusNode(currentFocusedNodeId, {
|
|
354
|
+
trapScopeOverride = trappedScope,
|
|
355
|
+
}) else nil
|
|
356
|
+
if currentFocusedNode then
|
|
357
|
+
syncRobloxSelection()
|
|
358
|
+
return nil
|
|
359
|
+
end
|
|
360
|
+
local fallbackNode = getBestScopeFallbackNode(trappedScope)
|
|
361
|
+
if fallbackNode then
|
|
362
|
+
setCurrentFocusedNode(fallbackNode.record.id)
|
|
363
|
+
return nil
|
|
364
|
+
end
|
|
365
|
+
setCurrentFocusedNode(nil)
|
|
366
|
+
end
|
|
367
|
+
local function isFocusNodeReferenced(nodeId)
|
|
368
|
+
if currentFocusedNodeId == nodeId then
|
|
369
|
+
return true
|
|
370
|
+
end
|
|
371
|
+
for _, scopeRecord in focusScopes do
|
|
372
|
+
local _condition = scopeRecord.lastFocusedNodeId == nodeId
|
|
373
|
+
if not _condition then
|
|
374
|
+
local _result = scopeRecord.restoreSnapshot
|
|
375
|
+
if _result ~= nil then
|
|
376
|
+
_result = _result.nodeId
|
|
377
|
+
end
|
|
378
|
+
_condition = _result == nodeId
|
|
379
|
+
end
|
|
380
|
+
if _condition then
|
|
381
|
+
return true
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
return false
|
|
385
|
+
end
|
|
386
|
+
local function pruneImplicitFocusNodes()
|
|
387
|
+
for index = #focusNodes - 1, 0, -1 do
|
|
388
|
+
local nodeRecord = focusNodes[index + 1]
|
|
389
|
+
if not nodeRecord.implicit then
|
|
390
|
+
continue
|
|
391
|
+
end
|
|
392
|
+
if isLiveGuiObject(nodeRecord.getGuiObject()) or isFocusNodeReferenced(nodeRecord.id) then
|
|
393
|
+
continue
|
|
394
|
+
end
|
|
395
|
+
local _index = index
|
|
396
|
+
table.remove(focusNodes, _index + 1)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
local function findBridgeRestoreNode()
|
|
400
|
+
local selectedObject = GuiService.SelectedObject
|
|
401
|
+
return resolveFocusNodeByGuiObject(selectedObject, {
|
|
402
|
+
allowImplicit = true,
|
|
403
|
+
})
|
|
404
|
+
end
|
|
405
|
+
local getFocusedNode
|
|
406
|
+
local function getCurrentFocusGuiObject()
|
|
407
|
+
local focusedNode = getFocusedNode()
|
|
408
|
+
local _result = focusedNode
|
|
409
|
+
if _result ~= nil then
|
|
410
|
+
_result = _result.getGuiObject()
|
|
411
|
+
end
|
|
412
|
+
local _condition = _result
|
|
413
|
+
if _condition == nil then
|
|
414
|
+
_condition = GuiService.SelectedObject
|
|
415
|
+
end
|
|
416
|
+
return _condition
|
|
417
|
+
end
|
|
418
|
+
local focusNode, focusGuiObject
|
|
419
|
+
local function restoreScopeFocus(scopeRecord)
|
|
420
|
+
local _snapshotNodeId = scopeRecord.restoreSnapshot
|
|
421
|
+
if _snapshotNodeId ~= nil then
|
|
422
|
+
_snapshotNodeId = _snapshotNodeId.nodeId
|
|
423
|
+
end
|
|
424
|
+
local snapshotNodeId = _snapshotNodeId
|
|
425
|
+
local restoreGuiObject = scopeRecord.restoreGuiObject
|
|
426
|
+
scopeRecord.restoreSnapshot = nil
|
|
427
|
+
scopeRecord.restoreGuiObject = nil
|
|
428
|
+
if snapshotNodeId ~= nil then
|
|
429
|
+
local restoredGuiObject = focusNode(snapshotNodeId)
|
|
430
|
+
if restoredGuiObject then
|
|
431
|
+
return restoredGuiObject
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
if restoreGuiObject then
|
|
435
|
+
local restoredGuiObject = focusGuiObject(restoreGuiObject)
|
|
436
|
+
if restoredGuiObject then
|
|
437
|
+
return restoredGuiObject
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
local ancestorScope = getFocusScopeRecord(scopeRecord.parentScopeId)
|
|
441
|
+
while ancestorScope do
|
|
442
|
+
local fallbackNode = getBestScopeFallbackNode(ancestorScope)
|
|
443
|
+
if fallbackNode then
|
|
444
|
+
local restoredGuiObject = focusNode(fallbackNode.record.id)
|
|
445
|
+
if restoredGuiObject then
|
|
446
|
+
return restoredGuiObject
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
ancestorScope = getFocusScopeRecord(ancestorScope.parentScopeId)
|
|
450
|
+
end
|
|
451
|
+
local topTrappedScope = getTopTrappedScope(scopeRecord.id)
|
|
452
|
+
if topTrappedScope then
|
|
453
|
+
local fallbackNode = getBestScopeFallbackNode(topTrappedScope)
|
|
454
|
+
if fallbackNode then
|
|
455
|
+
local restoredGuiObject = focusNode(fallbackNode.record.id)
|
|
456
|
+
if restoredGuiObject then
|
|
457
|
+
return restoredGuiObject
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
setCurrentFocusedNode(nil)
|
|
462
|
+
return nil
|
|
463
|
+
end
|
|
464
|
+
local function handleExternalSelectedObjectChange()
|
|
465
|
+
if bridgeWriteDepth > 0 then
|
|
466
|
+
return nil
|
|
467
|
+
end
|
|
468
|
+
local selectedObject = GuiService.SelectedObject
|
|
469
|
+
local resolvedNode = resolveFocusNodeByGuiObject(selectedObject, {
|
|
470
|
+
allowImplicit = true,
|
|
471
|
+
})
|
|
472
|
+
if resolvedNode then
|
|
473
|
+
setCurrentFocusedNode(resolvedNode.record.id)
|
|
474
|
+
return nil
|
|
475
|
+
end
|
|
476
|
+
enforceTrappedFocus()
|
|
477
|
+
end
|
|
478
|
+
local function startExternalSelectionListener()
|
|
479
|
+
if selectedObjectConnection then
|
|
480
|
+
return nil
|
|
481
|
+
end
|
|
482
|
+
selectedObjectConnection = GuiService:GetPropertyChangedSignal("SelectedObject"):Connect(function()
|
|
483
|
+
handleExternalSelectedObjectChange()
|
|
484
|
+
end)
|
|
485
|
+
end
|
|
486
|
+
local function stopExternalSelectionListener()
|
|
487
|
+
if not selectedObjectConnection then
|
|
488
|
+
return nil
|
|
489
|
+
end
|
|
490
|
+
selectedObjectConnection:Disconnect()
|
|
491
|
+
selectedObjectConnection = nil
|
|
492
|
+
end
|
|
493
|
+
local function syncExternalSelectionListener()
|
|
494
|
+
if externalSelectionConsumerCount > 0 then
|
|
495
|
+
startExternalSelectionListener()
|
|
496
|
+
else
|
|
497
|
+
stopExternalSelectionListener()
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
local function registerFocusNode(params)
|
|
501
|
+
nextFocusNodeId += 1
|
|
502
|
+
nextFocusOrder += 1
|
|
503
|
+
local nodeRecord = {
|
|
504
|
+
id = nextFocusNodeId,
|
|
505
|
+
scopeId = params.scopeId,
|
|
506
|
+
implicit = false,
|
|
507
|
+
order = nextFocusOrder,
|
|
508
|
+
getGuiObject = params.getGuiObject,
|
|
509
|
+
getDisabled = params.getDisabled or (function()
|
|
510
|
+
return false
|
|
511
|
+
end),
|
|
512
|
+
getVisible = params.getVisible or (function()
|
|
513
|
+
return nil
|
|
514
|
+
end),
|
|
515
|
+
getSyncToRoblox = params.getSyncToRoblox or (function()
|
|
516
|
+
return true
|
|
517
|
+
end),
|
|
518
|
+
}
|
|
519
|
+
table.insert(focusNodes, nodeRecord)
|
|
520
|
+
local guiObject = nodeRecord.getGuiObject()
|
|
521
|
+
local currentFocusedNode = if currentFocusedNodeId ~= nil then getFocusNodeRecord(currentFocusedNodeId) else nil
|
|
522
|
+
local _result = currentFocusedNode
|
|
523
|
+
if _result ~= nil then
|
|
524
|
+
_result = _result.implicit
|
|
525
|
+
end
|
|
526
|
+
local _condition = _result
|
|
527
|
+
if _condition then
|
|
528
|
+
_condition = currentFocusedNode.getGuiObject() == guiObject
|
|
529
|
+
end
|
|
530
|
+
if _condition then
|
|
531
|
+
currentFocusedNodeId = nodeRecord.id
|
|
532
|
+
for _, scopeRecord in focusScopes do
|
|
533
|
+
if scopeRecord.lastFocusedNodeId == currentFocusedNode.id then
|
|
534
|
+
scopeRecord.lastFocusedNodeId = nodeRecord.id
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
enforceTrappedFocus()
|
|
539
|
+
return nodeRecord.id
|
|
540
|
+
end
|
|
541
|
+
local function createFocusScopeId()
|
|
542
|
+
nextFocusScopeId += 1
|
|
543
|
+
return nextFocusScopeId
|
|
544
|
+
end
|
|
545
|
+
local function unregisterFocusNode(nodeId)
|
|
546
|
+
local nodeIndex = findFocusNodeIndex(nodeId)
|
|
547
|
+
if nodeIndex < 0 then
|
|
548
|
+
return nil
|
|
549
|
+
end
|
|
550
|
+
table.remove(focusNodes, nodeIndex + 1)
|
|
551
|
+
for _, scopeRecord in focusScopes do
|
|
552
|
+
if scopeRecord.lastFocusedNodeId == nodeId then
|
|
553
|
+
scopeRecord.lastFocusedNodeId = nil
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
if currentFocusedNodeId == nodeId then
|
|
557
|
+
currentFocusedNodeId = nil
|
|
558
|
+
end
|
|
559
|
+
pruneImplicitFocusNodes()
|
|
560
|
+
enforceTrappedFocus()
|
|
561
|
+
end
|
|
562
|
+
local syncFocusScope
|
|
563
|
+
local function registerFocusScope(scopeId, params)
|
|
564
|
+
nextFocusOrder += 1
|
|
565
|
+
local scopeRecord = {
|
|
566
|
+
id = scopeId,
|
|
567
|
+
parentScopeId = params.parentScopeId,
|
|
568
|
+
order = nextFocusOrder,
|
|
569
|
+
wasActive = false,
|
|
570
|
+
getRoot = params.getRoot,
|
|
571
|
+
getActive = params.getActive,
|
|
572
|
+
getTrapped = params.getTrapped,
|
|
573
|
+
getRestoreFocus = params.getRestoreFocus or (function()
|
|
574
|
+
return true
|
|
575
|
+
end),
|
|
576
|
+
getLayerOrder = params.getLayerOrder or (function()
|
|
577
|
+
return nil
|
|
578
|
+
end),
|
|
579
|
+
}
|
|
580
|
+
table.insert(focusScopes, scopeRecord)
|
|
581
|
+
syncFocusScope(scopeRecord.id)
|
|
582
|
+
return scopeRecord.id
|
|
583
|
+
end
|
|
584
|
+
local captureRestoreSnapshot
|
|
585
|
+
function syncFocusScope(scopeId)
|
|
586
|
+
local scopeRecord = getFocusScopeRecord(scopeId)
|
|
587
|
+
if not scopeRecord then
|
|
588
|
+
return nil
|
|
589
|
+
end
|
|
590
|
+
local nextActive = scopeRecord.getActive()
|
|
591
|
+
if nextActive and not scopeRecord.wasActive then
|
|
592
|
+
scopeRecord.restoreSnapshot = if scopeRecord.getRestoreFocus() then captureRestoreSnapshot() else nil
|
|
593
|
+
scopeRecord.restoreGuiObject = if scopeRecord.getRestoreFocus() then getCurrentFocusGuiObject() else nil
|
|
594
|
+
scopeRecord.wasActive = true
|
|
595
|
+
if scopeRecord.getTrapped() then
|
|
596
|
+
enforceTrappedFocus()
|
|
597
|
+
end
|
|
598
|
+
elseif not nextActive and scopeRecord.wasActive then
|
|
599
|
+
scopeRecord.wasActive = false
|
|
600
|
+
local scopeIndex = findFocusScopeIndex(scopeId)
|
|
601
|
+
if scopeIndex >= 0 then
|
|
602
|
+
table.remove(focusScopes, scopeIndex + 1)
|
|
603
|
+
end
|
|
604
|
+
if scopeRecord.getRestoreFocus() then
|
|
605
|
+
restoreScopeFocus(scopeRecord)
|
|
606
|
+
else
|
|
607
|
+
scopeRecord.restoreSnapshot = nil
|
|
608
|
+
scopeRecord.restoreGuiObject = nil
|
|
609
|
+
enforceTrappedFocus(scopeRecord.id)
|
|
610
|
+
end
|
|
611
|
+
table.insert(focusScopes, scopeRecord)
|
|
612
|
+
elseif nextActive and scopeRecord.getTrapped() then
|
|
613
|
+
enforceTrappedFocus()
|
|
614
|
+
end
|
|
615
|
+
pruneImplicitFocusNodes()
|
|
616
|
+
end
|
|
617
|
+
local function unregisterFocusScope(scopeId)
|
|
618
|
+
local scopeIndex = findFocusScopeIndex(scopeId)
|
|
619
|
+
if scopeIndex < 0 then
|
|
620
|
+
return nil
|
|
621
|
+
end
|
|
622
|
+
local scopeRecord = focusScopes[scopeIndex + 1]
|
|
623
|
+
table.remove(focusScopes, scopeIndex + 1)
|
|
624
|
+
if scopeRecord.wasActive and scopeRecord.getRestoreFocus() then
|
|
625
|
+
restoreScopeFocus(scopeRecord)
|
|
626
|
+
else
|
|
627
|
+
enforceTrappedFocus(scopeId)
|
|
628
|
+
end
|
|
629
|
+
pruneImplicitFocusNodes()
|
|
630
|
+
end
|
|
631
|
+
local function retainExternalFocusBridge()
|
|
632
|
+
externalSelectionConsumerCount += 1
|
|
633
|
+
syncExternalSelectionListener()
|
|
634
|
+
end
|
|
635
|
+
local function releaseExternalFocusBridge()
|
|
636
|
+
externalSelectionConsumerCount = math.max(0, externalSelectionConsumerCount - 1)
|
|
637
|
+
syncExternalSelectionListener()
|
|
638
|
+
end
|
|
639
|
+
local function canFocusNode(nodeId)
|
|
640
|
+
return getResolvedFocusNode(nodeId) ~= nil
|
|
641
|
+
end
|
|
642
|
+
function focusNode(nodeId)
|
|
643
|
+
local resolvedNode = getResolvedFocusNode(nodeId)
|
|
644
|
+
if not resolvedNode then
|
|
645
|
+
return nil
|
|
646
|
+
end
|
|
647
|
+
return setCurrentFocusedNode(resolvedNode.record.id)
|
|
648
|
+
end
|
|
649
|
+
function focusGuiObject(guiObject)
|
|
650
|
+
local resolvedNode = resolveFocusNodeByGuiObject(guiObject, {
|
|
651
|
+
allowImplicit = true,
|
|
652
|
+
})
|
|
653
|
+
if not resolvedNode then
|
|
654
|
+
return nil
|
|
655
|
+
end
|
|
656
|
+
return setCurrentFocusedNode(resolvedNode.record.id)
|
|
657
|
+
end
|
|
658
|
+
local function clearFocus()
|
|
659
|
+
setCurrentFocusedNode(nil)
|
|
660
|
+
enforceTrappedFocus()
|
|
661
|
+
end
|
|
662
|
+
function getFocusedNode()
|
|
663
|
+
if currentFocusedNodeId == nil then
|
|
664
|
+
return nil
|
|
665
|
+
end
|
|
666
|
+
local resolvedNode = getResolvedFocusNode(currentFocusedNodeId)
|
|
667
|
+
local _result = resolvedNode
|
|
668
|
+
if _result ~= nil then
|
|
669
|
+
_result = _result.record
|
|
670
|
+
end
|
|
671
|
+
return _result
|
|
672
|
+
end
|
|
673
|
+
local function getFocusedGuiObject()
|
|
674
|
+
local focusedNode = getFocusedNode()
|
|
675
|
+
local _result = focusedNode
|
|
676
|
+
if _result ~= nil then
|
|
677
|
+
_result = _result.getGuiObject()
|
|
678
|
+
end
|
|
679
|
+
return _result
|
|
680
|
+
end
|
|
681
|
+
function captureRestoreSnapshot()
|
|
682
|
+
local focusedNode = getFocusedNode()
|
|
683
|
+
if focusedNode then
|
|
684
|
+
return {
|
|
685
|
+
nodeId = focusedNode.id,
|
|
686
|
+
}
|
|
687
|
+
end
|
|
688
|
+
local bridgeNode = findBridgeRestoreNode()
|
|
689
|
+
local _object = {}
|
|
690
|
+
local _left = "nodeId"
|
|
691
|
+
local _result = bridgeNode
|
|
692
|
+
if _result ~= nil then
|
|
693
|
+
_result = _result.record.id
|
|
694
|
+
end
|
|
695
|
+
_object[_left] = _result
|
|
696
|
+
return _object
|
|
697
|
+
end
|
|
698
|
+
local function restoreSnapshot(snapshot)
|
|
699
|
+
local _snapshotNodeId = snapshot
|
|
700
|
+
if _snapshotNodeId ~= nil then
|
|
701
|
+
_snapshotNodeId = _snapshotNodeId.nodeId
|
|
702
|
+
end
|
|
703
|
+
local snapshotNodeId = _snapshotNodeId
|
|
704
|
+
if snapshotNodeId == nil then
|
|
705
|
+
clearFocus()
|
|
706
|
+
return nil
|
|
707
|
+
end
|
|
708
|
+
return focusNode(snapshotNodeId)
|
|
709
|
+
end
|
|
710
|
+
return {
|
|
711
|
+
registerFocusNode = registerFocusNode,
|
|
712
|
+
createFocusScopeId = createFocusScopeId,
|
|
713
|
+
unregisterFocusNode = unregisterFocusNode,
|
|
714
|
+
registerFocusScope = registerFocusScope,
|
|
715
|
+
syncFocusScope = syncFocusScope,
|
|
716
|
+
unregisterFocusScope = unregisterFocusScope,
|
|
717
|
+
retainExternalFocusBridge = retainExternalFocusBridge,
|
|
718
|
+
releaseExternalFocusBridge = releaseExternalFocusBridge,
|
|
719
|
+
canFocusNode = canFocusNode,
|
|
720
|
+
focusNode = focusNode,
|
|
721
|
+
focusGuiObject = focusGuiObject,
|
|
722
|
+
clearFocus = clearFocus,
|
|
723
|
+
getFocusedNode = getFocusedNode,
|
|
724
|
+
getFocusedGuiObject = getFocusedGuiObject,
|
|
725
|
+
captureRestoreSnapshot = captureRestoreSnapshot,
|
|
726
|
+
restoreSnapshot = restoreSnapshot,
|
|
727
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "../react";
|
|
2
|
+
export type UseFocusNodeOptions = {
|
|
3
|
+
ref: React.MutableRefObject<GuiObject | undefined>;
|
|
4
|
+
scopeId?: number;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
getDisabled?: () => boolean;
|
|
7
|
+
getVisible?: () => boolean | undefined;
|
|
8
|
+
syncToRoblox?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function useFocusNode(options: UseFocusNodeOptions): React.MutableRefObject<number | undefined>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local React = TS.import(script, script.Parent.Parent, "react").default
|
|
4
|
+
local useFocusScopeId = TS.import(script, script.Parent, "context").useFocusScopeId
|
|
5
|
+
local _focusManager = TS.import(script, script.Parent, "focusManager")
|
|
6
|
+
local registerFocusNode = _focusManager.registerFocusNode
|
|
7
|
+
local unregisterFocusNode = _focusManager.unregisterFocusNode
|
|
8
|
+
local function useLatest(value)
|
|
9
|
+
local ref = React.useRef(value)
|
|
10
|
+
React.useEffect(function()
|
|
11
|
+
ref.current = value
|
|
12
|
+
end, { value })
|
|
13
|
+
return ref
|
|
14
|
+
end
|
|
15
|
+
local function useFocusNode(options)
|
|
16
|
+
local inheritedScopeId = useFocusScopeId()
|
|
17
|
+
local _condition = options.scopeId
|
|
18
|
+
if _condition == nil then
|
|
19
|
+
_condition = inheritedScopeId
|
|
20
|
+
end
|
|
21
|
+
local scopeId = _condition
|
|
22
|
+
local nodeIdRef = React.useRef()
|
|
23
|
+
local disabledRef = useLatest(options.disabled == true)
|
|
24
|
+
local getDisabledRef = useLatest(options.getDisabled)
|
|
25
|
+
local getVisibleRef = useLatest(options.getVisible)
|
|
26
|
+
local syncToRobloxRef = useLatest(options.syncToRoblox ~= false)
|
|
27
|
+
React.useEffect(function()
|
|
28
|
+
local nodeId = registerFocusNode({
|
|
29
|
+
scopeId = scopeId,
|
|
30
|
+
getGuiObject = function()
|
|
31
|
+
return options.ref.current
|
|
32
|
+
end,
|
|
33
|
+
getDisabled = function()
|
|
34
|
+
local _condition_1 = disabledRef.current
|
|
35
|
+
if not _condition_1 then
|
|
36
|
+
local _result = getDisabledRef.current
|
|
37
|
+
if _result ~= nil then
|
|
38
|
+
_result = _result()
|
|
39
|
+
end
|
|
40
|
+
_condition_1 = _result == true
|
|
41
|
+
end
|
|
42
|
+
return _condition_1
|
|
43
|
+
end,
|
|
44
|
+
getVisible = function()
|
|
45
|
+
local _result = getVisibleRef.current
|
|
46
|
+
if _result ~= nil then
|
|
47
|
+
_result = _result()
|
|
48
|
+
end
|
|
49
|
+
return _result
|
|
50
|
+
end,
|
|
51
|
+
getSyncToRoblox = function()
|
|
52
|
+
return syncToRobloxRef.current
|
|
53
|
+
end,
|
|
54
|
+
})
|
|
55
|
+
nodeIdRef.current = nodeId
|
|
56
|
+
return function()
|
|
57
|
+
unregisterFocusNode(nodeId)
|
|
58
|
+
nodeIdRef.current = nil
|
|
59
|
+
end
|
|
60
|
+
end, { options.ref, scopeId })
|
|
61
|
+
return nodeIdRef
|
|
62
|
+
end
|
|
63
|
+
return {
|
|
64
|
+
useFocusNode = useFocusNode,
|
|
65
|
+
}
|
package/out/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export * from "./context";
|
|
2
|
+
export * from "./focus/context";
|
|
3
|
+
export * from "./focus/focusManager";
|
|
4
|
+
export * from "./focus/useFocusNode";
|
|
5
|
+
export * from "./orderedSelection";
|
|
2
6
|
export { default as React } from "./react";
|
|
3
7
|
export { default as ReactRoblox } from "./reactRoblox";
|
|
4
8
|
export * from "./refs";
|
package/out/init.luau
CHANGED
|
@@ -4,6 +4,18 @@ local exports = {}
|
|
|
4
4
|
for _k, _v in TS.import(script, script, "context") or {} do
|
|
5
5
|
exports[_k] = _v
|
|
6
6
|
end
|
|
7
|
+
for _k, _v in TS.import(script, script, "focus", "context") or {} do
|
|
8
|
+
exports[_k] = _v
|
|
9
|
+
end
|
|
10
|
+
for _k, _v in TS.import(script, script, "focus", "focusManager") or {} do
|
|
11
|
+
exports[_k] = _v
|
|
12
|
+
end
|
|
13
|
+
for _k, _v in TS.import(script, script, "focus", "useFocusNode") or {} do
|
|
14
|
+
exports[_k] = _v
|
|
15
|
+
end
|
|
16
|
+
for _k, _v in TS.import(script, script, "orderedSelection") or {} do
|
|
17
|
+
exports[_k] = _v
|
|
18
|
+
end
|
|
7
19
|
exports.React = TS.import(script, script, "react").default
|
|
8
20
|
exports.ReactRoblox = TS.import(script, script, "reactRoblox").default
|
|
9
21
|
for _k, _v in TS.import(script, script, "refs") or {} do
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type React from "@rbxts/react";
|
|
2
|
+
export type OrderedSelectionDirection = -1 | 1;
|
|
3
|
+
export type OrderedSelectionEntry = {
|
|
4
|
+
id: number;
|
|
5
|
+
order: number;
|
|
6
|
+
ref: React.MutableRefObject<GuiObject | undefined>;
|
|
7
|
+
getDisabled?: () => boolean;
|
|
8
|
+
getVisible?: () => boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function getOrderedSelectionEntries<T extends OrderedSelectionEntry>(entries: Array<T>): T[];
|
|
11
|
+
export declare function isOrderedSelectionEntryAvailable(entry: OrderedSelectionEntry): boolean;
|
|
12
|
+
export declare function findOrderedSelectionEntry<T extends OrderedSelectionEntry>(entries: Array<T>, predicate: (entry: T) => boolean): T | undefined;
|
|
13
|
+
export declare function getCurrentOrderedSelectionEntry<T extends OrderedSelectionEntry>(entries: Array<T>): T | undefined;
|
|
14
|
+
export declare function getFirstOrderedSelectionEntry<T extends OrderedSelectionEntry>(entries: Array<T>): T | undefined;
|
|
15
|
+
export declare function getRelativeOrderedSelectionEntry<T extends OrderedSelectionEntry>(entries: Array<T>, currentId: number | undefined, direction: OrderedSelectionDirection): T | undefined;
|
|
16
|
+
export declare function focusOrderedSelectionEntry(entry: OrderedSelectionEntry | undefined): GuiObject | undefined;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _focusManager = TS.import(script, script.Parent, "focus", "focusManager")
|
|
4
|
+
local focusManagedGuiObject = _focusManager.focusGuiObject
|
|
5
|
+
local getFocusedGuiObject = _focusManager.getFocusedGuiObject
|
|
6
|
+
local function isEntryVisible(entry)
|
|
7
|
+
local target = entry.ref.current
|
|
8
|
+
if not target then
|
|
9
|
+
return false
|
|
10
|
+
end
|
|
11
|
+
if entry.getVisible and not entry.getVisible() then
|
|
12
|
+
return false
|
|
13
|
+
end
|
|
14
|
+
return target.Visible
|
|
15
|
+
end
|
|
16
|
+
local function getOrderedSelectionEntries(entries)
|
|
17
|
+
local _array = {}
|
|
18
|
+
local _length = #_array
|
|
19
|
+
table.move(entries, 1, #entries, _length + 1, _array)
|
|
20
|
+
local ordered = _array
|
|
21
|
+
table.sort(ordered, function(left, right)
|
|
22
|
+
return left.order < right.order
|
|
23
|
+
end)
|
|
24
|
+
return ordered
|
|
25
|
+
end
|
|
26
|
+
local function isOrderedSelectionEntryAvailable(entry)
|
|
27
|
+
local target = entry.ref.current
|
|
28
|
+
if not target then
|
|
29
|
+
return false
|
|
30
|
+
end
|
|
31
|
+
local _result = entry.getDisabled
|
|
32
|
+
if _result ~= nil then
|
|
33
|
+
_result = _result()
|
|
34
|
+
end
|
|
35
|
+
if _result == true then
|
|
36
|
+
return false
|
|
37
|
+
end
|
|
38
|
+
if not isEntryVisible(entry) then
|
|
39
|
+
return false
|
|
40
|
+
end
|
|
41
|
+
return target.Selectable
|
|
42
|
+
end
|
|
43
|
+
local function findOrderedSelectionEntry(entries, predicate)
|
|
44
|
+
local _exp = getOrderedSelectionEntries(entries)
|
|
45
|
+
-- ▼ ReadonlyArray.find ▼
|
|
46
|
+
local _callback = function(entry)
|
|
47
|
+
return predicate(entry) and isOrderedSelectionEntryAvailable(entry)
|
|
48
|
+
end
|
|
49
|
+
local _result
|
|
50
|
+
for _i, _v in _exp do
|
|
51
|
+
if _callback(_v, _i - 1, _exp) == true then
|
|
52
|
+
_result = _v
|
|
53
|
+
break
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
-- ▲ ReadonlyArray.find ▲
|
|
57
|
+
return _result
|
|
58
|
+
end
|
|
59
|
+
local function getCurrentOrderedSelectionEntry(entries)
|
|
60
|
+
local current = getFocusedGuiObject()
|
|
61
|
+
if not current then
|
|
62
|
+
return nil
|
|
63
|
+
end
|
|
64
|
+
local _exp = getOrderedSelectionEntries(entries)
|
|
65
|
+
-- ▼ ReadonlyArray.find ▼
|
|
66
|
+
local _callback = function(entry)
|
|
67
|
+
return entry.ref.current == current and isOrderedSelectionEntryAvailable(entry)
|
|
68
|
+
end
|
|
69
|
+
local _result
|
|
70
|
+
for _i, _v in _exp do
|
|
71
|
+
if _callback(_v, _i - 1, _exp) == true then
|
|
72
|
+
_result = _v
|
|
73
|
+
break
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
-- ▲ ReadonlyArray.find ▲
|
|
77
|
+
return _result
|
|
78
|
+
end
|
|
79
|
+
local function getFirstOrderedSelectionEntry(entries)
|
|
80
|
+
local _exp = getOrderedSelectionEntries(entries)
|
|
81
|
+
-- ▼ ReadonlyArray.find ▼
|
|
82
|
+
local _result
|
|
83
|
+
for _i, _v in _exp do
|
|
84
|
+
if isOrderedSelectionEntryAvailable(_v, _i - 1, _exp) == true then
|
|
85
|
+
_result = _v
|
|
86
|
+
break
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
-- ▲ ReadonlyArray.find ▲
|
|
90
|
+
return _result
|
|
91
|
+
end
|
|
92
|
+
local function getRelativeOrderedSelectionEntry(entries, currentId, direction)
|
|
93
|
+
local _exp = getOrderedSelectionEntries(entries)
|
|
94
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
95
|
+
local _newValue = {}
|
|
96
|
+
local _length = 0
|
|
97
|
+
for _k, _v in _exp do
|
|
98
|
+
if isOrderedSelectionEntryAvailable(_v, _k - 1, _exp) == true then
|
|
99
|
+
_length += 1
|
|
100
|
+
_newValue[_length] = _v
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
104
|
+
local selectableEntries = _newValue
|
|
105
|
+
if #selectableEntries == 0 then
|
|
106
|
+
return nil
|
|
107
|
+
end
|
|
108
|
+
local _result
|
|
109
|
+
if currentId ~= nil then
|
|
110
|
+
-- ▼ ReadonlyArray.findIndex ▼
|
|
111
|
+
local _callback = function(entry)
|
|
112
|
+
return entry.id == currentId
|
|
113
|
+
end
|
|
114
|
+
local _result_1 = -1
|
|
115
|
+
for _i, _v in selectableEntries do
|
|
116
|
+
if _callback(_v, _i - 1, selectableEntries) == true then
|
|
117
|
+
_result_1 = _i - 1
|
|
118
|
+
break
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
-- ▲ ReadonlyArray.findIndex ▲
|
|
122
|
+
_result = _result_1
|
|
123
|
+
else
|
|
124
|
+
_result = -1
|
|
125
|
+
end
|
|
126
|
+
local currentIndex = _result
|
|
127
|
+
if currentIndex == -1 then
|
|
128
|
+
return if direction > 0 then selectableEntries[1] else selectableEntries[#selectableEntries]
|
|
129
|
+
end
|
|
130
|
+
local nextIndex = math.clamp(currentIndex + direction, 0, #selectableEntries - 1)
|
|
131
|
+
return selectableEntries[nextIndex + 1]
|
|
132
|
+
end
|
|
133
|
+
local function focusOrderedSelectionEntry(entry)
|
|
134
|
+
local _result = entry
|
|
135
|
+
if _result ~= nil then
|
|
136
|
+
_result = _result.ref.current
|
|
137
|
+
end
|
|
138
|
+
return focusManagedGuiObject(_result)
|
|
139
|
+
end
|
|
140
|
+
return {
|
|
141
|
+
getOrderedSelectionEntries = getOrderedSelectionEntries,
|
|
142
|
+
isOrderedSelectionEntryAvailable = isOrderedSelectionEntryAvailable,
|
|
143
|
+
findOrderedSelectionEntry = findOrderedSelectionEntry,
|
|
144
|
+
getCurrentOrderedSelectionEntry = getCurrentOrderedSelectionEntry,
|
|
145
|
+
getFirstOrderedSelectionEntry = getFirstOrderedSelectionEntry,
|
|
146
|
+
getRelativeOrderedSelectionEntry = getRelativeOrderedSelectionEntry,
|
|
147
|
+
focusOrderedSelectionEntry = focusOrderedSelectionEntry,
|
|
148
|
+
}
|