@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 CHANGED
@@ -1,10 +1,9 @@
1
1
  # @lattice-ui/focus
2
2
 
3
- Focus and directional navigation primitives for Roblox UI.
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 _guiSelection = TS.import(script, script.Parent.Parent, "internals", "guiSelection")
7
- local getSelectedGuiObject = _guiSelection.getSelectedGuiObject
8
- local setSelectedGuiObject = _guiSelection.setSelectedGuiObject
9
- local _focusManager = TS.import(script, script.Parent, "focusManager")
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 isLiveGuiObject(guiObject)
25
- return guiObject ~= nil and guiObject.Parent ~= nil
26
- end
27
- local function isFocusable(guiObject)
28
- return isLiveGuiObject(guiObject) and guiObject.Selectable
29
- end
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
- nextScopeId += 1
67
- scopeIdRef.current = nextScopeId
40
+ scopeIdRef.current = createFocusScopeId()
68
41
  end
69
42
  local scopeRootRef = React.useRef()
70
- local lastFocusedInsideRef = React.useRef()
71
- local restoreSnapshotRef = React.useRef()
72
- local isRedirectingRef = React.useRef(false)
73
- local shouldRestoreFocusRef = React.useRef(shouldRestoreFocus)
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
- local scopeRoot = toGuiObject(instance)
129
- scopeRootRef.current = scopeRoot
130
- if not scopeRoot then
131
- return nil
48
+ scopeRootRef.current = toGuiObject(instance)
49
+ if scopeIdRef.current ~= 0 then
50
+ syncFocusScope(scopeIdRef.current)
132
51
  end
133
- updateLastFocusedInside()
134
- if active and trapped then
135
- enforceFocusTrap()
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, { active, enforceFocusTrap, trapped, updateLastFocusedInside })
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
- if shouldRestoreFocusRef.current then
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
- selectedObjectConnection:Disconnect()
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, enforceFocusTrap, trapped, updateLastFocusedInside })
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
@@ -1,3 +1 @@
1
1
  export * from "./FocusScope/FocusScope";
2
- export * from "./RovingFocus/RovingFocusGroup";
3
- export * from "./RovingFocus/RovingFocusItem";
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.2",
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.3.2"
12
+ "@lattice-ui/core": "0.4.1"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@rbxts/react": "17.3.7-ts.1",
@@ -1,3 +0,0 @@
1
- import { React } from "@lattice-ui/core";
2
- import type { RovingFocusGroupProps } from "./types";
3
- export declare function RovingFocusGroup(props: RovingFocusGroupProps): React.JSX.Element;
@@ -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,3 +0,0 @@
1
- import { React } from "@lattice-ui/core";
2
- import type { RovingFocusItemProps } from "./types";
3
- export declare function RovingFocusItem(props: RovingFocusItemProps): React.JSX.Element;
@@ -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,3 +0,0 @@
1
- import type { RovingFocusContextValue } from "./types";
2
- declare const RovingFocusProvider: import("@rbxts/react").Provider<RovingFocusContextValue | undefined>, useRovingFocusContext: () => RovingFocusContextValue;
3
- export { RovingFocusProvider, useRovingFocusContext };
@@ -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,2 +0,0 @@
1
- -- Compiled with roblox-ts v3.0.0
2
- return nil
@@ -1,3 +0,0 @@
1
- export declare const UserInputService: UserInputService;
2
- export declare function getSelectedGuiObject(): GuiObject | undefined;
3
- export declare function setSelectedGuiObject(guiObject: GuiObject | undefined): void;
@@ -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
- }