@lattice-ui/menu 0.3.2 → 0.4.0
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/Menu/MenuContent.luau +4 -15
- package/out/Menu/MenuItem.luau +62 -11
- package/out/Menu/MenuRoot.luau +64 -1
- package/out/Menu/MenuTrigger.luau +29 -3
- package/out/Menu/types.d.ts +10 -2
- package/out/index.d.ts +1 -1
- package/package.json +5 -5
|
@@ -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
|
|
6
|
+
local FocusScope = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).FocusScope
|
|
7
7
|
local _layer = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out)
|
|
8
8
|
local DismissableLayer = _layer.DismissableLayer
|
|
9
9
|
local Presence = _layer.Presence
|
|
@@ -52,25 +52,18 @@ local function MenuContentImpl(props)
|
|
|
52
52
|
enabled = props.enabled,
|
|
53
53
|
modal = menuContext.modal,
|
|
54
54
|
onDismiss = props.onDismiss,
|
|
55
|
-
onEscapeKeyDown = props.onEscapeKeyDown,
|
|
56
55
|
onInteractOutside = props.onInteractOutside,
|
|
57
56
|
onPointerDownOutside = props.onPointerDownOutside,
|
|
58
|
-
}, React.createElement(
|
|
57
|
+
}, React.createElement(FocusScope, {
|
|
59
58
|
active = props.enabled,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
orientation = "vertical",
|
|
59
|
+
restoreFocus = true,
|
|
60
|
+
trapped = menuContext.modal,
|
|
63
61
|
}, contentNode))
|
|
64
62
|
end
|
|
65
63
|
local function MenuContent(props)
|
|
66
64
|
local menuContext = useMenuContext()
|
|
67
65
|
local open = menuContext.open
|
|
68
66
|
local forceMount = props.forceMount == true
|
|
69
|
-
local _condition = props.loop
|
|
70
|
-
if _condition == nil then
|
|
71
|
-
_condition = true
|
|
72
|
-
end
|
|
73
|
-
local loop = _condition
|
|
74
67
|
local handleDismiss = React.useCallback(function()
|
|
75
68
|
menuContext.setOpen(false)
|
|
76
69
|
end, { menuContext.setOpen })
|
|
@@ -81,10 +74,8 @@ local function MenuContent(props)
|
|
|
81
74
|
return React.createElement(MenuContentImpl, {
|
|
82
75
|
asChild = props.asChild,
|
|
83
76
|
enabled = open,
|
|
84
|
-
loop = loop,
|
|
85
77
|
offset = props.offset,
|
|
86
78
|
onDismiss = handleDismiss,
|
|
87
|
-
onEscapeKeyDown = props.onEscapeKeyDown,
|
|
88
79
|
onInteractOutside = props.onInteractOutside,
|
|
89
80
|
onPointerDownOutside = props.onPointerDownOutside,
|
|
90
81
|
padding = props.padding,
|
|
@@ -99,10 +90,8 @@ local function MenuContent(props)
|
|
|
99
90
|
return React.createElement(MenuContentImpl, {
|
|
100
91
|
asChild = props.asChild,
|
|
101
92
|
enabled = state.isPresent,
|
|
102
|
-
loop = loop,
|
|
103
93
|
offset = props.offset,
|
|
104
94
|
onDismiss = handleDismiss,
|
|
105
|
-
onEscapeKeyDown = props.onEscapeKeyDown,
|
|
106
95
|
onInteractOutside = props.onInteractOutside,
|
|
107
96
|
onPointerDownOutside = props.onPointerDownOutside,
|
|
108
97
|
padding = props.padding,
|
package/out/Menu/MenuItem.luau
CHANGED
|
@@ -3,8 +3,10 @@ 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
|
|
6
|
+
local useFocusNode = _core.useFocusNode
|
|
7
7
|
local useMenuContext = TS.import(script, script.Parent, "context").useMenuContext
|
|
8
|
+
local nextItemId = 0
|
|
9
|
+
local nextItemOrder = 0
|
|
8
10
|
local function createMenuSelectEvent()
|
|
9
11
|
local event
|
|
10
12
|
event = {
|
|
@@ -17,6 +19,44 @@ local function createMenuSelectEvent()
|
|
|
17
19
|
end
|
|
18
20
|
local function MenuItem(props)
|
|
19
21
|
local menuContext = useMenuContext()
|
|
22
|
+
local itemRef = React.useRef()
|
|
23
|
+
local disabledRef = React.useRef(props.disabled == true)
|
|
24
|
+
React.useEffect(function()
|
|
25
|
+
disabledRef.current = props.disabled == true
|
|
26
|
+
end, { props.disabled })
|
|
27
|
+
local itemIdRef = React.useRef(0)
|
|
28
|
+
if itemIdRef.current == 0 then
|
|
29
|
+
nextItemId += 1
|
|
30
|
+
itemIdRef.current = nextItemId
|
|
31
|
+
end
|
|
32
|
+
local itemOrderRef = React.useRef(0)
|
|
33
|
+
if itemOrderRef.current == 0 then
|
|
34
|
+
nextItemOrder += 1
|
|
35
|
+
itemOrderRef.current = nextItemOrder
|
|
36
|
+
end
|
|
37
|
+
React.useEffect(function()
|
|
38
|
+
return menuContext.registerItem({
|
|
39
|
+
id = itemIdRef.current,
|
|
40
|
+
order = itemOrderRef.current,
|
|
41
|
+
ref = itemRef,
|
|
42
|
+
getDisabled = function()
|
|
43
|
+
return disabledRef.current
|
|
44
|
+
end,
|
|
45
|
+
})
|
|
46
|
+
end, { menuContext })
|
|
47
|
+
useFocusNode({
|
|
48
|
+
ref = itemRef,
|
|
49
|
+
getDisabled = function()
|
|
50
|
+
return disabledRef.current
|
|
51
|
+
end,
|
|
52
|
+
})
|
|
53
|
+
local setItemRef = React.useCallback(function(instance)
|
|
54
|
+
if not instance or not instance:IsA("GuiObject") then
|
|
55
|
+
itemRef.current = nil
|
|
56
|
+
return nil
|
|
57
|
+
end
|
|
58
|
+
itemRef.current = instance
|
|
59
|
+
end, {})
|
|
20
60
|
local handleActivated = React.useCallback(function()
|
|
21
61
|
if props.disabled then
|
|
22
62
|
return nil
|
|
@@ -30,32 +70,42 @@ local function MenuItem(props)
|
|
|
30
70
|
menuContext.setOpen(false)
|
|
31
71
|
end
|
|
32
72
|
end, { menuContext, props.disabled, props.onSelect })
|
|
73
|
+
local handleInputBegan = React.useCallback(function(_rbx, inputObject)
|
|
74
|
+
if props.disabled then
|
|
75
|
+
return nil
|
|
76
|
+
end
|
|
77
|
+
local keyCode = inputObject.KeyCode
|
|
78
|
+
if keyCode == Enum.KeyCode.Up or keyCode == Enum.KeyCode.Down then
|
|
79
|
+
menuContext.moveSelection(if keyCode == Enum.KeyCode.Up then -1 else 1)
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
if keyCode == Enum.KeyCode.Return or keyCode == Enum.KeyCode.Space then
|
|
83
|
+
handleActivated()
|
|
84
|
+
end
|
|
85
|
+
end, { handleActivated, menuContext, props.disabled })
|
|
33
86
|
if props.asChild then
|
|
34
87
|
local child = props.children
|
|
35
88
|
if not child then
|
|
36
89
|
error("[MenuItem] `asChild` requires a child element.")
|
|
37
90
|
end
|
|
38
|
-
return React.createElement(
|
|
39
|
-
asChild = true,
|
|
40
|
-
disabled = props.disabled,
|
|
41
|
-
}, React.createElement(Slot, {
|
|
91
|
+
return React.createElement(Slot, {
|
|
42
92
|
Active = props.disabled ~= true,
|
|
43
93
|
Event = {
|
|
44
94
|
Activated = handleActivated,
|
|
95
|
+
InputBegan = handleInputBegan,
|
|
45
96
|
},
|
|
46
97
|
Selectable = props.disabled ~= true,
|
|
47
|
-
|
|
98
|
+
ref = setItemRef,
|
|
99
|
+
}, child)
|
|
48
100
|
end
|
|
49
|
-
return React.createElement(
|
|
50
|
-
asChild = true,
|
|
51
|
-
disabled = props.disabled,
|
|
52
|
-
}, React.createElement("textbutton", {
|
|
101
|
+
return React.createElement("textbutton", {
|
|
53
102
|
Active = props.disabled ~= true,
|
|
54
103
|
AutoButtonColor = false,
|
|
55
104
|
BackgroundColor3 = Color3.fromRGB(47, 53, 68),
|
|
56
105
|
BorderSizePixel = 0,
|
|
57
106
|
Event = {
|
|
58
107
|
Activated = handleActivated,
|
|
108
|
+
InputBegan = handleInputBegan,
|
|
59
109
|
},
|
|
60
110
|
Selectable = props.disabled ~= true,
|
|
61
111
|
Size = UDim2.fromOffset(220, 34),
|
|
@@ -63,10 +113,11 @@ local function MenuItem(props)
|
|
|
63
113
|
TextColor3 = if props.disabled then Color3.fromRGB(135, 142, 156) else Color3.fromRGB(234, 239, 247),
|
|
64
114
|
TextSize = 15,
|
|
65
115
|
TextXAlignment = Enum.TextXAlignment.Left,
|
|
116
|
+
ref = setItemRef,
|
|
66
117
|
}, React.createElement("uipadding", {
|
|
67
118
|
PaddingLeft = UDim.new(0, 10),
|
|
68
119
|
PaddingRight = UDim.new(0, 10),
|
|
69
|
-
}), props.children)
|
|
120
|
+
}), props.children)
|
|
70
121
|
end
|
|
71
122
|
return {
|
|
72
123
|
MenuItem = MenuItem,
|
package/out/Menu/MenuRoot.luau
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
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 focusGuiObject = _core.focusGuiObject
|
|
5
|
+
local focusOrderedSelectionEntry = _core.focusOrderedSelectionEntry
|
|
6
|
+
local getCurrentOrderedSelectionEntry = _core.getCurrentOrderedSelectionEntry
|
|
7
|
+
local getFirstOrderedSelectionEntry = _core.getFirstOrderedSelectionEntry
|
|
8
|
+
local getRelativeOrderedSelectionEntry = _core.getRelativeOrderedSelectionEntry
|
|
4
9
|
local React = _core.React
|
|
5
10
|
local useControllableState = _core.useControllableState
|
|
6
11
|
local MenuContextProvider = TS.import(script, script.Parent, "context").MenuContextProvider
|
|
@@ -25,9 +30,63 @@ local function Menu(props)
|
|
|
25
30
|
local modal = _condition_1
|
|
26
31
|
local triggerRef = React.useRef()
|
|
27
32
|
local contentRef = React.useRef()
|
|
33
|
+
local itemEntriesRef = React.useRef({})
|
|
34
|
+
local registryRevision, setRegistryRevision = React.useState(0)
|
|
28
35
|
local setOpen = React.useCallback(function(nextOpen)
|
|
29
36
|
setOpenState(nextOpen)
|
|
30
37
|
end, { setOpenState })
|
|
38
|
+
local registerItem = React.useCallback(function(item)
|
|
39
|
+
local _current = itemEntriesRef.current
|
|
40
|
+
local _item = item
|
|
41
|
+
table.insert(_current, _item)
|
|
42
|
+
setRegistryRevision(function(revision)
|
|
43
|
+
return revision + 1
|
|
44
|
+
end)
|
|
45
|
+
return function()
|
|
46
|
+
local _exp = itemEntriesRef.current
|
|
47
|
+
-- ▼ ReadonlyArray.findIndex ▼
|
|
48
|
+
local _callback = function(entry)
|
|
49
|
+
return entry.id == item.id
|
|
50
|
+
end
|
|
51
|
+
local _result = -1
|
|
52
|
+
for _i, _v in _exp do
|
|
53
|
+
if _callback(_v, _i - 1, _exp) == true then
|
|
54
|
+
_result = _i - 1
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
-- ▲ ReadonlyArray.findIndex ▲
|
|
59
|
+
local index = _result
|
|
60
|
+
if index >= 0 then
|
|
61
|
+
table.remove(itemEntriesRef.current, index + 1)
|
|
62
|
+
setRegistryRevision(function(revision)
|
|
63
|
+
return revision + 1
|
|
64
|
+
end)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end, {})
|
|
68
|
+
local focusFirstItem = React.useCallback(function()
|
|
69
|
+
focusOrderedSelectionEntry(getFirstOrderedSelectionEntry(itemEntriesRef.current))
|
|
70
|
+
end, {})
|
|
71
|
+
local moveSelection = React.useCallback(function(direction)
|
|
72
|
+
local currentItem = getCurrentOrderedSelectionEntry(itemEntriesRef.current)
|
|
73
|
+
local _exp = itemEntriesRef.current
|
|
74
|
+
local _result = currentItem
|
|
75
|
+
if _result ~= nil then
|
|
76
|
+
_result = _result.id
|
|
77
|
+
end
|
|
78
|
+
local nextItem = getRelativeOrderedSelectionEntry(_exp, _result, direction)
|
|
79
|
+
focusOrderedSelectionEntry(nextItem)
|
|
80
|
+
end, {})
|
|
81
|
+
local restoreTriggerFocus = React.useCallback(function()
|
|
82
|
+
focusGuiObject(triggerRef.current)
|
|
83
|
+
end, {})
|
|
84
|
+
React.useEffect(function()
|
|
85
|
+
if not open then
|
|
86
|
+
return nil
|
|
87
|
+
end
|
|
88
|
+
focusFirstItem()
|
|
89
|
+
end, { focusFirstItem, open, registryRevision })
|
|
31
90
|
local contextValue = React.useMemo(function()
|
|
32
91
|
return {
|
|
33
92
|
open = open,
|
|
@@ -35,8 +94,12 @@ local function Menu(props)
|
|
|
35
94
|
modal = modal,
|
|
36
95
|
triggerRef = triggerRef,
|
|
37
96
|
contentRef = contentRef,
|
|
97
|
+
registerItem = registerItem,
|
|
98
|
+
focusFirstItem = focusFirstItem,
|
|
99
|
+
moveSelection = moveSelection,
|
|
100
|
+
restoreTriggerFocus = restoreTriggerFocus,
|
|
38
101
|
}
|
|
39
|
-
end, { modal, open, setOpen })
|
|
102
|
+
end, { focusFirstItem, modal, moveSelection, open, registerItem, restoreTriggerFocus, setOpen })
|
|
40
103
|
return React.createElement(MenuContextProvider, {
|
|
41
104
|
value = contextValue,
|
|
42
105
|
}, props.children)
|
|
@@ -1,8 +1,10 @@
|
|
|
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 focusGuiObject = _core.focusGuiObject
|
|
4
5
|
local React = _core.React
|
|
5
6
|
local Slot = _core.Slot
|
|
7
|
+
local useFocusNode = _core.useFocusNode
|
|
6
8
|
local useMenuContext = TS.import(script, script.Parent, "context").useMenuContext
|
|
7
9
|
local function toGuiObject(instance)
|
|
8
10
|
if not instance or not instance:IsA("GuiObject") then
|
|
@@ -12,24 +14,47 @@ local function toGuiObject(instance)
|
|
|
12
14
|
end
|
|
13
15
|
local function MenuTrigger(props)
|
|
14
16
|
local menuContext = useMenuContext()
|
|
17
|
+
local triggerRef = menuContext.triggerRef
|
|
15
18
|
local setTriggerRef = React.useCallback(function(instance)
|
|
16
|
-
|
|
17
|
-
end, {
|
|
19
|
+
triggerRef.current = toGuiObject(instance)
|
|
20
|
+
end, { triggerRef })
|
|
21
|
+
useFocusNode({
|
|
22
|
+
ref = triggerRef,
|
|
23
|
+
disabled = props.disabled == true,
|
|
24
|
+
})
|
|
18
25
|
local handleActivated = React.useCallback(function()
|
|
19
26
|
if props.disabled then
|
|
20
27
|
return nil
|
|
21
28
|
end
|
|
29
|
+
if not menuContext.open then
|
|
30
|
+
focusGuiObject(triggerRef.current)
|
|
31
|
+
end
|
|
22
32
|
menuContext.setOpen(not menuContext.open)
|
|
23
|
-
end, { menuContext.open, menuContext.setOpen, props.disabled })
|
|
33
|
+
end, { menuContext.open, menuContext.setOpen, props.disabled, triggerRef })
|
|
34
|
+
local handleInputBegan = React.useCallback(function(_rbx, inputObject)
|
|
35
|
+
if props.disabled then
|
|
36
|
+
return nil
|
|
37
|
+
end
|
|
38
|
+
local keyCode = inputObject.KeyCode
|
|
39
|
+
if keyCode == Enum.KeyCode.Return or keyCode == Enum.KeyCode.Space then
|
|
40
|
+
if not menuContext.open then
|
|
41
|
+
focusGuiObject(triggerRef.current)
|
|
42
|
+
end
|
|
43
|
+
menuContext.setOpen(not menuContext.open)
|
|
44
|
+
end
|
|
45
|
+
end, { menuContext.open, menuContext.setOpen, props.disabled, triggerRef })
|
|
24
46
|
if props.asChild then
|
|
25
47
|
local child = props.children
|
|
26
48
|
if not child then
|
|
27
49
|
error("[MenuTrigger] `asChild` requires a child element.")
|
|
28
50
|
end
|
|
29
51
|
return React.createElement(Slot, {
|
|
52
|
+
Active = props.disabled ~= true,
|
|
30
53
|
Event = {
|
|
31
54
|
Activated = handleActivated,
|
|
55
|
+
InputBegan = handleInputBegan,
|
|
32
56
|
},
|
|
57
|
+
Selectable = props.disabled ~= true,
|
|
33
58
|
ref = setTriggerRef,
|
|
34
59
|
}, child)
|
|
35
60
|
end
|
|
@@ -40,6 +65,7 @@ local function MenuTrigger(props)
|
|
|
40
65
|
BorderSizePixel = 0,
|
|
41
66
|
Event = {
|
|
42
67
|
Activated = handleActivated,
|
|
68
|
+
InputBegan = handleInputBegan,
|
|
43
69
|
},
|
|
44
70
|
Selectable = props.disabled ~= true,
|
|
45
71
|
Size = UDim2.fromOffset(140, 38),
|
package/out/Menu/types.d.ts
CHANGED
|
@@ -2,12 +2,22 @@ import type { LayerInteractEvent } from "@lattice-ui/layer";
|
|
|
2
2
|
import type { PopperPlacement } from "@lattice-ui/popper";
|
|
3
3
|
import type React from "@rbxts/react";
|
|
4
4
|
export type MenuSetOpen = (open: boolean) => void;
|
|
5
|
+
export type MenuItemRegistration = {
|
|
6
|
+
id: number;
|
|
7
|
+
order: number;
|
|
8
|
+
ref: React.MutableRefObject<GuiObject | undefined>;
|
|
9
|
+
getDisabled: () => boolean;
|
|
10
|
+
};
|
|
5
11
|
export type MenuContextValue = {
|
|
6
12
|
open: boolean;
|
|
7
13
|
setOpen: MenuSetOpen;
|
|
8
14
|
modal: boolean;
|
|
9
15
|
triggerRef: React.MutableRefObject<GuiObject | undefined>;
|
|
10
16
|
contentRef: React.MutableRefObject<GuiObject | undefined>;
|
|
17
|
+
registerItem: (item: MenuItemRegistration) => () => void;
|
|
18
|
+
focusFirstItem: () => void;
|
|
19
|
+
moveSelection: (direction: -1 | 1) => void;
|
|
20
|
+
restoreTriggerFocus: () => void;
|
|
11
21
|
};
|
|
12
22
|
export type MenuProps = {
|
|
13
23
|
open?: boolean;
|
|
@@ -32,8 +42,6 @@ export type MenuContentProps = {
|
|
|
32
42
|
placement?: PopperPlacement;
|
|
33
43
|
offset?: Vector2;
|
|
34
44
|
padding?: number;
|
|
35
|
-
loop?: boolean;
|
|
36
|
-
onEscapeKeyDown?: (event: LayerInteractEvent) => void;
|
|
37
45
|
onPointerDownOutside?: (event: LayerInteractEvent) => void;
|
|
38
46
|
onInteractOutside?: (event: LayerInteractEvent) => void;
|
|
39
47
|
children?: React.ReactNode;
|
package/out/index.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ export declare const Menu: {
|
|
|
16
16
|
readonly Label: typeof MenuLabel;
|
|
17
17
|
readonly Separator: typeof MenuSeparator;
|
|
18
18
|
};
|
|
19
|
-
export { MenuContent, MenuGroup, MenuItem, MenuLabel, MenuPortal, MenuRoot, MenuSeparator, MenuTrigger };
|
|
20
19
|
export type { MenuContentProps, MenuGroupProps, MenuItemProps, MenuLabelProps, MenuPortalProps, MenuProps, MenuSelectEvent, MenuSeparatorProps, MenuTriggerProps, } from "./Menu/types";
|
|
20
|
+
export { MenuContent, MenuGroup, MenuItem, MenuLabel, MenuPortal, MenuRoot, MenuSeparator, MenuTrigger };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lattice-ui/menu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "out/init.luau",
|
|
6
6
|
"types": "out/index.d.ts",
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"README.md"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@lattice-ui/core": "0.
|
|
13
|
-
"@lattice-ui/focus": "0.
|
|
14
|
-
"@lattice-ui/
|
|
15
|
-
"@lattice-ui/
|
|
12
|
+
"@lattice-ui/core": "0.4.0",
|
|
13
|
+
"@lattice-ui/focus": "0.4.0",
|
|
14
|
+
"@lattice-ui/layer": "0.4.0",
|
|
15
|
+
"@lattice-ui/popper": "0.4.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@rbxts/react": "17.3.7-ts.1",
|