@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.
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuContentProps } from "./types";
3
+ export declare function MenuContent(props: MenuContentProps): React.JSX.Element | undefined;
@@ -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,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuGroupProps } from "./types";
3
+ export declare function MenuGroup(props: MenuGroupProps): React.JSX.Element;
@@ -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,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuItemProps } from "./types";
3
+ export declare function MenuItem(props: MenuItemProps): React.JSX.Element;
@@ -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,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuLabelProps } from "./types";
3
+ export declare function MenuLabel(props: MenuLabelProps): React.JSX.Element;
@@ -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,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuPortalProps } from "./types";
3
+ export declare function MenuPortal(props: MenuPortalProps): React.JSX.Element;
@@ -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,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuProps } from "./types";
3
+ export declare function Menu(props: MenuProps): React.JSX.Element;
@@ -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,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuSeparatorProps } from "./types";
3
+ export declare function MenuSeparator(props: MenuSeparatorProps): React.JSX.Element;
@@ -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,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { MenuTriggerProps } from "./types";
3
+ export declare function MenuTrigger(props: MenuTriggerProps): React.JSX.Element;
@@ -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,3 @@
1
+ import type { MenuContextValue } from "./types";
2
+ declare const MenuContextProvider: import("@rbxts/react").Provider<MenuContextValue | undefined>, useMenuContext: () => MenuContextValue;
3
+ export { MenuContextProvider, useMenuContext };
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
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,6 @@
1
+ import { createStrictContext } from "@lattice-ui/core";
2
+ import type { MenuContextValue } from "./types";
3
+
4
+ const [MenuContextProvider, useMenuContext] = createStrictContext<MenuContextValue>("Menu");
5
+
6
+ export { MenuContextProvider, useMenuContext };
@@ -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
+ }