@lattice-ui/tabs 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
@@ -9,3 +9,8 @@ Behavior-only Tabs primitives for Roblox UI, aligned with the lattice compound p
9
9
  - `Tabs.List`
10
10
  - `Tabs.Trigger`
11
11
  - `Tabs.Content`
12
+
13
+ ## Notes
14
+
15
+ - `orientation` supports `horizontal` and `vertical` trigger navigation.
16
+ - Enabled triggers are selectable and activate immediately when selection focus moves onto them.
@@ -3,27 +3,19 @@ local TS = _G[script]
3
3
  local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
4
  local React = _core.React
5
5
  local Slot = _core.Slot
6
- local RovingFocusGroup = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusGroup
7
- local useTabsContext = TS.import(script, script.Parent, "context").useTabsContext
8
6
  local function TabsList(props)
9
- local tabsContext = useTabsContext()
10
- local listNode = if props.asChild then ((function()
7
+ if props.asChild then
11
8
  local child = props.children
12
9
  if not React.isValidElement(child) then
13
10
  error("[TabsList] `asChild` requires a child element.")
14
11
  end
15
12
  return React.createElement(Slot, nil, child)
16
- end)()) else (React.createElement("frame", {
13
+ end
14
+ return React.createElement("frame", {
17
15
  BackgroundTransparency = 1,
18
16
  BorderSizePixel = 0,
19
17
  Size = UDim2.fromOffset(0, 0),
20
- }, props.children))
21
- return React.createElement(RovingFocusGroup, {
22
- active = true,
23
- autoFocus = "none",
24
- loop = true,
25
- orientation = tabsContext.orientation,
26
- }, listNode)
18
+ }, props.children)
27
19
  end
28
20
  return {
29
21
  TabsList = TabsList,
@@ -1,24 +1,18 @@
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 findOrderedSelectionEntry = _core.findOrderedSelectionEntry
5
+ local focusOrderedSelectionEntry = _core.focusOrderedSelectionEntry
6
+ local getOrderedSelectionEntries = _core.getOrderedSelectionEntries
7
+ local getRelativeOrderedSelectionEntry = _core.getRelativeOrderedSelectionEntry
4
8
  local React = _core.React
5
9
  local useControllableState = _core.useControllableState
6
10
  local TabsContextProvider = TS.import(script, script.Parent, "context").TabsContextProvider
7
- local function getOrderedTriggers(triggers)
8
- local _array = {}
9
- local _length = #_array
10
- table.move(triggers, 1, #triggers, _length + 1, _array)
11
- local ordered = _array
12
- table.sort(ordered, function(a, b)
13
- return a.order < b.order
14
- end)
15
- return ordered
16
- end
17
11
  local function resolveNextValue(currentValue, orderedTriggers, fallbackOrder)
18
12
  -- ▼ ReadonlyArray.filter ▼
19
13
  local _newValue = {}
20
14
  local _callback = function(trigger)
21
- return not trigger.disabled
15
+ return not trigger.getDisabled()
22
16
  end
23
17
  local _length = 0
24
18
  for _k, _v in orderedTriggers do
@@ -102,6 +96,7 @@ local function resolveNextValue(currentValue, orderedTriggers, fallbackOrder)
102
96
  return _result_3
103
97
  end
104
98
  local function TabsRoot(props)
99
+ local orientation = props.orientation or "horizontal"
105
100
  local _binding = useControllableState({
106
101
  value = props.value,
107
102
  defaultValue = props.defaultValue,
@@ -116,8 +111,6 @@ local function TabsRoot(props)
116
111
  })
117
112
  local value = _binding[1]
118
113
  local setValueState = _binding[2]
119
- local orientation = props.orientation or "horizontal"
120
- local activationMode = props.activationMode or "automatic"
121
114
  local triggerRegistryRef = React.useRef({})
122
115
  local lastSelectedOrderRef = React.useRef()
123
116
  local registryRevision, setRegistryRevision = React.useState(0)
@@ -152,10 +145,10 @@ local function TabsRoot(props)
152
145
  end
153
146
  end, {})
154
147
  local setValue = React.useCallback(function(nextValue)
155
- local orderedTriggers = getOrderedTriggers(triggerRegistryRef.current)
148
+ local orderedTriggers = getOrderedSelectionEntries(triggerRegistryRef.current)
156
149
  -- ▼ ReadonlyArray.find ▼
157
150
  local _callback = function(trigger)
158
- return trigger.value == nextValue and not trigger.disabled
151
+ return trigger.value == nextValue and not trigger.getDisabled()
159
152
  end
160
153
  local _result
161
154
  for _i, _v in orderedTriggers do
@@ -171,11 +164,27 @@ local function TabsRoot(props)
171
164
  end
172
165
  setValueState(nextValue)
173
166
  end, { setValueState })
167
+ local moveSelection = React.useCallback(function(fromValue, direction)
168
+ local currentTrigger = findOrderedSelectionEntry(triggerRegistryRef.current, function(trigger)
169
+ return trigger.value == fromValue
170
+ end) or nil
171
+ local _exp = triggerRegistryRef.current
172
+ local _result = currentTrigger
173
+ if _result ~= nil then
174
+ _result = _result.id
175
+ end
176
+ local nextTrigger = getRelativeOrderedSelectionEntry(_exp, _result, direction)
177
+ if not nextTrigger then
178
+ return nil
179
+ end
180
+ focusOrderedSelectionEntry(nextTrigger)
181
+ setValue(nextTrigger.value)
182
+ end, { setValue })
174
183
  React.useEffect(function()
175
- local orderedTriggers = getOrderedTriggers(triggerRegistryRef.current)
184
+ local orderedTriggers = getOrderedSelectionEntries(triggerRegistryRef.current)
176
185
  -- ▼ ReadonlyArray.find ▼
177
186
  local _callback = function(trigger)
178
- return trigger.value == value and not trigger.disabled
187
+ return trigger.value == value and not trigger.getDisabled()
179
188
  end
180
189
  local _result
181
190
  for _i, _v in orderedTriggers do
@@ -197,12 +206,12 @@ local function TabsRoot(props)
197
206
  local contextValue = React.useMemo(function()
198
207
  return {
199
208
  value = value,
200
- setValue = setValue,
201
209
  orientation = orientation,
202
- activationMode = activationMode,
210
+ setValue = setValue,
203
211
  registerTrigger = registerTrigger,
212
+ moveSelection = moveSelection,
204
213
  }
205
- end, { activationMode, orientation, registerTrigger, setValue, value })
214
+ end, { moveSelection, orientation, registerTrigger, setValue, value })
206
215
  return React.createElement(TabsContextProvider, {
207
216
  value = contextValue,
208
217
  }, props.children)
@@ -3,7 +3,7 @@ local TS = _G[script]
3
3
  local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
4
  local React = _core.React
5
5
  local Slot = _core.Slot
6
- local RovingFocusItem = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusItem
6
+ local useFocusNode = _core.useFocusNode
7
7
  local useTabsContext = TS.import(script, script.Parent, "context").useTabsContext
8
8
  local createTabsTriggerName = TS.import(script, script.Parent, "internals", "ids").createTabsTriggerName
9
9
  local nextTriggerId = 0
@@ -19,6 +19,10 @@ local function TabsTrigger(props)
19
19
  local triggerRef = React.useRef()
20
20
  local selected = tabsContext.value == props.value
21
21
  local disabled = props.disabled == true
22
+ local disabledRef = React.useRef(disabled)
23
+ React.useEffect(function()
24
+ disabledRef.current = disabled
25
+ end, { disabled })
22
26
  local triggerIdRef = React.useRef(0)
23
27
  if triggerIdRef.current == 0 then
24
28
  nextTriggerId += 1
@@ -33,11 +37,19 @@ local function TabsTrigger(props)
33
37
  return tabsContext.registerTrigger({
34
38
  id = triggerIdRef.current,
35
39
  value = props.value,
36
- disabled = disabled,
37
40
  ref = triggerRef,
38
41
  order = triggerOrderRef.current,
42
+ getDisabled = function()
43
+ return disabledRef.current
44
+ end,
39
45
  })
40
- end, { disabled, props.value, tabsContext })
46
+ end, { props.value, tabsContext })
47
+ useFocusNode({
48
+ ref = triggerRef,
49
+ getDisabled = function()
50
+ return disabledRef.current
51
+ end,
52
+ })
41
53
  local setTriggerRef = React.useCallback(function(instance)
42
54
  triggerRef.current = toGuiObject(instance)
43
55
  end, {})
@@ -48,16 +60,21 @@ local function TabsTrigger(props)
48
60
  tabsContext.setValue(props.value)
49
61
  end, { disabled, props.value, tabsContext })
50
62
  local handleSelectionGained = React.useCallback(function()
51
- if disabled or tabsContext.activationMode ~= "automatic" then
63
+ if disabled then
52
64
  return nil
53
65
  end
54
66
  tabsContext.setValue(props.value)
55
67
  end, { disabled, props.value, tabsContext })
56
68
  local handleInputBegan = React.useCallback(function(_rbx, inputObject)
57
- if disabled or tabsContext.activationMode ~= "manual" then
69
+ if disabled then
58
70
  return nil
59
71
  end
60
72
  local keyCode = inputObject.KeyCode
73
+ local direction = if tabsContext.orientation == "horizontal" then if keyCode == Enum.KeyCode.Left then -1 elseif keyCode == Enum.KeyCode.Right then 1 else nil elseif keyCode == Enum.KeyCode.Up then -1 elseif keyCode == Enum.KeyCode.Down then 1 else nil
74
+ if direction ~= nil then
75
+ tabsContext.moveSelection(props.value, direction)
76
+ return nil
77
+ end
61
78
  if keyCode ~= Enum.KeyCode.Return and keyCode ~= Enum.KeyCode.Space then
62
79
  return nil
63
80
  end
@@ -66,8 +83,8 @@ local function TabsTrigger(props)
66
83
  local eventHandlers = React.useMemo(function()
67
84
  return {
68
85
  Activated = handleActivated,
69
- SelectionGained = handleSelectionGained,
70
86
  InputBegan = handleInputBegan,
87
+ SelectionGained = handleSelectionGained,
71
88
  }
72
89
  end, { handleActivated, handleInputBegan, handleSelectionGained })
73
90
  local triggerName = React.useMemo(function()
@@ -78,19 +95,15 @@ local function TabsTrigger(props)
78
95
  if not child then
79
96
  error("[TabsTrigger] `asChild` requires a child element.")
80
97
  end
81
- return React.createElement(RovingFocusItem, {
82
- asChild = true,
83
- disabled = disabled,
84
- }, React.createElement(Slot, {
98
+ return React.createElement(Slot, {
99
+ Active = not disabled,
85
100
  Event = eventHandlers,
86
101
  Name = triggerName,
102
+ Selectable = not disabled,
87
103
  ref = setTriggerRef,
88
- }, child))
104
+ }, child)
89
105
  end
90
- return React.createElement(RovingFocusItem, {
91
- asChild = true,
92
- disabled = disabled,
93
- }, React.createElement("textbutton", {
106
+ return React.createElement("textbutton", {
94
107
  Active = not disabled,
95
108
  AutoButtonColor = false,
96
109
  BackgroundColor3 = if selected then Color3.fromRGB(86, 137, 245) else Color3.fromRGB(47, 53, 68),
@@ -102,7 +115,7 @@ local function TabsTrigger(props)
102
115
  TextColor3 = if disabled then Color3.fromRGB(136, 144, 159) else Color3.fromRGB(235, 240, 248),
103
116
  TextSize = 15,
104
117
  ref = setTriggerRef,
105
- }, props.children))
118
+ }, props.children)
106
119
  end
107
120
  return {
108
121
  TabsTrigger = TabsTrigger,
@@ -1,27 +1,25 @@
1
1
  import type React from "@rbxts/react";
2
- export type TabsOrientation = "horizontal" | "vertical";
3
- export type TabsActivationMode = "automatic" | "manual";
4
2
  export type TabsSetValue = (value: string) => void;
3
+ export type TabsOrientation = "horizontal" | "vertical";
5
4
  export type TabsTriggerRegistration = {
6
5
  id: number;
7
6
  value: string;
8
- disabled: boolean;
9
7
  ref: React.MutableRefObject<GuiObject | undefined>;
10
8
  order: number;
9
+ getDisabled: () => boolean;
11
10
  };
12
11
  export type TabsContextValue = {
13
12
  value?: string;
14
- setValue: TabsSetValue;
15
13
  orientation: TabsOrientation;
16
- activationMode: TabsActivationMode;
14
+ setValue: TabsSetValue;
17
15
  registerTrigger: (trigger: TabsTriggerRegistration) => () => void;
16
+ moveSelection: (fromValue: string, direction: -1 | 1) => void;
18
17
  };
19
18
  export type TabsProps = {
20
19
  value?: string;
21
20
  defaultValue?: string;
22
21
  onValueChange?: (value: string) => void;
23
22
  orientation?: TabsOrientation;
24
- activationMode?: TabsActivationMode;
25
23
  children?: React.ReactNode;
26
24
  };
27
25
  export type TabsListProps = {
package/out/index.d.ts CHANGED
@@ -8,4 +8,4 @@ export declare const Tabs: {
8
8
  readonly Trigger: typeof TabsTrigger;
9
9
  readonly Content: typeof TabsContent;
10
10
  };
11
- export type { TabsActivationMode, TabsContentProps, TabsContextValue, TabsListProps, TabsOrientation, TabsProps, TabsTriggerProps, } from "./Tabs/types";
11
+ export type { TabsContentProps, TabsContextValue, TabsListProps, TabsOrientation, TabsProps, TabsTriggerProps, } from "./Tabs/types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lattice-ui/tabs",
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,9 +9,8 @@
9
9
  "README.md"
10
10
  ],
11
11
  "dependencies": {
12
- "@lattice-ui/core": "0.3.2",
13
- "@lattice-ui/focus": "0.3.2",
14
- "@lattice-ui/layer": "0.3.2"
12
+ "@lattice-ui/core": "0.4.1",
13
+ "@lattice-ui/layer": "0.4.1"
15
14
  },
16
15
  "devDependencies": {
17
16
  "@rbxts/react": "17.3.7-ts.1",