diff --git a/esoui/ingame/group/keyboard/zo_groupmenu_keyboard.lua b/esoui/ingame/group/keyboard/zo_groupmenu_keyboard.lua
new file mode 100644
index 0000000..7b86a72
--- /dev/null
+++ b/esoui/ingame/group/keyboard/zo_groupmenu_keyboard.lua
@@ -0,0 +1,167 @@
+local GroupMenu_Keyboard = ZO_Object:Subclass()
+
+function GroupMenu_Keyboard:New(...)
+ local object = ZO_Object.New(self)
+ object:Initialize(...)
+ return object
+end
+
+function GroupMenu_Keyboard:Initialize(control)
+ self.control = control
+
+ self.headerControl = self.control:GetNamedChild("Header")
+ self.categoriesControl = self.control:GetNamedChild("Categories")
+
+ self:InitializeCategories()
+
+ KEYBOARD_GROUP_MENU_SCENE = ZO_Scene:New("groupMenuKeyboard", SCENE_MANAGER)
+ KEYBOARD_GROUP_MENU_SCENE:RegisterCallback("StateChange", function(oldState, newState)
+ if(newState == SCENE_SHOWING) then
+ KEYBIND_STRIP:AddKeybindButton(self.keybindStripDescriptor)
+ if self.currentCategoryFragment then
+ SCENE_MANAGER:AddFragment(self.currentCategoryFragment)
+ end
+ elseif(newState == SCENE_HIDDEN) then
+ KEYBIND_STRIP:RemoveKeybindButton(self.keybindStripDescriptor)
+ end
+ end)
+ self:InitializeKeybindDescriptors()
+ ZO_ACTIVITY_FINDER_ROOT_MANAGER:RegisterCallback("OnUpdateGroupStatus", function(...) self:OnUpdateGroupStatus(...) end)
+
+ local function RefreshCategories()
+ self:RefreshCategories()
+ end
+
+ ZO_ACTIVITY_FINDER_ROOT_MANAGER:RegisterCallback("OnLevelUpdate", RefreshCategories)
+ self.control:RegisterForEvent(EVENT_PLAYER_ACTIVATED, RefreshCategories)
+end
+
+function GroupMenu_Keyboard:InitializeCategories()
+ self.navigationTree = ZO_Tree:New(self.categoriesControl:GetNamedChild("ScrollChild"), 60, -10, 260)
+ self.categoryFragmentToNodeLookup = {}
+
+ local function BaseIconSetup(control, data, open)
+ local iconTexture = open and data.pressedIcon or data.normalIcon
+ local mouseoverTexture = data.mouseoverIcon
+
+ control.icon:SetTexture(iconTexture)
+ control.iconHighlight:SetTexture(mouseoverTexture)
+ end
+
+ local function TreeIconEntrySetup(node, control, data, open)
+ local selected = node.selected
+ BaseIconSetup(control, data, selected)
+
+ control.text:SetModifyTextType(MODIFY_TEXT_TYPE_UPPERCASE)
+ control.text:SetText(data.name)
+ control:SetSelected(selected)
+
+ local isLevelLocked = data.activityFinderObject and data.activityFinderObject:GetLevelLockInfo()
+ node:SetEnabled(not isLevelLocked)
+ ZO_IconHeader_Setup(control, selected, not isLevelLocked)
+ end
+
+ local function TreeEntryOnSelected(control, data, selected, reselectingDuringRebuild)
+ control:SetSelected(selected)
+
+ if selected then
+ if data.activityFinderObject then
+ ZO_ACTIVITY_FINDER_ROOT_MANAGER:ClearSelections()
+ end
+
+ if KEYBOARD_GROUP_MENU_SCENE:IsShowing() then
+ if self.currentCategoryFragment then
+ SCENE_MANAGER:RemoveFragment(self.currentCategoryFragment)
+ end
+
+ SCENE_MANAGER:AddFragment(data.categoryFragment)
+ end
+
+ self.currentCategoryFragment = data.categoryFragment
+ end
+
+ BaseIconSetup(control, data, selected)
+ ZO_IconHeader_Setup(control, selected, control.node:IsEnabled())
+ end
+
+ self.navigationTree:AddTemplate("ZO_GroupMenuKeyboard_CategoryHeader", TreeIconEntrySetup, TreeEntryOnSelected)
+
+ self.navigationTree:SetExclusive(true)
+ self.navigationTree:SetOpenAnimation("ZO_TreeOpenAnimation")
+end
+
+function GroupMenu_Keyboard:InitializeKeybindDescriptors()
+ self.keybindStripDescriptor =
+ {
+ -- Invite to Group
+ alignment = KEYBIND_STRIP_ALIGN_CENTER,
+
+ name = GetString(SI_GROUP_WINDOW_INVITE_PLAYER),
+ keybind = "UI_SHORTCUT_PRIMARY",
+
+ callback = function()
+ ZO_Dialogs_ShowDialog("GROUP_INVITE")
+ end,
+
+ visible = function()
+ local playerIsGrouped, playerIsLeader, groupSize = ZO_ACTIVITY_FINDER_ROOT_MANAGER:GetGroupStatus()
+ return not playerIsGrouped or (playerIsLeader and groupSize < GROUP_SIZE_MAX)
+ end
+ }
+end
+
+function GroupMenu_Keyboard:OnUpdateGroupStatus()
+ if KEYBOARD_GROUP_MENU_SCENE:IsShowing() then
+ KEYBIND_STRIP:UpdateKeybindButton(self.keybindStripDescriptor)
+ end
+end
+
+function GroupMenu_Keyboard:SetCurrentCategory(categoryFragment)
+ if KEYBOARD_GROUP_MENU_SCENE:IsShowing() then
+ local node = self.categoryFragmentToNodeLookup[categoryFragment]
+ self.navigationTree:SelectNode(node)
+ end
+end
+
+do
+ local lockTexture = zo_iconFormat("EsoUI/Art/Miscellaneous/locked_disabled.dds", "100%", "100%")
+ local rankIcon = zo_iconFormat(GetVeteranRankIcon(), "100%", "100%")
+
+ function GroupMenu_Keyboard:OnActivityCategoryMouseEnter(control, data)
+ ZO_IconHeader_OnMouseEnter(control)
+ if not control.enabled then
+ local isLevelLocked, lowestLevelLimit, lowestRankLimit = data.activityFinderObject:GetLevelLockInfo()
+ local lockedText
+ if isLevelLocked then
+ if lowestLevelLimit then
+ lockedText = zo_strformat(SI_ACTIVITY_FINDER_TOOLTIP_LEVEL_LOCK, lockTexture, lowestLevelLimit)
+ elseif lowestRankLimit then
+ lockedText = zo_strformat(SI_ACTIVITY_FINDER_TOOLTIP_RANK_LOCK, lockTexture, rankIcon, lowestRankLimit)
+ end
+
+ if lockedText then
+ InitializeTooltip(InformationTooltip, control, RIGHT, -10)
+ SetTooltipText(InformationTooltip, lockedText)
+ end
+ end
+ end
+ end
+end
+
+function GroupMenu_Keyboard:AddCategory(data)
+ local node = self.navigationTree:AddNode("ZO_GroupMenuKeyboard_CategoryHeader", data, nil, SOUNDS.JOURNAL_PROGRESS_CATEGORY_SELECTED)
+ self.categoryFragmentToNodeLookup[data.categoryFragment] = node
+ if data.activityFinderObject then
+ node.control.OnMouseEnter = function(control) self:OnActivityCategoryMouseEnter(control, data) end
+ end
+ self.navigationTree:Commit()
+end
+
+function GroupMenu_Keyboard:RefreshCategories()
+ self.navigationTree:RefreshVisible()
+ self.navigationTree:Commit()
+end
+
+function ZO_GroupMenuKeyboard_OnInitialized(control)
+ GROUP_MENU_KEYBOARD = GroupMenu_Keyboard:New(control)
+end
\ No newline at end of file