@lattice-ui/menu 0.1.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/Menu/MenuContent.d.ts +3 -0
- package/out/Menu/MenuContent.luau +117 -0
- package/out/Menu/MenuGroup.d.ts +3 -0
- package/out/Menu/MenuGroup.luau +25 -0
- package/out/Menu/MenuItem.d.ts +3 -0
- package/out/Menu/MenuItem.luau +73 -0
- package/out/Menu/MenuLabel.d.ts +3 -0
- package/out/Menu/MenuLabel.luau +27 -0
- package/out/Menu/MenuPortal.d.ts +3 -0
- package/out/Menu/MenuPortal.luau +33 -0
- package/out/Menu/MenuRoot.d.ts +3 -0
- package/out/Menu/MenuRoot.luau +46 -0
- package/out/Menu/MenuSeparator.d.ts +3 -0
- package/out/Menu/MenuSeparator.luau +22 -0
- package/out/Menu/MenuTrigger.d.ts +3 -0
- package/out/Menu/MenuTrigger.luau +54 -0
- package/out/Menu/context.d.ts +3 -0
- package/out/Menu/context.luau +10 -0
- package/out/Menu/types.d.ts +62 -0
- package/out/Menu/types.luau +2 -0
- package/out/index.d.ts +9 -0
- package/out/init.luau +12 -0
- package/package.json +26 -0
- package/src/Menu/MenuContent.tsx +146 -0
- package/src/Menu/MenuGroup.tsx +24 -0
- package/src/Menu/MenuItem.tsx +72 -0
- package/src/Menu/MenuLabel.tsx +27 -0
- package/src/Menu/MenuPortal.tsx +28 -0
- package/src/Menu/MenuRoot.tsx +35 -0
- package/src/Menu/MenuSeparator.tsx +15 -0
- package/src/Menu/MenuTrigger.tsx +61 -0
- package/src/Menu/context.ts +6 -0
- package/src/Menu/types.ts +73 -0
- package/src/index.ts +19 -0
- package/tsconfig.json +16 -0
- package/tsconfig.typecheck.json +25 -0
|
@@ -0,0 +1,117 @@
|
|
|
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 RovingFocusGroup = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusGroup
|
|
7
|
+
local _layer = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out)
|
|
8
|
+
local DismissableLayer = _layer.DismissableLayer
|
|
9
|
+
local Presence = _layer.Presence
|
|
10
|
+
local usePopper = TS.import(script, TS.getModule(script, "@lattice-ui", "popper").out).usePopper
|
|
11
|
+
local useMenuContext = TS.import(script, script.Parent, "context").useMenuContext
|
|
12
|
+
local function toGuiObject(instance)
|
|
13
|
+
if not instance or not instance:IsA("GuiObject") then
|
|
14
|
+
return nil
|
|
15
|
+
end
|
|
16
|
+
return instance
|
|
17
|
+
end
|
|
18
|
+
local function MenuContentImpl(props)
|
|
19
|
+
local menuContext = useMenuContext()
|
|
20
|
+
local popper = usePopper({
|
|
21
|
+
anchorRef = menuContext.triggerRef,
|
|
22
|
+
contentRef = menuContext.contentRef,
|
|
23
|
+
placement = props.placement,
|
|
24
|
+
offset = props.offset,
|
|
25
|
+
padding = props.padding,
|
|
26
|
+
enabled = props.enabled,
|
|
27
|
+
})
|
|
28
|
+
local setContentRef = React.useCallback(function(instance)
|
|
29
|
+
menuContext.contentRef.current = toGuiObject(instance)
|
|
30
|
+
end, { menuContext.contentRef })
|
|
31
|
+
local contentNode = if props.asChild then ((function()
|
|
32
|
+
local child = props.children
|
|
33
|
+
if not React.isValidElement(child) then
|
|
34
|
+
error("[MenuContent] `asChild` requires a child element.")
|
|
35
|
+
end
|
|
36
|
+
return React.createElement(Slot, {
|
|
37
|
+
AnchorPoint = popper.anchorPoint,
|
|
38
|
+
Position = popper.position,
|
|
39
|
+
Visible = props.visible,
|
|
40
|
+
ref = setContentRef,
|
|
41
|
+
}, child)
|
|
42
|
+
end)()) else (React.createElement("frame", {
|
|
43
|
+
AnchorPoint = popper.anchorPoint,
|
|
44
|
+
BackgroundTransparency = 1,
|
|
45
|
+
BorderSizePixel = 0,
|
|
46
|
+
Position = popper.position,
|
|
47
|
+
Size = UDim2.fromOffset(0, 0),
|
|
48
|
+
Visible = props.visible,
|
|
49
|
+
ref = setContentRef,
|
|
50
|
+
}, props.children))
|
|
51
|
+
return React.createElement(DismissableLayer, {
|
|
52
|
+
enabled = props.enabled,
|
|
53
|
+
modal = menuContext.modal,
|
|
54
|
+
onDismiss = props.onDismiss,
|
|
55
|
+
onEscapeKeyDown = props.onEscapeKeyDown,
|
|
56
|
+
onInteractOutside = props.onInteractOutside,
|
|
57
|
+
onPointerDownOutside = props.onPointerDownOutside,
|
|
58
|
+
}, React.createElement(RovingFocusGroup, {
|
|
59
|
+
active = props.enabled,
|
|
60
|
+
autoFocus = "first",
|
|
61
|
+
loop = props.loop,
|
|
62
|
+
orientation = "vertical",
|
|
63
|
+
}, contentNode))
|
|
64
|
+
end
|
|
65
|
+
local function MenuContent(props)
|
|
66
|
+
local menuContext = useMenuContext()
|
|
67
|
+
local open = menuContext.open
|
|
68
|
+
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
|
+
local handleDismiss = React.useCallback(function()
|
|
75
|
+
menuContext.setOpen(false)
|
|
76
|
+
end, { menuContext.setOpen })
|
|
77
|
+
if not open and not forceMount then
|
|
78
|
+
return nil
|
|
79
|
+
end
|
|
80
|
+
if forceMount then
|
|
81
|
+
return React.createElement(MenuContentImpl, {
|
|
82
|
+
asChild = props.asChild,
|
|
83
|
+
enabled = open,
|
|
84
|
+
loop = loop,
|
|
85
|
+
offset = props.offset,
|
|
86
|
+
onDismiss = handleDismiss,
|
|
87
|
+
onEscapeKeyDown = props.onEscapeKeyDown,
|
|
88
|
+
onInteractOutside = props.onInteractOutside,
|
|
89
|
+
onPointerDownOutside = props.onPointerDownOutside,
|
|
90
|
+
padding = props.padding,
|
|
91
|
+
placement = props.placement,
|
|
92
|
+
visible = open,
|
|
93
|
+
}, props.children)
|
|
94
|
+
end
|
|
95
|
+
return React.createElement(Presence, {
|
|
96
|
+
exitFallbackMs = 0,
|
|
97
|
+
present = open,
|
|
98
|
+
render = function(state)
|
|
99
|
+
return React.createElement(MenuContentImpl, {
|
|
100
|
+
asChild = props.asChild,
|
|
101
|
+
enabled = state.isPresent,
|
|
102
|
+
loop = loop,
|
|
103
|
+
offset = props.offset,
|
|
104
|
+
onDismiss = handleDismiss,
|
|
105
|
+
onEscapeKeyDown = props.onEscapeKeyDown,
|
|
106
|
+
onInteractOutside = props.onInteractOutside,
|
|
107
|
+
onPointerDownOutside = props.onPointerDownOutside,
|
|
108
|
+
padding = props.padding,
|
|
109
|
+
placement = props.placement,
|
|
110
|
+
visible = state.isPresent,
|
|
111
|
+
}, props.children)
|
|
112
|
+
end,
|
|
113
|
+
})
|
|
114
|
+
end
|
|
115
|
+
return {
|
|
116
|
+
MenuContent = MenuContent,
|
|
117
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 function MenuGroup(props)
|
|
7
|
+
if props.asChild then
|
|
8
|
+
local child = props.children
|
|
9
|
+
if not child then
|
|
10
|
+
error("[MenuGroup] `asChild` requires a child element.")
|
|
11
|
+
end
|
|
12
|
+
return React.createElement(Slot, nil, child)
|
|
13
|
+
end
|
|
14
|
+
return React.createElement("frame", {
|
|
15
|
+
BackgroundTransparency = 1,
|
|
16
|
+
Size = UDim2.fromOffset(220, 0),
|
|
17
|
+
}, React.createElement("uilistlayout", {
|
|
18
|
+
FillDirection = Enum.FillDirection.Vertical,
|
|
19
|
+
Padding = UDim.new(0, 4),
|
|
20
|
+
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
21
|
+
}), props.children)
|
|
22
|
+
end
|
|
23
|
+
return {
|
|
24
|
+
MenuGroup = MenuGroup,
|
|
25
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 RovingFocusItem = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusItem
|
|
7
|
+
local useMenuContext = TS.import(script, script.Parent, "context").useMenuContext
|
|
8
|
+
local function createMenuSelectEvent()
|
|
9
|
+
local event
|
|
10
|
+
event = {
|
|
11
|
+
defaultPrevented = false,
|
|
12
|
+
preventDefault = function()
|
|
13
|
+
event.defaultPrevented = true
|
|
14
|
+
end,
|
|
15
|
+
}
|
|
16
|
+
return event
|
|
17
|
+
end
|
|
18
|
+
local function MenuItem(props)
|
|
19
|
+
local menuContext = useMenuContext()
|
|
20
|
+
local handleActivated = React.useCallback(function()
|
|
21
|
+
if props.disabled then
|
|
22
|
+
return nil
|
|
23
|
+
end
|
|
24
|
+
local event = createMenuSelectEvent()
|
|
25
|
+
local _result = props.onSelect
|
|
26
|
+
if _result ~= nil then
|
|
27
|
+
_result(event)
|
|
28
|
+
end
|
|
29
|
+
if not event.defaultPrevented then
|
|
30
|
+
menuContext.setOpen(false)
|
|
31
|
+
end
|
|
32
|
+
end, { menuContext, props.disabled, props.onSelect })
|
|
33
|
+
if props.asChild then
|
|
34
|
+
local child = props.children
|
|
35
|
+
if not child then
|
|
36
|
+
error("[MenuItem] `asChild` requires a child element.")
|
|
37
|
+
end
|
|
38
|
+
return React.createElement(RovingFocusItem, {
|
|
39
|
+
asChild = true,
|
|
40
|
+
disabled = props.disabled,
|
|
41
|
+
}, React.createElement(Slot, {
|
|
42
|
+
Active = props.disabled ~= true,
|
|
43
|
+
Event = {
|
|
44
|
+
Activated = handleActivated,
|
|
45
|
+
},
|
|
46
|
+
Selectable = props.disabled ~= true,
|
|
47
|
+
}, child))
|
|
48
|
+
end
|
|
49
|
+
return React.createElement(RovingFocusItem, {
|
|
50
|
+
asChild = true,
|
|
51
|
+
disabled = props.disabled,
|
|
52
|
+
}, React.createElement("textbutton", {
|
|
53
|
+
Active = props.disabled ~= true,
|
|
54
|
+
AutoButtonColor = false,
|
|
55
|
+
BackgroundColor3 = Color3.fromRGB(47, 53, 68),
|
|
56
|
+
BorderSizePixel = 0,
|
|
57
|
+
Event = {
|
|
58
|
+
Activated = handleActivated,
|
|
59
|
+
},
|
|
60
|
+
Selectable = props.disabled ~= true,
|
|
61
|
+
Size = UDim2.fromOffset(220, 34),
|
|
62
|
+
Text = "Menu Item",
|
|
63
|
+
TextColor3 = if props.disabled then Color3.fromRGB(135, 142, 156) else Color3.fromRGB(234, 239, 247),
|
|
64
|
+
TextSize = 15,
|
|
65
|
+
TextXAlignment = Enum.TextXAlignment.Left,
|
|
66
|
+
}, React.createElement("uipadding", {
|
|
67
|
+
PaddingLeft = UDim.new(0, 10),
|
|
68
|
+
PaddingRight = UDim.new(0, 10),
|
|
69
|
+
}), props.children))
|
|
70
|
+
end
|
|
71
|
+
return {
|
|
72
|
+
MenuItem = MenuItem,
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 function MenuLabel(props)
|
|
7
|
+
if props.asChild then
|
|
8
|
+
local child = props.children
|
|
9
|
+
if not child then
|
|
10
|
+
error("[MenuLabel] `asChild` requires a child element.")
|
|
11
|
+
end
|
|
12
|
+
return React.createElement(Slot, nil, child)
|
|
13
|
+
end
|
|
14
|
+
return React.createElement("textlabel", {
|
|
15
|
+
BackgroundTransparency = 1,
|
|
16
|
+
Size = UDim2.fromOffset(220, 24),
|
|
17
|
+
Text = "Label",
|
|
18
|
+
TextColor3 = Color3.fromRGB(162, 173, 191),
|
|
19
|
+
TextSize = 14,
|
|
20
|
+
TextXAlignment = Enum.TextXAlignment.Left,
|
|
21
|
+
}, React.createElement("uipadding", {
|
|
22
|
+
PaddingLeft = UDim.new(0, 10),
|
|
23
|
+
}), props.children)
|
|
24
|
+
end
|
|
25
|
+
return {
|
|
26
|
+
MenuLabel = MenuLabel,
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 _layer = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out)
|
|
5
|
+
local Portal = _layer.Portal
|
|
6
|
+
local PortalProvider = _layer.PortalProvider
|
|
7
|
+
local usePortalContext = _layer.usePortalContext
|
|
8
|
+
local function MenuPortalWithOverrides(props)
|
|
9
|
+
local portalContext = usePortalContext()
|
|
10
|
+
local container = props.container or portalContext.container
|
|
11
|
+
local _condition = props.displayOrderBase
|
|
12
|
+
if _condition == nil then
|
|
13
|
+
_condition = portalContext.displayOrderBase
|
|
14
|
+
end
|
|
15
|
+
local displayOrderBase = _condition
|
|
16
|
+
return React.createElement(PortalProvider, {
|
|
17
|
+
container = container,
|
|
18
|
+
displayOrderBase = displayOrderBase,
|
|
19
|
+
}, React.createElement(Portal, nil, props.children))
|
|
20
|
+
end
|
|
21
|
+
local function MenuPortal(props)
|
|
22
|
+
local hasOverrides = props.container ~= nil or props.displayOrderBase ~= nil
|
|
23
|
+
if hasOverrides then
|
|
24
|
+
return React.createElement(MenuPortalWithOverrides, {
|
|
25
|
+
container = props.container,
|
|
26
|
+
displayOrderBase = props.displayOrderBase,
|
|
27
|
+
}, props.children)
|
|
28
|
+
end
|
|
29
|
+
return React.createElement(Portal, nil, props.children)
|
|
30
|
+
end
|
|
31
|
+
return {
|
|
32
|
+
MenuPortal = MenuPortal,
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 useControllableState = _core.useControllableState
|
|
6
|
+
local MenuContextProvider = TS.import(script, script.Parent, "context").MenuContextProvider
|
|
7
|
+
local function Menu(props)
|
|
8
|
+
local _object = {
|
|
9
|
+
value = props.open,
|
|
10
|
+
}
|
|
11
|
+
local _left = "defaultValue"
|
|
12
|
+
local _condition = props.defaultOpen
|
|
13
|
+
if _condition == nil then
|
|
14
|
+
_condition = false
|
|
15
|
+
end
|
|
16
|
+
_object[_left] = _condition
|
|
17
|
+
_object.onChange = props.onOpenChange
|
|
18
|
+
local _binding = useControllableState(_object)
|
|
19
|
+
local open = _binding[1]
|
|
20
|
+
local setOpenState = _binding[2]
|
|
21
|
+
local _condition_1 = props.modal
|
|
22
|
+
if _condition_1 == nil then
|
|
23
|
+
_condition_1 = true
|
|
24
|
+
end
|
|
25
|
+
local modal = _condition_1
|
|
26
|
+
local triggerRef = React.useRef()
|
|
27
|
+
local contentRef = React.useRef()
|
|
28
|
+
local setOpen = React.useCallback(function(nextOpen)
|
|
29
|
+
setOpenState(nextOpen)
|
|
30
|
+
end, { setOpenState })
|
|
31
|
+
local contextValue = React.useMemo(function()
|
|
32
|
+
return {
|
|
33
|
+
open = open,
|
|
34
|
+
setOpen = setOpen,
|
|
35
|
+
modal = modal,
|
|
36
|
+
triggerRef = triggerRef,
|
|
37
|
+
contentRef = contentRef,
|
|
38
|
+
}
|
|
39
|
+
end, { modal, open, setOpen })
|
|
40
|
+
return React.createElement(MenuContextProvider, {
|
|
41
|
+
value = contextValue,
|
|
42
|
+
}, props.children)
|
|
43
|
+
end
|
|
44
|
+
return {
|
|
45
|
+
Menu = Menu,
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 function MenuSeparator(props)
|
|
7
|
+
if props.asChild then
|
|
8
|
+
local child = props.children
|
|
9
|
+
if not child then
|
|
10
|
+
error("[MenuSeparator] `asChild` requires a child element.")
|
|
11
|
+
end
|
|
12
|
+
return React.createElement(Slot, nil, child)
|
|
13
|
+
end
|
|
14
|
+
return React.createElement("frame", {
|
|
15
|
+
BackgroundColor3 = Color3.fromRGB(72, 79, 97),
|
|
16
|
+
BorderSizePixel = 0,
|
|
17
|
+
Size = UDim2.fromOffset(220, 1),
|
|
18
|
+
})
|
|
19
|
+
end
|
|
20
|
+
return {
|
|
21
|
+
MenuSeparator = MenuSeparator,
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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 useMenuContext = TS.import(script, script.Parent, "context").useMenuContext
|
|
7
|
+
local function toGuiObject(instance)
|
|
8
|
+
if not instance or not instance:IsA("GuiObject") then
|
|
9
|
+
return nil
|
|
10
|
+
end
|
|
11
|
+
return instance
|
|
12
|
+
end
|
|
13
|
+
local function MenuTrigger(props)
|
|
14
|
+
local menuContext = useMenuContext()
|
|
15
|
+
local setTriggerRef = React.useCallback(function(instance)
|
|
16
|
+
menuContext.triggerRef.current = toGuiObject(instance)
|
|
17
|
+
end, { menuContext.triggerRef })
|
|
18
|
+
local handleActivated = React.useCallback(function()
|
|
19
|
+
if props.disabled then
|
|
20
|
+
return nil
|
|
21
|
+
end
|
|
22
|
+
menuContext.setOpen(not menuContext.open)
|
|
23
|
+
end, { menuContext.open, menuContext.setOpen, props.disabled })
|
|
24
|
+
if props.asChild then
|
|
25
|
+
local child = props.children
|
|
26
|
+
if not child then
|
|
27
|
+
error("[MenuTrigger] `asChild` requires a child element.")
|
|
28
|
+
end
|
|
29
|
+
return React.createElement(Slot, {
|
|
30
|
+
Event = {
|
|
31
|
+
Activated = handleActivated,
|
|
32
|
+
},
|
|
33
|
+
ref = setTriggerRef,
|
|
34
|
+
}, child)
|
|
35
|
+
end
|
|
36
|
+
return React.createElement("textbutton", {
|
|
37
|
+
Active = props.disabled ~= true,
|
|
38
|
+
AutoButtonColor = false,
|
|
39
|
+
BackgroundTransparency = 1,
|
|
40
|
+
BorderSizePixel = 0,
|
|
41
|
+
Event = {
|
|
42
|
+
Activated = handleActivated,
|
|
43
|
+
},
|
|
44
|
+
Selectable = props.disabled ~= true,
|
|
45
|
+
Size = UDim2.fromOffset(140, 38),
|
|
46
|
+
Text = "Toggle Menu",
|
|
47
|
+
TextColor3 = Color3.fromRGB(240, 244, 250),
|
|
48
|
+
TextSize = 16,
|
|
49
|
+
ref = setTriggerRef,
|
|
50
|
+
}, props.children)
|
|
51
|
+
end
|
|
52
|
+
return {
|
|
53
|
+
MenuTrigger = MenuTrigger,
|
|
54
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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("Menu")
|
|
5
|
+
local MenuContextProvider = _binding[1]
|
|
6
|
+
local useMenuContext = _binding[2]
|
|
7
|
+
return {
|
|
8
|
+
MenuContextProvider = MenuContextProvider,
|
|
9
|
+
useMenuContext = useMenuContext,
|
|
10
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LayerInteractEvent } from "@lattice-ui/layer";
|
|
2
|
+
import type { PopperPlacement } from "@lattice-ui/popper";
|
|
3
|
+
import type React from "@rbxts/react";
|
|
4
|
+
export type MenuSetOpen = (open: boolean) => void;
|
|
5
|
+
export type MenuContextValue = {
|
|
6
|
+
open: boolean;
|
|
7
|
+
setOpen: MenuSetOpen;
|
|
8
|
+
modal: boolean;
|
|
9
|
+
triggerRef: React.MutableRefObject<GuiObject | undefined>;
|
|
10
|
+
contentRef: React.MutableRefObject<GuiObject | undefined>;
|
|
11
|
+
};
|
|
12
|
+
export type MenuProps = {
|
|
13
|
+
open?: boolean;
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
modal?: boolean;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
};
|
|
19
|
+
export type MenuTriggerProps = {
|
|
20
|
+
asChild?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
children?: React.ReactElement;
|
|
23
|
+
};
|
|
24
|
+
export type MenuPortalProps = {
|
|
25
|
+
container?: BasePlayerGui;
|
|
26
|
+
displayOrderBase?: number;
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
};
|
|
29
|
+
export type MenuContentProps = {
|
|
30
|
+
asChild?: boolean;
|
|
31
|
+
forceMount?: boolean;
|
|
32
|
+
placement?: PopperPlacement;
|
|
33
|
+
offset?: Vector2;
|
|
34
|
+
padding?: number;
|
|
35
|
+
loop?: boolean;
|
|
36
|
+
onEscapeKeyDown?: (event: LayerInteractEvent) => void;
|
|
37
|
+
onPointerDownOutside?: (event: LayerInteractEvent) => void;
|
|
38
|
+
onInteractOutside?: (event: LayerInteractEvent) => void;
|
|
39
|
+
children?: React.ReactNode;
|
|
40
|
+
};
|
|
41
|
+
export type MenuSelectEvent = {
|
|
42
|
+
defaultPrevented: boolean;
|
|
43
|
+
preventDefault: () => void;
|
|
44
|
+
};
|
|
45
|
+
export type MenuItemProps = {
|
|
46
|
+
asChild?: boolean;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
onSelect?: (event: MenuSelectEvent) => void;
|
|
49
|
+
children?: React.ReactElement;
|
|
50
|
+
};
|
|
51
|
+
export type MenuSeparatorProps = {
|
|
52
|
+
asChild?: boolean;
|
|
53
|
+
children?: React.ReactElement;
|
|
54
|
+
};
|
|
55
|
+
export type MenuGroupProps = {
|
|
56
|
+
asChild?: boolean;
|
|
57
|
+
children?: React.ReactElement;
|
|
58
|
+
};
|
|
59
|
+
export type MenuLabelProps = {
|
|
60
|
+
asChild?: boolean;
|
|
61
|
+
children?: React.ReactElement;
|
|
62
|
+
};
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { MenuContent } from "./Menu/MenuContent";
|
|
2
|
+
export { MenuGroup } from "./Menu/MenuGroup";
|
|
3
|
+
export { MenuItem } from "./Menu/MenuItem";
|
|
4
|
+
export { MenuLabel } from "./Menu/MenuLabel";
|
|
5
|
+
export { MenuPortal } from "./Menu/MenuPortal";
|
|
6
|
+
export { Menu } from "./Menu/MenuRoot";
|
|
7
|
+
export { MenuSeparator } from "./Menu/MenuSeparator";
|
|
8
|
+
export { MenuTrigger } from "./Menu/MenuTrigger";
|
|
9
|
+
export type { MenuContentProps, MenuGroupProps, MenuItemProps, MenuLabelProps, MenuPortalProps, MenuProps, MenuSelectEvent, MenuSeparatorProps, MenuTriggerProps, } from "./Menu/types";
|
package/out/init.luau
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local exports = {}
|
|
4
|
+
exports.MenuContent = TS.import(script, script, "Menu", "MenuContent").MenuContent
|
|
5
|
+
exports.MenuGroup = TS.import(script, script, "Menu", "MenuGroup").MenuGroup
|
|
6
|
+
exports.MenuItem = TS.import(script, script, "Menu", "MenuItem").MenuItem
|
|
7
|
+
exports.MenuLabel = TS.import(script, script, "Menu", "MenuLabel").MenuLabel
|
|
8
|
+
exports.MenuPortal = TS.import(script, script, "Menu", "MenuPortal").MenuPortal
|
|
9
|
+
exports.Menu = TS.import(script, script, "Menu", "MenuRoot").Menu
|
|
10
|
+
exports.MenuSeparator = TS.import(script, script, "Menu", "MenuSeparator").MenuSeparator
|
|
11
|
+
exports.MenuTrigger = TS.import(script, script, "Menu", "MenuTrigger").MenuTrigger
|
|
12
|
+
return exports
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lattice-ui/menu",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "out/init.luau",
|
|
6
|
+
"types": "out/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@lattice-ui/core": "0.1.1",
|
|
9
|
+
"@lattice-ui/focus": "0.1.1",
|
|
10
|
+
"@lattice-ui/layer": "0.1.1",
|
|
11
|
+
"@lattice-ui/popper": "0.1.1"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@rbxts/react": "17.3.7-ts.1",
|
|
15
|
+
"@rbxts/react-roblox": "17.3.7-ts.1"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@rbxts/react": "^17",
|
|
19
|
+
"@rbxts/react-roblox": "^17"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "rbxtsc -p tsconfig.json",
|
|
23
|
+
"watch": "rbxtsc -p tsconfig.json -w",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { RovingFocusGroup } from "@lattice-ui/focus";
|
|
3
|
+
import { DismissableLayer, Presence } from "@lattice-ui/layer";
|
|
4
|
+
import { usePopper } from "@lattice-ui/popper";
|
|
5
|
+
import { useMenuContext } from "./context";
|
|
6
|
+
import type { MenuContentProps } from "./types";
|
|
7
|
+
|
|
8
|
+
type MenuContentImplProps = {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
visible: boolean;
|
|
11
|
+
onDismiss: () => void;
|
|
12
|
+
loop: boolean;
|
|
13
|
+
asChild?: boolean;
|
|
14
|
+
placement?: MenuContentProps["placement"];
|
|
15
|
+
offset?: MenuContentProps["offset"];
|
|
16
|
+
padding?: MenuContentProps["padding"];
|
|
17
|
+
} & Pick<MenuContentProps, "children" | "onEscapeKeyDown" | "onInteractOutside" | "onPointerDownOutside">;
|
|
18
|
+
|
|
19
|
+
function toGuiObject(instance: Instance | undefined) {
|
|
20
|
+
if (!instance || !instance.IsA("GuiObject")) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function MenuContentImpl(props: MenuContentImplProps) {
|
|
28
|
+
const menuContext = useMenuContext();
|
|
29
|
+
|
|
30
|
+
const popper = usePopper({
|
|
31
|
+
anchorRef: menuContext.triggerRef,
|
|
32
|
+
contentRef: menuContext.contentRef,
|
|
33
|
+
placement: props.placement,
|
|
34
|
+
offset: props.offset,
|
|
35
|
+
padding: props.padding,
|
|
36
|
+
enabled: props.enabled,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const setContentRef = React.useCallback(
|
|
40
|
+
(instance: Instance | undefined) => {
|
|
41
|
+
menuContext.contentRef.current = toGuiObject(instance);
|
|
42
|
+
},
|
|
43
|
+
[menuContext.contentRef],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const contentNode = props.asChild ? (
|
|
47
|
+
(() => {
|
|
48
|
+
const child = props.children;
|
|
49
|
+
if (!React.isValidElement(child)) {
|
|
50
|
+
error("[MenuContent] `asChild` requires a child element.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Slot AnchorPoint={popper.anchorPoint} Position={popper.position} Visible={props.visible} ref={setContentRef}>
|
|
55
|
+
{child}
|
|
56
|
+
</Slot>
|
|
57
|
+
);
|
|
58
|
+
})()
|
|
59
|
+
) : (
|
|
60
|
+
<frame
|
|
61
|
+
AnchorPoint={popper.anchorPoint}
|
|
62
|
+
BackgroundTransparency={1}
|
|
63
|
+
BorderSizePixel={0}
|
|
64
|
+
Position={popper.position}
|
|
65
|
+
Size={UDim2.fromOffset(0, 0)}
|
|
66
|
+
Visible={props.visible}
|
|
67
|
+
ref={setContentRef}
|
|
68
|
+
>
|
|
69
|
+
{props.children}
|
|
70
|
+
</frame>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<DismissableLayer
|
|
75
|
+
enabled={props.enabled}
|
|
76
|
+
modal={menuContext.modal}
|
|
77
|
+
onDismiss={props.onDismiss}
|
|
78
|
+
onEscapeKeyDown={props.onEscapeKeyDown}
|
|
79
|
+
onInteractOutside={props.onInteractOutside}
|
|
80
|
+
onPointerDownOutside={props.onPointerDownOutside}
|
|
81
|
+
>
|
|
82
|
+
<RovingFocusGroup active={props.enabled} autoFocus="first" loop={props.loop} orientation="vertical">
|
|
83
|
+
{contentNode}
|
|
84
|
+
</RovingFocusGroup>
|
|
85
|
+
</DismissableLayer>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function MenuContent(props: MenuContentProps) {
|
|
90
|
+
const menuContext = useMenuContext();
|
|
91
|
+
const open = menuContext.open;
|
|
92
|
+
const forceMount = props.forceMount === true;
|
|
93
|
+
const loop = props.loop ?? true;
|
|
94
|
+
|
|
95
|
+
const handleDismiss = React.useCallback(() => {
|
|
96
|
+
menuContext.setOpen(false);
|
|
97
|
+
}, [menuContext.setOpen]);
|
|
98
|
+
|
|
99
|
+
if (!open && !forceMount) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (forceMount) {
|
|
104
|
+
return (
|
|
105
|
+
<MenuContentImpl
|
|
106
|
+
asChild={props.asChild}
|
|
107
|
+
enabled={open}
|
|
108
|
+
loop={loop}
|
|
109
|
+
offset={props.offset}
|
|
110
|
+
onDismiss={handleDismiss}
|
|
111
|
+
onEscapeKeyDown={props.onEscapeKeyDown}
|
|
112
|
+
onInteractOutside={props.onInteractOutside}
|
|
113
|
+
onPointerDownOutside={props.onPointerDownOutside}
|
|
114
|
+
padding={props.padding}
|
|
115
|
+
placement={props.placement}
|
|
116
|
+
visible={open}
|
|
117
|
+
>
|
|
118
|
+
{props.children}
|
|
119
|
+
</MenuContentImpl>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Presence
|
|
125
|
+
exitFallbackMs={0}
|
|
126
|
+
present={open}
|
|
127
|
+
render={(state) => (
|
|
128
|
+
<MenuContentImpl
|
|
129
|
+
asChild={props.asChild}
|
|
130
|
+
enabled={state.isPresent}
|
|
131
|
+
loop={loop}
|
|
132
|
+
offset={props.offset}
|
|
133
|
+
onDismiss={handleDismiss}
|
|
134
|
+
onEscapeKeyDown={props.onEscapeKeyDown}
|
|
135
|
+
onInteractOutside={props.onInteractOutside}
|
|
136
|
+
onPointerDownOutside={props.onPointerDownOutside}
|
|
137
|
+
padding={props.padding}
|
|
138
|
+
placement={props.placement}
|
|
139
|
+
visible={state.isPresent}
|
|
140
|
+
>
|
|
141
|
+
{props.children}
|
|
142
|
+
</MenuContentImpl>
|
|
143
|
+
)}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import type { MenuGroupProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export function MenuGroup(props: MenuGroupProps) {
|
|
5
|
+
if (props.asChild) {
|
|
6
|
+
const child = props.children;
|
|
7
|
+
if (!child) {
|
|
8
|
+
error("[MenuGroup] `asChild` requires a child element.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return <Slot>{child}</Slot>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(220, 0)}>
|
|
16
|
+
<uilistlayout
|
|
17
|
+
FillDirection={Enum.FillDirection.Vertical}
|
|
18
|
+
Padding={new UDim(0, 4)}
|
|
19
|
+
SortOrder={Enum.SortOrder.LayoutOrder}
|
|
20
|
+
/>
|
|
21
|
+
{props.children}
|
|
22
|
+
</frame>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { RovingFocusItem } from "@lattice-ui/focus";
|
|
3
|
+
import { useMenuContext } from "./context";
|
|
4
|
+
import type { MenuItemProps, MenuSelectEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
function createMenuSelectEvent(): MenuSelectEvent {
|
|
7
|
+
const event: MenuSelectEvent = {
|
|
8
|
+
defaultPrevented: false,
|
|
9
|
+
preventDefault: () => {
|
|
10
|
+
event.defaultPrevented = true;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return event;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function MenuItem(props: MenuItemProps) {
|
|
18
|
+
const menuContext = useMenuContext();
|
|
19
|
+
|
|
20
|
+
const handleActivated = React.useCallback(() => {
|
|
21
|
+
if (props.disabled) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const event = createMenuSelectEvent();
|
|
26
|
+
props.onSelect?.(event);
|
|
27
|
+
|
|
28
|
+
if (!event.defaultPrevented) {
|
|
29
|
+
menuContext.setOpen(false);
|
|
30
|
+
}
|
|
31
|
+
}, [menuContext, props.disabled, props.onSelect]);
|
|
32
|
+
|
|
33
|
+
if (props.asChild) {
|
|
34
|
+
const child = props.children;
|
|
35
|
+
if (!child) {
|
|
36
|
+
error("[MenuItem] `asChild` requires a child element.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<RovingFocusItem asChild disabled={props.disabled}>
|
|
41
|
+
<Slot
|
|
42
|
+
Active={props.disabled !== true}
|
|
43
|
+
Event={{ Activated: handleActivated }}
|
|
44
|
+
Selectable={props.disabled !== true}
|
|
45
|
+
>
|
|
46
|
+
{child}
|
|
47
|
+
</Slot>
|
|
48
|
+
</RovingFocusItem>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<RovingFocusItem asChild disabled={props.disabled}>
|
|
54
|
+
<textbutton
|
|
55
|
+
Active={props.disabled !== true}
|
|
56
|
+
AutoButtonColor={false}
|
|
57
|
+
BackgroundColor3={Color3.fromRGB(47, 53, 68)}
|
|
58
|
+
BorderSizePixel={0}
|
|
59
|
+
Event={{ Activated: handleActivated }}
|
|
60
|
+
Selectable={props.disabled !== true}
|
|
61
|
+
Size={UDim2.fromOffset(220, 34)}
|
|
62
|
+
Text="Menu Item"
|
|
63
|
+
TextColor3={props.disabled ? Color3.fromRGB(135, 142, 156) : Color3.fromRGB(234, 239, 247)}
|
|
64
|
+
TextSize={15}
|
|
65
|
+
TextXAlignment={Enum.TextXAlignment.Left}
|
|
66
|
+
>
|
|
67
|
+
<uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 10)} />
|
|
68
|
+
{props.children}
|
|
69
|
+
</textbutton>
|
|
70
|
+
</RovingFocusItem>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import type { MenuLabelProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export function MenuLabel(props: MenuLabelProps) {
|
|
5
|
+
if (props.asChild) {
|
|
6
|
+
const child = props.children;
|
|
7
|
+
if (!child) {
|
|
8
|
+
error("[MenuLabel] `asChild` requires a child element.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return <Slot>{child}</Slot>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<textlabel
|
|
16
|
+
BackgroundTransparency={1}
|
|
17
|
+
Size={UDim2.fromOffset(220, 24)}
|
|
18
|
+
Text="Label"
|
|
19
|
+
TextColor3={Color3.fromRGB(162, 173, 191)}
|
|
20
|
+
TextSize={14}
|
|
21
|
+
TextXAlignment={Enum.TextXAlignment.Left}
|
|
22
|
+
>
|
|
23
|
+
<uipadding PaddingLeft={new UDim(0, 10)} />
|
|
24
|
+
{props.children}
|
|
25
|
+
</textlabel>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { React } from "@lattice-ui/core";
|
|
2
|
+
import { Portal, PortalProvider, usePortalContext } from "@lattice-ui/layer";
|
|
3
|
+
import type { MenuPortalProps } from "./types";
|
|
4
|
+
|
|
5
|
+
function MenuPortalWithOverrides(props: MenuPortalProps) {
|
|
6
|
+
const portalContext = usePortalContext();
|
|
7
|
+
const container = props.container ?? portalContext.container;
|
|
8
|
+
const displayOrderBase = props.displayOrderBase ?? portalContext.displayOrderBase;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<PortalProvider container={container} displayOrderBase={displayOrderBase}>
|
|
12
|
+
<Portal>{props.children}</Portal>
|
|
13
|
+
</PortalProvider>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function MenuPortal(props: MenuPortalProps) {
|
|
18
|
+
const hasOverrides = props.container !== undefined || props.displayOrderBase !== undefined;
|
|
19
|
+
if (hasOverrides) {
|
|
20
|
+
return (
|
|
21
|
+
<MenuPortalWithOverrides container={props.container} displayOrderBase={props.displayOrderBase}>
|
|
22
|
+
{props.children}
|
|
23
|
+
</MenuPortalWithOverrides>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return <Portal>{props.children}</Portal>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { React, useControllableState } from "@lattice-ui/core";
|
|
2
|
+
import { MenuContextProvider } from "./context";
|
|
3
|
+
import type { MenuProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function Menu(props: MenuProps) {
|
|
6
|
+
const [open, setOpenState] = useControllableState<boolean>({
|
|
7
|
+
value: props.open,
|
|
8
|
+
defaultValue: props.defaultOpen ?? false,
|
|
9
|
+
onChange: props.onOpenChange,
|
|
10
|
+
});
|
|
11
|
+
const modal = props.modal ?? true;
|
|
12
|
+
|
|
13
|
+
const triggerRef = React.useRef<GuiObject>();
|
|
14
|
+
const contentRef = React.useRef<GuiObject>();
|
|
15
|
+
|
|
16
|
+
const setOpen = React.useCallback(
|
|
17
|
+
(nextOpen: boolean) => {
|
|
18
|
+
setOpenState(nextOpen);
|
|
19
|
+
},
|
|
20
|
+
[setOpenState],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const contextValue = React.useMemo(
|
|
24
|
+
() => ({
|
|
25
|
+
open,
|
|
26
|
+
setOpen,
|
|
27
|
+
modal,
|
|
28
|
+
triggerRef,
|
|
29
|
+
contentRef,
|
|
30
|
+
}),
|
|
31
|
+
[modal, open, setOpen],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return <MenuContextProvider value={contextValue}>{props.children}</MenuContextProvider>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import type { MenuSeparatorProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export function MenuSeparator(props: MenuSeparatorProps) {
|
|
5
|
+
if (props.asChild) {
|
|
6
|
+
const child = props.children;
|
|
7
|
+
if (!child) {
|
|
8
|
+
error("[MenuSeparator] `asChild` requires a child element.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return <Slot>{child}</Slot>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return <frame BackgroundColor3={Color3.fromRGB(72, 79, 97)} BorderSizePixel={0} Size={UDim2.fromOffset(220, 1)} />;
|
|
15
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { useMenuContext } from "./context";
|
|
3
|
+
import type { MenuTriggerProps } from "./types";
|
|
4
|
+
|
|
5
|
+
function toGuiObject(instance: Instance | undefined) {
|
|
6
|
+
if (!instance || !instance.IsA("GuiObject")) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return instance;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function MenuTrigger(props: MenuTriggerProps) {
|
|
14
|
+
const menuContext = useMenuContext();
|
|
15
|
+
|
|
16
|
+
const setTriggerRef = React.useCallback(
|
|
17
|
+
(instance: Instance | undefined) => {
|
|
18
|
+
menuContext.triggerRef.current = toGuiObject(instance);
|
|
19
|
+
},
|
|
20
|
+
[menuContext.triggerRef],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const handleActivated = React.useCallback(() => {
|
|
24
|
+
if (props.disabled) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
menuContext.setOpen(!menuContext.open);
|
|
29
|
+
}, [menuContext.open, menuContext.setOpen, props.disabled]);
|
|
30
|
+
|
|
31
|
+
if (props.asChild) {
|
|
32
|
+
const child = props.children;
|
|
33
|
+
if (!child) {
|
|
34
|
+
error("[MenuTrigger] `asChild` requires a child element.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Slot Event={{ Activated: handleActivated }} ref={setTriggerRef}>
|
|
39
|
+
{child}
|
|
40
|
+
</Slot>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<textbutton
|
|
46
|
+
Active={props.disabled !== true}
|
|
47
|
+
AutoButtonColor={false}
|
|
48
|
+
BackgroundTransparency={1}
|
|
49
|
+
BorderSizePixel={0}
|
|
50
|
+
Event={{ Activated: handleActivated }}
|
|
51
|
+
Selectable={props.disabled !== true}
|
|
52
|
+
Size={UDim2.fromOffset(140, 38)}
|
|
53
|
+
Text="Toggle Menu"
|
|
54
|
+
TextColor3={Color3.fromRGB(240, 244, 250)}
|
|
55
|
+
TextSize={16}
|
|
56
|
+
ref={setTriggerRef}
|
|
57
|
+
>
|
|
58
|
+
{props.children}
|
|
59
|
+
</textbutton>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { LayerInteractEvent } from "@lattice-ui/layer";
|
|
2
|
+
import type { PopperPlacement } from "@lattice-ui/popper";
|
|
3
|
+
import type React from "@rbxts/react";
|
|
4
|
+
|
|
5
|
+
export type MenuSetOpen = (open: boolean) => void;
|
|
6
|
+
|
|
7
|
+
export type MenuContextValue = {
|
|
8
|
+
open: boolean;
|
|
9
|
+
setOpen: MenuSetOpen;
|
|
10
|
+
modal: boolean;
|
|
11
|
+
triggerRef: React.MutableRefObject<GuiObject | undefined>;
|
|
12
|
+
contentRef: React.MutableRefObject<GuiObject | undefined>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type MenuProps = {
|
|
16
|
+
open?: boolean;
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
onOpenChange?: (open: boolean) => void;
|
|
19
|
+
modal?: boolean;
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type MenuTriggerProps = {
|
|
24
|
+
asChild?: boolean;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
children?: React.ReactElement;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type MenuPortalProps = {
|
|
30
|
+
container?: BasePlayerGui;
|
|
31
|
+
displayOrderBase?: number;
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type MenuContentProps = {
|
|
36
|
+
asChild?: boolean;
|
|
37
|
+
forceMount?: boolean;
|
|
38
|
+
placement?: PopperPlacement;
|
|
39
|
+
offset?: Vector2;
|
|
40
|
+
padding?: number;
|
|
41
|
+
loop?: boolean;
|
|
42
|
+
onEscapeKeyDown?: (event: LayerInteractEvent) => void;
|
|
43
|
+
onPointerDownOutside?: (event: LayerInteractEvent) => void;
|
|
44
|
+
onInteractOutside?: (event: LayerInteractEvent) => void;
|
|
45
|
+
children?: React.ReactNode;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MenuSelectEvent = {
|
|
49
|
+
defaultPrevented: boolean;
|
|
50
|
+
preventDefault: () => void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type MenuItemProps = {
|
|
54
|
+
asChild?: boolean;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
onSelect?: (event: MenuSelectEvent) => void;
|
|
57
|
+
children?: React.ReactElement;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type MenuSeparatorProps = {
|
|
61
|
+
asChild?: boolean;
|
|
62
|
+
children?: React.ReactElement;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type MenuGroupProps = {
|
|
66
|
+
asChild?: boolean;
|
|
67
|
+
children?: React.ReactElement;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type MenuLabelProps = {
|
|
71
|
+
asChild?: boolean;
|
|
72
|
+
children?: React.ReactElement;
|
|
73
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { MenuContent } from "./Menu/MenuContent";
|
|
2
|
+
export { MenuGroup } from "./Menu/MenuGroup";
|
|
3
|
+
export { MenuItem } from "./Menu/MenuItem";
|
|
4
|
+
export { MenuLabel } from "./Menu/MenuLabel";
|
|
5
|
+
export { MenuPortal } from "./Menu/MenuPortal";
|
|
6
|
+
export { Menu } from "./Menu/MenuRoot";
|
|
7
|
+
export { MenuSeparator } from "./Menu/MenuSeparator";
|
|
8
|
+
export { MenuTrigger } from "./Menu/MenuTrigger";
|
|
9
|
+
export type {
|
|
10
|
+
MenuContentProps,
|
|
11
|
+
MenuGroupProps,
|
|
12
|
+
MenuItemProps,
|
|
13
|
+
MenuLabelProps,
|
|
14
|
+
MenuPortalProps,
|
|
15
|
+
MenuProps,
|
|
16
|
+
MenuSelectEvent,
|
|
17
|
+
MenuSeparatorProps,
|
|
18
|
+
MenuTriggerProps,
|
|
19
|
+
} from "./Menu/types";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "out",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"typeRoots": [
|
|
8
|
+
"./node_modules/@rbxts",
|
|
9
|
+
"../../node_modules/@rbxts",
|
|
10
|
+
"./node_modules/@lattice-ui",
|
|
11
|
+
"../../node_modules/@lattice-ui"
|
|
12
|
+
],
|
|
13
|
+
"types": ["types", "compiler-types"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"baseUrl": "..",
|
|
6
|
+
"rootDir": "..",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@lattice-ui/checkbox": ["checkbox/src/index.ts"],
|
|
9
|
+
"@lattice-ui/core": ["core/src/index.ts"],
|
|
10
|
+
"@lattice-ui/dialog": ["dialog/src/index.ts"],
|
|
11
|
+
"@lattice-ui/focus": ["focus/src/index.ts"],
|
|
12
|
+
"@lattice-ui/layer": ["layer/src/index.ts"],
|
|
13
|
+
"@lattice-ui/menu": ["menu/src/index.ts"],
|
|
14
|
+
"@lattice-ui/popover": ["popover/src/index.ts"],
|
|
15
|
+
"@lattice-ui/popper": ["popper/src/index.ts"],
|
|
16
|
+
"@lattice-ui/radio-group": ["radio-group/src/index.ts"],
|
|
17
|
+
"@lattice-ui/style": ["style/src/index.ts"],
|
|
18
|
+
"@lattice-ui/switch": ["switch/src/index.ts"],
|
|
19
|
+
"@lattice-ui/system": ["system/src/index.ts"],
|
|
20
|
+
"@lattice-ui/tabs": ["tabs/src/index.ts"],
|
|
21
|
+
"@lattice-ui/toggle-group": ["toggle-group/src/index.ts"],
|
|
22
|
+
"@lattice-ui/tooltip": ["tooltip/src/index.ts"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|