@lattice-ui/menu 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 RovingFocusGroup = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusGroup
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(RovingFocusGroup, {
57
+ }, React.createElement(FocusScope, {
59
58
  active = props.enabled,
60
- autoFocus = "first",
61
- loop = props.loop,
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,
@@ -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 RovingFocusItem = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusItem
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(RovingFocusItem, {
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
- }, child))
98
+ ref = setItemRef,
99
+ }, child)
48
100
  end
49
- return React.createElement(RovingFocusItem, {
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,
@@ -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
- menuContext.triggerRef.current = toGuiObject(instance)
17
- end, { menuContext.triggerRef })
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),
@@ -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.2",
3
+ "version": "0.4.1",
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.3.2",
13
- "@lattice-ui/focus": "0.3.2",
14
- "@lattice-ui/popper": "0.3.2",
15
- "@lattice-ui/layer": "0.3.2"
12
+ "@lattice-ui/focus": "0.4.1",
13
+ "@lattice-ui/core": "0.4.1",
14
+ "@lattice-ui/layer": "0.4.1",
15
+ "@lattice-ui/popper": "0.4.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@rbxts/react": "17.3.7-ts.1",