@lattice-ui/focus 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/README.md +1 -2
- package/out/FocusScope/FocusScope.luau +61 -146
- package/out/index.d.ts +0 -2
- package/out/init.luau +0 -6
- package/package.json +2 -2
- package/out/RovingFocus/RovingFocusGroup.d.ts +0 -3
- package/out/RovingFocus/RovingFocusGroup.luau +0 -196
- package/out/RovingFocus/RovingFocusItem.d.ts +0 -3
- package/out/RovingFocus/RovingFocusItem.luau +0 -66
- package/out/RovingFocus/context.d.ts +0 -3
- package/out/RovingFocus/context.luau +0 -10
- package/out/RovingFocus/roving.d.ts +0 -4
- package/out/RovingFocus/roving.luau +0 -85
- package/out/RovingFocus/types.d.ts +0 -25
- package/out/RovingFocus/types.luau +0 -2
- package/out/internals/guiSelection.d.ts +0 -3
- package/out/internals/guiSelection.luau +0 -15
package/README.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# @lattice-ui/focus
|
|
2
2
|
|
|
3
|
-
Focus
|
|
3
|
+
Focus management primitives for Roblox UI.
|
|
4
4
|
|
|
5
5
|
## Current status
|
|
6
6
|
|
|
7
|
-
- `RovingFocusGroup` and `RovingFocusItem` provide arrow/Home/End navigation across registered selectable items.
|
|
8
7
|
- `FocusScope` can trap `GuiService.SelectedObject` and restore captured focus on scope teardown.
|
|
9
8
|
|
|
10
9
|
## FocusScope behavior
|
|
@@ -1,59 +1,33 @@
|
|
|
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 createFocusScopeId = _core.createFocusScopeId
|
|
5
|
+
local FocusScopeProvider = _core.FocusScopeProvider
|
|
4
6
|
local React = _core.React
|
|
7
|
+
local registerFocusScope = _core.registerFocusScope
|
|
8
|
+
local releaseExternalFocusBridge = _core.releaseExternalFocusBridge
|
|
9
|
+
local retainExternalFocusBridge = _core.retainExternalFocusBridge
|
|
5
10
|
local Slot = _core.Slot
|
|
6
|
-
local
|
|
7
|
-
local
|
|
8
|
-
local
|
|
9
|
-
local
|
|
10
|
-
local captureFocus = _focusManager.captureFocus
|
|
11
|
-
local GuiService = _focusManager.GuiService
|
|
12
|
-
local restoreFocus = _focusManager.restoreFocus
|
|
13
|
-
local _scopeStack = TS.import(script, script.Parent, "scopeStack")
|
|
14
|
-
local isTopTrappedScope = _scopeStack.isTopTrappedScope
|
|
15
|
-
local registerTrappedScope = _scopeStack.registerTrappedScope
|
|
16
|
-
local unregisterTrappedScope = _scopeStack.unregisterTrappedScope
|
|
17
|
-
local nextScopeId = 0
|
|
11
|
+
local syncFocusScope = _core.syncFocusScope
|
|
12
|
+
local unregisterFocusScope = _core.unregisterFocusScope
|
|
13
|
+
local useFocusLayerOrder = _core.useFocusLayerOrder
|
|
14
|
+
local useFocusScopeId = _core.useFocusScopeId
|
|
18
15
|
local function toGuiObject(instance)
|
|
19
16
|
if not instance or not instance:IsA("GuiObject") then
|
|
20
17
|
return nil
|
|
21
18
|
end
|
|
22
19
|
return instance
|
|
23
20
|
end
|
|
24
|
-
local function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
local function isInsideScope(scopeRoot, guiObject)
|
|
31
|
-
if not isLiveGuiObject(scopeRoot) or not isLiveGuiObject(guiObject) then
|
|
32
|
-
return false
|
|
33
|
-
end
|
|
34
|
-
return guiObject == scopeRoot or guiObject:IsDescendantOf(scopeRoot)
|
|
35
|
-
end
|
|
36
|
-
local function findFirstFocusableInScope(scopeRoot)
|
|
37
|
-
if isFocusable(scopeRoot) then
|
|
38
|
-
return scopeRoot
|
|
39
|
-
end
|
|
40
|
-
for _, descendant in scopeRoot:GetDescendants() do
|
|
41
|
-
if descendant:IsA("GuiObject") and isFocusable(descendant) then
|
|
42
|
-
return descendant
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
return nil
|
|
46
|
-
end
|
|
47
|
-
local function getFocusableRestoreTarget(snapshot)
|
|
48
|
-
if not snapshot then
|
|
49
|
-
return nil
|
|
50
|
-
end
|
|
51
|
-
if not isFocusable(snapshot) then
|
|
52
|
-
return nil
|
|
53
|
-
end
|
|
54
|
-
return snapshot
|
|
21
|
+
local function useLatest(value)
|
|
22
|
+
local ref = React.useRef(value)
|
|
23
|
+
React.useEffect(function()
|
|
24
|
+
ref.current = value
|
|
25
|
+
end, { value })
|
|
26
|
+
return ref
|
|
55
27
|
end
|
|
56
28
|
local function FocusScope(props)
|
|
29
|
+
local parentScopeId = useFocusScopeId()
|
|
30
|
+
local layerOrder = useFocusLayerOrder()
|
|
57
31
|
local _condition = props.active
|
|
58
32
|
if _condition == nil then
|
|
59
33
|
_condition = true
|
|
@@ -63,117 +37,56 @@ local function FocusScope(props)
|
|
|
63
37
|
local shouldRestoreFocus = props.restoreFocus ~= false
|
|
64
38
|
local scopeIdRef = React.useRef(0)
|
|
65
39
|
if scopeIdRef.current == 0 then
|
|
66
|
-
|
|
67
|
-
scopeIdRef.current = nextScopeId
|
|
40
|
+
scopeIdRef.current = createFocusScopeId()
|
|
68
41
|
end
|
|
69
42
|
local scopeRootRef = React.useRef()
|
|
70
|
-
local
|
|
71
|
-
local
|
|
72
|
-
local
|
|
73
|
-
local
|
|
74
|
-
React.useEffect(function()
|
|
75
|
-
shouldRestoreFocusRef.current = shouldRestoreFocus
|
|
76
|
-
end, { shouldRestoreFocus })
|
|
77
|
-
local updateLastFocusedInside = React.useCallback(function()
|
|
78
|
-
local scopeRoot = scopeRootRef.current
|
|
79
|
-
if not scopeRoot then
|
|
80
|
-
return nil
|
|
81
|
-
end
|
|
82
|
-
local selectedObject = getSelectedGuiObject()
|
|
83
|
-
if not selectedObject or not isInsideScope(scopeRoot, selectedObject) then
|
|
84
|
-
return nil
|
|
85
|
-
end
|
|
86
|
-
if isFocusable(selectedObject) then
|
|
87
|
-
lastFocusedInsideRef.current = selectedObject
|
|
88
|
-
end
|
|
89
|
-
end, {})
|
|
90
|
-
local resolveFallbackTarget = React.useCallback(function()
|
|
91
|
-
local scopeRoot = scopeRootRef.current
|
|
92
|
-
if not isLiveGuiObject(scopeRoot) then
|
|
93
|
-
return nil
|
|
94
|
-
end
|
|
95
|
-
local previousFocus = lastFocusedInsideRef.current
|
|
96
|
-
if previousFocus and isInsideScope(scopeRoot, previousFocus) and isFocusable(previousFocus) then
|
|
97
|
-
return previousFocus
|
|
98
|
-
end
|
|
99
|
-
return findFirstFocusableInScope(scopeRoot)
|
|
100
|
-
end, {})
|
|
101
|
-
local enforceFocusTrap = React.useCallback(function()
|
|
102
|
-
if not active or not trapped then
|
|
103
|
-
return nil
|
|
104
|
-
end
|
|
105
|
-
if not isTopTrappedScope(scopeIdRef.current) then
|
|
106
|
-
return nil
|
|
107
|
-
end
|
|
108
|
-
local scopeRoot = scopeRootRef.current
|
|
109
|
-
if not isLiveGuiObject(scopeRoot) then
|
|
110
|
-
return nil
|
|
111
|
-
end
|
|
112
|
-
local selectedObject = getSelectedGuiObject()
|
|
113
|
-
if selectedObject and isInsideScope(scopeRoot, selectedObject) then
|
|
114
|
-
if isFocusable(selectedObject) then
|
|
115
|
-
lastFocusedInsideRef.current = selectedObject
|
|
116
|
-
end
|
|
117
|
-
return nil
|
|
118
|
-
end
|
|
119
|
-
local fallbackTarget = resolveFallbackTarget()
|
|
120
|
-
if not fallbackTarget or fallbackTarget == selectedObject then
|
|
121
|
-
return nil
|
|
122
|
-
end
|
|
123
|
-
isRedirectingRef.current = true
|
|
124
|
-
setSelectedGuiObject(fallbackTarget)
|
|
125
|
-
isRedirectingRef.current = false
|
|
126
|
-
end, { active, resolveFallbackTarget, trapped })
|
|
43
|
+
local activeRef = useLatest(active)
|
|
44
|
+
local trappedRef = useLatest(trapped)
|
|
45
|
+
local restoreFocusRef = useLatest(shouldRestoreFocus)
|
|
46
|
+
local layerOrderRef = useLatest(layerOrder)
|
|
127
47
|
local setScopeRoot = React.useCallback(function(instance)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return nil
|
|
48
|
+
scopeRootRef.current = toGuiObject(instance)
|
|
49
|
+
if scopeIdRef.current ~= 0 then
|
|
50
|
+
syncFocusScope(scopeIdRef.current)
|
|
132
51
|
end
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
52
|
+
end, {})
|
|
53
|
+
React.useEffect(function()
|
|
54
|
+
local scopeId = scopeIdRef.current
|
|
55
|
+
registerFocusScope(scopeId, {
|
|
56
|
+
parentScopeId = parentScopeId,
|
|
57
|
+
getRoot = function()
|
|
58
|
+
return scopeRootRef.current
|
|
59
|
+
end,
|
|
60
|
+
getActive = function()
|
|
61
|
+
return activeRef.current
|
|
62
|
+
end,
|
|
63
|
+
getTrapped = function()
|
|
64
|
+
return trappedRef.current
|
|
65
|
+
end,
|
|
66
|
+
getRestoreFocus = function()
|
|
67
|
+
return restoreFocusRef.current
|
|
68
|
+
end,
|
|
69
|
+
getLayerOrder = function()
|
|
70
|
+
return layerOrderRef.current
|
|
71
|
+
end,
|
|
72
|
+
})
|
|
73
|
+
return function()
|
|
74
|
+
unregisterFocusScope(scopeId)
|
|
136
75
|
end
|
|
137
|
-
end, {
|
|
76
|
+
end, { activeRef, layerOrderRef, parentScopeId, restoreFocusRef, trappedRef })
|
|
77
|
+
React.useEffect(function()
|
|
78
|
+
syncFocusScope(scopeIdRef.current)
|
|
79
|
+
end, { active, layerOrder, shouldRestoreFocus, trapped })
|
|
138
80
|
React.useEffect(function()
|
|
139
81
|
if not active then
|
|
140
|
-
restoreSnapshotRef.current = nil
|
|
141
82
|
return nil
|
|
142
83
|
end
|
|
143
|
-
|
|
144
|
-
restoreSnapshotRef.current = captureFocus()
|
|
145
|
-
else
|
|
146
|
-
restoreSnapshotRef.current = nil
|
|
147
|
-
end
|
|
148
|
-
local currentScopeId = scopeIdRef.current
|
|
149
|
-
if trapped then
|
|
150
|
-
registerTrappedScope(currentScopeId)
|
|
151
|
-
end
|
|
152
|
-
local selectedObjectConnection = GuiService:GetPropertyChangedSignal("SelectedObject"):Connect(function()
|
|
153
|
-
if isRedirectingRef.current then
|
|
154
|
-
return nil
|
|
155
|
-
end
|
|
156
|
-
updateLastFocusedInside()
|
|
157
|
-
if trapped then
|
|
158
|
-
enforceFocusTrap()
|
|
159
|
-
end
|
|
160
|
-
end)
|
|
161
|
-
if trapped then
|
|
162
|
-
enforceFocusTrap()
|
|
163
|
-
end
|
|
84
|
+
retainExternalFocusBridge()
|
|
164
85
|
return function()
|
|
165
|
-
|
|
166
|
-
if trapped then
|
|
167
|
-
unregisterTrappedScope(currentScopeId)
|
|
168
|
-
end
|
|
169
|
-
local restoreTarget = getFocusableRestoreTarget(restoreSnapshotRef.current)
|
|
170
|
-
restoreSnapshotRef.current = nil
|
|
171
|
-
if restoreTarget and shouldRestoreFocusRef.current then
|
|
172
|
-
restoreFocus(restoreTarget)
|
|
173
|
-
end
|
|
86
|
+
releaseExternalFocusBridge()
|
|
174
87
|
end
|
|
175
|
-
end, { active
|
|
176
|
-
if props.asChild then
|
|
88
|
+
end, { active })
|
|
89
|
+
local content = if props.asChild then ((function()
|
|
177
90
|
local child = props.children
|
|
178
91
|
if not React.isValidElement(child) then
|
|
179
92
|
error("[FocusScope] `asChild` requires a child element.")
|
|
@@ -181,14 +94,16 @@ local function FocusScope(props)
|
|
|
181
94
|
return React.createElement(Slot, {
|
|
182
95
|
ref = setScopeRoot,
|
|
183
96
|
}, child)
|
|
184
|
-
end
|
|
185
|
-
return React.createElement("frame", {
|
|
97
|
+
end)()) else (React.createElement("frame", {
|
|
186
98
|
BackgroundTransparency = 1,
|
|
187
99
|
BorderSizePixel = 0,
|
|
188
100
|
Position = UDim2.fromScale(0, 0),
|
|
189
101
|
Size = UDim2.fromScale(1, 1),
|
|
190
102
|
ref = setScopeRoot,
|
|
191
|
-
}, props.children)
|
|
103
|
+
}, props.children))
|
|
104
|
+
return React.createElement(FocusScopeProvider, {
|
|
105
|
+
scopeId = scopeIdRef.current,
|
|
106
|
+
}, content)
|
|
192
107
|
end
|
|
193
108
|
return {
|
|
194
109
|
FocusScope = FocusScope,
|
package/out/index.d.ts
CHANGED
package/out/init.luau
CHANGED
|
@@ -4,10 +4,4 @@ local exports = {}
|
|
|
4
4
|
for _k, _v in TS.import(script, script, "FocusScope", "FocusScope") or {} do
|
|
5
5
|
exports[_k] = _v
|
|
6
6
|
end
|
|
7
|
-
for _k, _v in TS.import(script, script, "RovingFocus", "RovingFocusGroup") or {} do
|
|
8
|
-
exports[_k] = _v
|
|
9
|
-
end
|
|
10
|
-
for _k, _v in TS.import(script, script, "RovingFocus", "RovingFocusItem") or {} do
|
|
11
|
-
exports[_k] = _v
|
|
12
|
-
end
|
|
13
7
|
return exports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lattice-ui/focus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "out/init.luau",
|
|
6
6
|
"types": "out/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"README.md"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@lattice-ui/core": "0.
|
|
12
|
+
"@lattice-ui/core": "0.4.1"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@rbxts/react": "17.3.7-ts.1",
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
-- Compiled with roblox-ts v3.0.0
|
|
2
|
-
local TS = _G[script]
|
|
3
|
-
local React = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).React
|
|
4
|
-
local _guiSelection = TS.import(script, script.Parent.Parent, "internals", "guiSelection")
|
|
5
|
-
local getSelectedGuiObject = _guiSelection.getSelectedGuiObject
|
|
6
|
-
local setSelectedGuiObject = _guiSelection.setSelectedGuiObject
|
|
7
|
-
local UserInputService = _guiSelection.UserInputService
|
|
8
|
-
local RovingFocusProvider = TS.import(script, script.Parent, "context").RovingFocusProvider
|
|
9
|
-
local _roving = TS.import(script, script.Parent, "roving")
|
|
10
|
-
local getFirstEnabledRovingIndex = _roving.getFirstEnabledRovingIndex
|
|
11
|
-
local getLastEnabledRovingIndex = _roving.getLastEnabledRovingIndex
|
|
12
|
-
local getNextRovingIndex = _roving.getNextRovingIndex
|
|
13
|
-
local function findCurrentIndex(items, selectedObject)
|
|
14
|
-
if not selectedObject then
|
|
15
|
-
return -1
|
|
16
|
-
end
|
|
17
|
-
-- ▼ ReadonlyArray.findIndex ▼
|
|
18
|
-
local _callback = function(item)
|
|
19
|
-
local node = item.getNode()
|
|
20
|
-
if not node then
|
|
21
|
-
return false
|
|
22
|
-
end
|
|
23
|
-
return selectedObject == node or selectedObject:IsDescendantOf(node)
|
|
24
|
-
end
|
|
25
|
-
local _result = -1
|
|
26
|
-
for _i, _v in items do
|
|
27
|
-
if _callback(_v, _i - 1, items) == true then
|
|
28
|
-
_result = _i - 1
|
|
29
|
-
break
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
-- ▲ ReadonlyArray.findIndex ▲
|
|
33
|
-
return _result
|
|
34
|
-
end
|
|
35
|
-
local function isItemDisabled(items, index)
|
|
36
|
-
local item = items[index + 1]
|
|
37
|
-
if not item then
|
|
38
|
-
return true
|
|
39
|
-
end
|
|
40
|
-
return item.getDisabled()
|
|
41
|
-
end
|
|
42
|
-
local function focusItem(items, index)
|
|
43
|
-
local item = items[index + 1]
|
|
44
|
-
if not item then
|
|
45
|
-
return nil
|
|
46
|
-
end
|
|
47
|
-
if item.getDisabled() then
|
|
48
|
-
return nil
|
|
49
|
-
end
|
|
50
|
-
local node = item.getNode()
|
|
51
|
-
if not node or not node.Selectable then
|
|
52
|
-
return nil
|
|
53
|
-
end
|
|
54
|
-
setSelectedGuiObject(node)
|
|
55
|
-
end
|
|
56
|
-
local function resolveArrowDirection(keyCode, orientation)
|
|
57
|
-
if (orientation == "vertical" or orientation == "both") and keyCode == Enum.KeyCode.Up then
|
|
58
|
-
return "prev"
|
|
59
|
-
end
|
|
60
|
-
if (orientation == "vertical" or orientation == "both") and keyCode == Enum.KeyCode.Down then
|
|
61
|
-
return "next"
|
|
62
|
-
end
|
|
63
|
-
if (orientation == "horizontal" or orientation == "both") and keyCode == Enum.KeyCode.Left then
|
|
64
|
-
return "prev"
|
|
65
|
-
end
|
|
66
|
-
if (orientation == "horizontal" or orientation == "both") and keyCode == Enum.KeyCode.Right then
|
|
67
|
-
return "next"
|
|
68
|
-
end
|
|
69
|
-
return nil
|
|
70
|
-
end
|
|
71
|
-
local function RovingFocusGroup(props)
|
|
72
|
-
local _condition = props.loop
|
|
73
|
-
if _condition == nil then
|
|
74
|
-
_condition = true
|
|
75
|
-
end
|
|
76
|
-
local loop = _condition
|
|
77
|
-
local orientation = props.orientation or "both"
|
|
78
|
-
local _condition_1 = props.active
|
|
79
|
-
if _condition_1 == nil then
|
|
80
|
-
_condition_1 = true
|
|
81
|
-
end
|
|
82
|
-
local active = _condition_1
|
|
83
|
-
local autoFocus = props.autoFocus or "none"
|
|
84
|
-
local itemEntriesRef = React.useRef({})
|
|
85
|
-
local revision, setRevision = React.useState(0)
|
|
86
|
-
local registerItem = React.useCallback(function(item)
|
|
87
|
-
local _current = itemEntriesRef.current
|
|
88
|
-
local _item = item
|
|
89
|
-
table.insert(_current, _item)
|
|
90
|
-
setRevision(function(value)
|
|
91
|
-
return value + 1
|
|
92
|
-
end)
|
|
93
|
-
return function()
|
|
94
|
-
local _exp = itemEntriesRef.current
|
|
95
|
-
-- ▼ ReadonlyArray.findIndex ▼
|
|
96
|
-
local _callback = function(entry)
|
|
97
|
-
return entry.id == item.id
|
|
98
|
-
end
|
|
99
|
-
local _result = -1
|
|
100
|
-
for _i, _v in _exp do
|
|
101
|
-
if _callback(_v, _i - 1, _exp) == true then
|
|
102
|
-
_result = _i - 1
|
|
103
|
-
break
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
-- ▲ ReadonlyArray.findIndex ▲
|
|
107
|
-
local index = _result
|
|
108
|
-
if index >= 0 then
|
|
109
|
-
table.remove(itemEntriesRef.current, index + 1)
|
|
110
|
-
setRevision(function(value)
|
|
111
|
-
return value + 1
|
|
112
|
-
end)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end, {})
|
|
116
|
-
React.useEffect(function()
|
|
117
|
-
if not active or autoFocus ~= "first" then
|
|
118
|
-
return nil
|
|
119
|
-
end
|
|
120
|
-
local items = itemEntriesRef.current
|
|
121
|
-
local firstEnabledIndex = getFirstEnabledRovingIndex(#items, function(index)
|
|
122
|
-
return isItemDisabled(items, index)
|
|
123
|
-
end)
|
|
124
|
-
if firstEnabledIndex >= 0 then
|
|
125
|
-
focusItem(items, firstEnabledIndex)
|
|
126
|
-
end
|
|
127
|
-
end, { active, autoFocus, revision })
|
|
128
|
-
React.useEffect(function()
|
|
129
|
-
if not active then
|
|
130
|
-
return nil
|
|
131
|
-
end
|
|
132
|
-
local connection = UserInputService.InputBegan:Connect(function(inputObject, gameProcessedEvent)
|
|
133
|
-
if gameProcessedEvent then
|
|
134
|
-
return nil
|
|
135
|
-
end
|
|
136
|
-
local keyCode = inputObject.KeyCode
|
|
137
|
-
local isHomeKey = keyCode == Enum.KeyCode.Home
|
|
138
|
-
local isEndKey = keyCode == Enum.KeyCode.End
|
|
139
|
-
local direction = resolveArrowDirection(keyCode, orientation)
|
|
140
|
-
if not isHomeKey and not isEndKey and not direction then
|
|
141
|
-
return nil
|
|
142
|
-
end
|
|
143
|
-
local items = itemEntriesRef.current
|
|
144
|
-
local itemCount = #items
|
|
145
|
-
if itemCount <= 0 then
|
|
146
|
-
return nil
|
|
147
|
-
end
|
|
148
|
-
local selectedObject = getSelectedGuiObject()
|
|
149
|
-
local currentIndex = findCurrentIndex(items, selectedObject)
|
|
150
|
-
if currentIndex < 0 then
|
|
151
|
-
return nil
|
|
152
|
-
end
|
|
153
|
-
if isHomeKey then
|
|
154
|
-
local firstEnabledIndex = getFirstEnabledRovingIndex(itemCount, function(index)
|
|
155
|
-
return isItemDisabled(items, index)
|
|
156
|
-
end)
|
|
157
|
-
if firstEnabledIndex >= 0 then
|
|
158
|
-
focusItem(items, firstEnabledIndex)
|
|
159
|
-
end
|
|
160
|
-
return nil
|
|
161
|
-
end
|
|
162
|
-
if isEndKey then
|
|
163
|
-
local lastEnabledIndex = getLastEnabledRovingIndex(itemCount, function(index)
|
|
164
|
-
return isItemDisabled(items, index)
|
|
165
|
-
end)
|
|
166
|
-
if lastEnabledIndex >= 0 then
|
|
167
|
-
focusItem(items, lastEnabledIndex)
|
|
168
|
-
end
|
|
169
|
-
return nil
|
|
170
|
-
end
|
|
171
|
-
if not direction then
|
|
172
|
-
return nil
|
|
173
|
-
end
|
|
174
|
-
local nextIndex = getNextRovingIndex(currentIndex, itemCount, direction, loop, function(index)
|
|
175
|
-
return isItemDisabled(items, index)
|
|
176
|
-
end)
|
|
177
|
-
if nextIndex >= 0 then
|
|
178
|
-
focusItem(items, nextIndex)
|
|
179
|
-
end
|
|
180
|
-
end)
|
|
181
|
-
return function()
|
|
182
|
-
connection:Disconnect()
|
|
183
|
-
end
|
|
184
|
-
end, { active, loop, orientation })
|
|
185
|
-
local contextValue = React.useMemo(function()
|
|
186
|
-
return {
|
|
187
|
-
registerItem = registerItem,
|
|
188
|
-
}
|
|
189
|
-
end, { registerItem })
|
|
190
|
-
return React.createElement(RovingFocusProvider, {
|
|
191
|
-
value = contextValue,
|
|
192
|
-
}, props.children)
|
|
193
|
-
end
|
|
194
|
-
return {
|
|
195
|
-
RovingFocusGroup = RovingFocusGroup,
|
|
196
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
-- Compiled with roblox-ts v3.0.0
|
|
2
|
-
local TS = _G[script]
|
|
3
|
-
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
|
-
local React = _core.React
|
|
5
|
-
local Slot = _core.Slot
|
|
6
|
-
local useRovingFocusContext = TS.import(script, script.Parent, "context").useRovingFocusContext
|
|
7
|
-
local nextRovingItemId = 0
|
|
8
|
-
local function toGuiObject(instance)
|
|
9
|
-
if not instance or not instance:IsA("GuiObject") then
|
|
10
|
-
return nil
|
|
11
|
-
end
|
|
12
|
-
return instance
|
|
13
|
-
end
|
|
14
|
-
local function RovingFocusItem(props)
|
|
15
|
-
local rovingFocusContext = useRovingFocusContext()
|
|
16
|
-
local itemRef = React.useRef()
|
|
17
|
-
local disabledRef = React.useRef(props.disabled == true)
|
|
18
|
-
React.useEffect(function()
|
|
19
|
-
disabledRef.current = props.disabled == true
|
|
20
|
-
end, { props.disabled })
|
|
21
|
-
local itemIdRef = React.useRef(0)
|
|
22
|
-
if itemIdRef.current == 0 then
|
|
23
|
-
nextRovingItemId += 1
|
|
24
|
-
itemIdRef.current = nextRovingItemId
|
|
25
|
-
end
|
|
26
|
-
React.useEffect(function()
|
|
27
|
-
return rovingFocusContext.registerItem({
|
|
28
|
-
id = itemIdRef.current,
|
|
29
|
-
getNode = function()
|
|
30
|
-
return itemRef.current
|
|
31
|
-
end,
|
|
32
|
-
getDisabled = function()
|
|
33
|
-
return disabledRef.current
|
|
34
|
-
end,
|
|
35
|
-
})
|
|
36
|
-
end, { rovingFocusContext })
|
|
37
|
-
local setItemRef = React.useCallback(function(instance)
|
|
38
|
-
itemRef.current = toGuiObject(instance)
|
|
39
|
-
end, {})
|
|
40
|
-
if props.asChild then
|
|
41
|
-
local child = props.children
|
|
42
|
-
if not child then
|
|
43
|
-
error("[RovingFocusItem] `asChild` requires a child element.")
|
|
44
|
-
end
|
|
45
|
-
return React.createElement(Slot, {
|
|
46
|
-
Active = props.disabled ~= true,
|
|
47
|
-
Selectable = props.disabled ~= true,
|
|
48
|
-
ref = setItemRef,
|
|
49
|
-
}, child)
|
|
50
|
-
end
|
|
51
|
-
return React.createElement("textbutton", {
|
|
52
|
-
Active = props.disabled ~= true,
|
|
53
|
-
AutoButtonColor = false,
|
|
54
|
-
BackgroundTransparency = 1,
|
|
55
|
-
BorderSizePixel = 0,
|
|
56
|
-
Selectable = props.disabled ~= true,
|
|
57
|
-
Size = UDim2.fromOffset(140, 30),
|
|
58
|
-
Text = "Item",
|
|
59
|
-
TextColor3 = Color3.fromRGB(240, 244, 250),
|
|
60
|
-
TextSize = 15,
|
|
61
|
-
ref = setItemRef,
|
|
62
|
-
}, props.children)
|
|
63
|
-
end
|
|
64
|
-
return {
|
|
65
|
-
RovingFocusItem = RovingFocusItem,
|
|
66
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
-- Compiled with roblox-ts v3.0.0
|
|
2
|
-
local TS = _G[script]
|
|
3
|
-
local createStrictContext = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).createStrictContext
|
|
4
|
-
local _binding = createStrictContext("RovingFocusGroup")
|
|
5
|
-
local RovingFocusProvider = _binding[1]
|
|
6
|
-
local useRovingFocusContext = _binding[2]
|
|
7
|
-
return {
|
|
8
|
-
RovingFocusProvider = RovingFocusProvider,
|
|
9
|
-
useRovingFocusContext = useRovingFocusContext,
|
|
10
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { RovingDirection } from "./types";
|
|
2
|
-
export declare function getNextRovingIndex(currentIndex: number, itemCount: number, direction: RovingDirection, loop: boolean, isDisabled?: (index: number) => boolean): number;
|
|
3
|
-
export declare function getFirstEnabledRovingIndex(itemCount: number, isDisabled?: (index: number) => boolean): number;
|
|
4
|
-
export declare function getLastEnabledRovingIndex(itemCount: number, isDisabled?: (index: number) => boolean): number;
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
-- Compiled with roblox-ts v3.0.0
|
|
2
|
-
local function getDirectionDelta(direction)
|
|
3
|
-
return if direction == "next" then 1 else -1
|
|
4
|
-
end
|
|
5
|
-
local function getNextRovingIndex(currentIndex, itemCount, direction, loop, isDisabled)
|
|
6
|
-
if itemCount <= 0 then
|
|
7
|
-
return -1
|
|
8
|
-
end
|
|
9
|
-
-- Keep focus unchanged when the current index is outside this group.
|
|
10
|
-
if currentIndex < 0 or currentIndex >= itemCount then
|
|
11
|
-
return currentIndex
|
|
12
|
-
end
|
|
13
|
-
local delta = getDirectionDelta(direction)
|
|
14
|
-
local candidate = currentIndex
|
|
15
|
-
do
|
|
16
|
-
local attempts = 0
|
|
17
|
-
local _shouldIncrement = false
|
|
18
|
-
while true do
|
|
19
|
-
if _shouldIncrement then
|
|
20
|
-
attempts += 1
|
|
21
|
-
else
|
|
22
|
-
_shouldIncrement = true
|
|
23
|
-
end
|
|
24
|
-
if not (attempts < itemCount) then
|
|
25
|
-
break
|
|
26
|
-
end
|
|
27
|
-
candidate += delta
|
|
28
|
-
if candidate < 0 or candidate >= itemCount then
|
|
29
|
-
if not loop then
|
|
30
|
-
return currentIndex
|
|
31
|
-
end
|
|
32
|
-
candidate = if direction == "next" then 0 else itemCount - 1
|
|
33
|
-
end
|
|
34
|
-
if not isDisabled or not isDisabled(candidate) then
|
|
35
|
-
return candidate
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
return currentIndex
|
|
40
|
-
end
|
|
41
|
-
local function getFirstEnabledRovingIndex(itemCount, isDisabled)
|
|
42
|
-
do
|
|
43
|
-
local index = 0
|
|
44
|
-
local _shouldIncrement = false
|
|
45
|
-
while true do
|
|
46
|
-
if _shouldIncrement then
|
|
47
|
-
index += 1
|
|
48
|
-
else
|
|
49
|
-
_shouldIncrement = true
|
|
50
|
-
end
|
|
51
|
-
if not (index < itemCount) then
|
|
52
|
-
break
|
|
53
|
-
end
|
|
54
|
-
if not isDisabled or not isDisabled(index) then
|
|
55
|
-
return index
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
return -1
|
|
60
|
-
end
|
|
61
|
-
local function getLastEnabledRovingIndex(itemCount, isDisabled)
|
|
62
|
-
do
|
|
63
|
-
local index = itemCount - 1
|
|
64
|
-
local _shouldIncrement = false
|
|
65
|
-
while true do
|
|
66
|
-
if _shouldIncrement then
|
|
67
|
-
index -= 1
|
|
68
|
-
else
|
|
69
|
-
_shouldIncrement = true
|
|
70
|
-
end
|
|
71
|
-
if not (index >= 0) then
|
|
72
|
-
break
|
|
73
|
-
end
|
|
74
|
-
if not isDisabled or not isDisabled(index) then
|
|
75
|
-
return index
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
return -1
|
|
80
|
-
end
|
|
81
|
-
return {
|
|
82
|
-
getNextRovingIndex = getNextRovingIndex,
|
|
83
|
-
getFirstEnabledRovingIndex = getFirstEnabledRovingIndex,
|
|
84
|
-
getLastEnabledRovingIndex = getLastEnabledRovingIndex,
|
|
85
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type React from "@rbxts/react";
|
|
2
|
-
export type RovingDirection = "next" | "prev";
|
|
3
|
-
export type RovingOrientation = "horizontal" | "vertical" | "both";
|
|
4
|
-
export type RovingAutoFocus = "none" | "first";
|
|
5
|
-
export type RovingFocusGroupProps = {
|
|
6
|
-
loop?: boolean;
|
|
7
|
-
orientation?: RovingOrientation;
|
|
8
|
-
active?: boolean;
|
|
9
|
-
autoFocus?: RovingAutoFocus;
|
|
10
|
-
children?: React.ReactNode;
|
|
11
|
-
};
|
|
12
|
-
export type RovingFocusItemProps = {
|
|
13
|
-
asChild?: boolean;
|
|
14
|
-
disabled?: boolean;
|
|
15
|
-
children?: React.ReactElement;
|
|
16
|
-
};
|
|
17
|
-
export type RovingItemRegistration = {
|
|
18
|
-
id: number;
|
|
19
|
-
getNode: () => GuiObject | undefined;
|
|
20
|
-
getDisabled: () => boolean;
|
|
21
|
-
};
|
|
22
|
-
export type RovingFocusContextValue = {
|
|
23
|
-
registerItem: (item: RovingItemRegistration) => () => void;
|
|
24
|
-
children?: React.ReactNode;
|
|
25
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
-- Compiled with roblox-ts v3.0.0
|
|
2
|
-
local TS = _G[script]
|
|
3
|
-
local GuiService = TS.import(script, script.Parent.Parent, "FocusScope", "focusManager").GuiService
|
|
4
|
-
local UserInputService = game:GetService("UserInputService")
|
|
5
|
-
local function getSelectedGuiObject()
|
|
6
|
-
return GuiService.SelectedObject
|
|
7
|
-
end
|
|
8
|
-
local function setSelectedGuiObject(guiObject)
|
|
9
|
-
GuiService.SelectedObject = guiObject
|
|
10
|
-
end
|
|
11
|
-
return {
|
|
12
|
-
getSelectedGuiObject = getSelectedGuiObject,
|
|
13
|
-
setSelectedGuiObject = setSelectedGuiObject,
|
|
14
|
-
UserInputService = UserInputService,
|
|
15
|
-
}
|