This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nocheck"
},
"lint": {
"*": "disabled"
}
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["AvatarExperienceDeps"]["AvatarExperienceDeps"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent._Index
local package = PackageIndex["roblox_developer-tools"]["developer-tools"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent._Index
local package = PackageIndex["roblox_rhodium"]["rhodium"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent._Index
local package = PackageIndex["roblox_testez"]["testez"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["InGameMenuDependencies"]["InGameMenuDependencies"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["LuaChatDeps"]["LuaChatDeps"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["LuaDiscussionsDeps"]["LuaDiscussionsDeps"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["LuaSocialLibrariesDeps"]["LuaSocialLibrariesDeps"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_lumberyak-c9fb3068-76fee23f"]["lumberyak"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_otter"]["otter"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_lua-roact-policy-provider"]["lua-roact-policy-provider"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["PremiumUpsellDeps"]["PremiumUpsellDeps"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["lua-promise"]["lua-promise"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_purchase-prompt"]["purchase-prompt"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_purchase-prompt-deps"]["purchase-prompt-deps"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_lua-result"]["lua-result"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_roact"]["roact"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_roact-gamepad"]["roact-gamepad"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_roact-navigation"]["roact-navigation"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_rodux"]["rodux"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_string-utilities"]["string-utilities"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["UIBlox"]["UIBlox"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent._Index
local package = PackageIndex["roblox_url-builder"]["url-builder"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,5 @@
local AvatarExperineceDeps = script.Parent
return {
RoactFitComponents = require(AvatarExperineceDeps.RoactFitComponents),
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,6 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "AvatarExperienceDeps"
version = "0.0.1"
commit = "5a31b41867d2c5ebf0b5670d9be27f3edd403545"
source = "git+https://github.com/roblox/avatar-experience-deps#master"
dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"]
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
local InGameMenuDependencies = script.Parent
return {
Roact = require(InGameMenuDependencies.Roact),
Rodux = require(InGameMenuDependencies.Rodux),
RoactRodux = require(InGameMenuDependencies.RoactRodux),
UIBlox = require(InGameMenuDependencies.UIBlox),
Otter = require(InGameMenuDependencies.Otter),
Cryo = require(InGameMenuDependencies.Cryo),
t = require(InGameMenuDependencies.t),
PolicyProvider = require(InGameMenuDependencies.PolicyProvider),
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_otter"]["otter"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_lua-roact-policy-provider"]["lua-roact-policy-provider"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact"]["roact"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_rodux"]["rodux"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["UIBlox"]["UIBlox"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,15 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "InGameMenuDependencies"
version = "0.1.0"
commit = "aecc56c1fa6348886a1073785dbb196b655a0a90"
source = "git+https://github.com/roblox/in-game-menu-dependencies#master"
dependencies = [
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
"Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter",
"PolicyProvider roblox/lua-roact-policy-provider 5f782af8 git+https://github.com/roblox/lua-roact-policy-provider#master",
"Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact",
"RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux",
"Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux",
"UIBlox <patched> UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master",
"t roblox/t 1.2.5 url+https://github.com/roblox/t",
]
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_t"]["t"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_asset-card"]["asset-card"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_infinite-scroller-98304e77-0.7.3"]["infinite-scroller"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_infinite-scroller-3aafd7fe-7286a922"]["infinite-scroller"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,9 @@
local LuaChatDeps = script.Parent
return {
InfiniteScroll = require(LuaChatDeps.InfiniteScroller),
InfiniteScroll71 = require(LuaChatDeps.InfiniteScroller71),
RoduxNetworking = require(LuaChatDeps.RoduxNetworking),
UIBlox = require(LuaChatDeps.UIBlox),
AssetCard = require(LuaChatDeps.AssetCard),
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["rodux-networking"]["rodux-networking"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["UIBlox"]["UIBlox"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "LuaChatDeps"
version = "0.1.3"
commit = "f16c004618db2eacb01bf85c73946d0711bfe922"
source = "git+https://github.com/roblox/lua-chat-deps#master"
dependencies = [
"AssetCard roblox/asset-card 103b2a57 git+https://github.com/roblox/asset-card#v1.0.3",
"InfiniteScroller roblox/infinite-scroller 0.7.3 url+https://github.com/roblox/infinite-scroller",
"InfiniteScroller71 roblox/infinite-scroller 7286a922 git+https://github.com/roblox/infinite-scroller#v0.7.1",
"RoduxNetworking rodux-networking 8902a6aa git+https://github.com/roblox/rodux-networking#v1.0.2",
"UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master",
]
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_infinite-scroller-98304e77-0.3.4"]["infinite-scroller"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,8 @@
local LuaDiscussionsDeps = script.Parent
return {
InfiniteScroll = require(LuaDiscussionsDeps.InfiniteScroll),
RoactFitComponents = require(LuaDiscussionsDeps.RoactFitComponents),
RoactNavigation = require(LuaDiscussionsDeps.RoactNavigation),
RoduxNetworking = require(LuaDiscussionsDeps.RoduxNetworking),
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roact-navigation"]["roact-navigation"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["rodux-networking"]["rodux-networking"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,11 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "LuaDiscussionsDeps"
version = "0.1.2"
commit = "149f4695b874c2e43760131eda24779669bb3bec"
source = "git+https://github.com/roblox/lua-discussions-deps#master"
dependencies = [
"InfiniteScroll roblox/infinite-scroller 0.3.4 url+https://github.com/roblox/infinite-scroller",
"RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components",
"RoactNavigation roact-navigation 0.1.1-linter-fix url+https://github.com/roblox/roact-navigation",
"RoduxNetworking rodux-networking 8902a6aa git+https://github.com/roblox/rodux-networking#v1.0.2",
]
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_genericpagination"]["genericpagination"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,7 @@
local LuaSocialLibrariesDeps = script.Parent
return {
GenericPagination = require(LuaSocialLibrariesDeps.GenericPagination),
RoactFitComponents = require(LuaSocialLibrariesDeps.RoactFitComponents),
Mock = require(LuaSocialLibrariesDeps.Mock),
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["jtaylor_mock"]["mock"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,10 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "LuaSocialLibrariesDeps"
version = "0.1.1"
commit = "b04c58051c6067aaceb41d062c8eb1ddffeaba0a"
source = "git+https://github.com/roblox/lua-social-libraries-deps#master"
dependencies = [
"GenericPagination roblox/genericpagination 885168d0 git+https://github.com/roblox/genericpagination#master",
"Mock jtaylor/mock d2c4005c git+https://github.com/roblox/mock#master",
"RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components",
]
@@ -0,0 +1,5 @@
local PremiumUpsellDeps = script.Parent
return {
RoactFitComponents = require(PremiumUpsellDeps.RoactFitComponents),
}
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,6 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "PremiumUpsellDeps"
version = "0.0.0"
commit = "c51030b70236f2e9d69bb34348b7e045c2c437f6"
source = "git+https://github.com/roblox/premium-upsell-deps#master"
dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"]
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_infinite-scroller-98304e77-0.5.6"]["infinite-scroller"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_otter"]["otter"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact"]["roact"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-gamepad"]["roact-gamepad"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,314 @@
local AccordionRoot = script.Parent
local AppRoot = AccordionRoot.Parent
local UIBloxRoot = AppRoot.Parent
local Packages = UIBloxRoot.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local SpringAnimatedItem = require(UIBloxRoot.Utility.SpringAnimatedItem)
local ITEM_PADDING = 10
local ITEM_WIDTH_SHRINK_STEP = 20 -- How much each item shrinks below card above it
local COMPACT_VIEW_PLACEHOLDER_HEIGHT = 10
local PRESSED_SCALE = 0.9
local ANIMATION_SPRING_SETTINGS = {
dampingRatio = 1,
frequency = 3.5,
}
local AccordionView = Roact.PureComponent:extend("AccordionView")
AccordionView.defaultProps = {
maxItemsInCompactView = 3,
}
local validateProps = t.strictInterface({
items = t.table,
itemWidth = t.number,
itemHeight = t.number,
renderItem = t.callback,
placeholderColor = t.Color3,
placeholderBaseTransparency = t.number,
collapseButtonSize = t.number,
renderCollapseButton = t.callback,
LayoutOrder = t.optional(t.integer),
maxItemsInCompactView = t.numberPositive,
})
function AccordionView:init()
self.state = {
expanded = false,
isExpandButtonPressed = false,
}
self.onExpandButtonActivated = function()
self:setState({
expanded = true,
isExpandButtonPressed = false,
})
end
self.onCollapseButtonActivated = function()
self:setState({
expanded = false,
})
end
self.onExpandButtonInputBegan = function(_, inputObject)
if inputObject.UserInputState == Enum.UserInputState.Begin and
(inputObject.UserInputType == Enum.UserInputType.Touch or
inputObject.UserInputType == Enum.UserInputType.MouseButton1) then
self:setState({
isExpandButtonPressed = true,
})
end
end
self.onExpandButtonInputEnded = function()
if self.state.isExpandButtonPressed then
self:setState({
isExpandButtonPressed = false,
})
end
end
self.rootFrameRef = Roact.createRef()
self.onListLayoutAbsoluteContentSizeChanged = function(rbx)
if self.rootFrameRef.current then
local itemWidth = self.props.itemWidth
local minimumHeight = self:getCompactTotalHeight()
self.rootFrameRef.current.Size = UDim2.new(0, itemWidth,
0, math.max(rbx.AbsoluteContentSize.Y, minimumHeight))
end
end
end
function AccordionView:getCompactTotalHeight()
local items = self.props.items
local itemHeight = self.props.itemHeight
local maxItemsInCompactView = self.props.maxItemsInCompactView
local totalNumberOfItems = #items
if totalNumberOfItems == 0 then
return 0
else
return itemHeight + (math.min(maxItemsInCompactView, totalNumberOfItems) - 1) * COMPACT_VIEW_PLACEHOLDER_HEIGHT
end
end
function AccordionView:getLayoutInfo()
local items = self.props.items
local itemWidth = self.props.itemWidth
local itemHeight = self.props.itemHeight
local placeholderBaseTransparency = self.props.placeholderBaseTransparency
local maxItemsInCompactView = self.props.maxItemsInCompactView
local expanded = self.state.expanded
local layoutData = {}
local totalNumberOfItems = #items
local itemsShownInCompactView = math.min(maxItemsInCompactView, totalNumberOfItems)
local placeholderTransparencyStep = 0
if itemsShownInCompactView > 1 then
placeholderTransparencyStep = (1 - placeholderBaseTransparency) / (itemsShownInCompactView - 1)
end
for index = 1, totalNumberOfItems do
if expanded then
layoutData[index] = {
width = itemWidth,
height = itemHeight,
placeholderTransparency = 1,
itemTransparency = 0,
}
else
if index == 1 then
layoutData[index] = {
width = itemWidth,
height = itemHeight,
placeholderTransparency = 1,
itemTransparency = 0,
}
elseif index <= maxItemsInCompactView then
layoutData[index] = {
width = itemWidth - ITEM_WIDTH_SHRINK_STEP * (index - 1),
height = COMPACT_VIEW_PLACEHOLDER_HEIGHT,
placeholderTransparency = placeholderBaseTransparency + placeholderTransparencyStep * (index - 2),
itemTransparency = 1,
}
else
layoutData[index] = {
width = itemWidth - ITEM_WIDTH_SHRINK_STEP * (index - 1),
height = 0,
placeholderTransparency = 1,
itemTransparency = 1,
}
end
end
end
return layoutData
end
function AccordionView:render()
assert(validateProps(self.props))
local items = self.props.items
local totalNumberOfItems = #items
if totalNumberOfItems == 0 then
return nil
end
local layoutOrder = self.props.LayoutOrder
local itemWidth = self.props.itemWidth
local renderItem = self.props.renderItem
local placeholderColor = self.props.placeholderColor
local collapseButtonSize = self.props.collapseButtonSize
local renderCollapseButton = self.props.renderCollapseButton
local expanded = self.state.expanded
local isExpandButtonPressed = self.state.isExpandButtonPressed
local layoutData = self:getLayoutInfo()
local accordionContent = {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, expanded and ITEM_PADDING or 0),
[Roact.Change.AbsoluteContentSize] = self.onListLayoutAbsoluteContentSizeChanged,
}),
Scaler = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, {
springOptions = ANIMATION_SPRING_SETTINGS,
animatedValues = {
scale = isExpandButtonPressed and PRESSED_SCALE or 1,
},
mapValuesToProps = function(values)
return {
Scale = values.scale,
}
end,
}),
CollapseButton = Roact.createElement(SpringAnimatedItem.AnimatedFrame, {
springOptions = ANIMATION_SPRING_SETTINGS,
animatedValues = {
-- Increase the size by 1 pixel so the animation looks better
-- when the spring is damping in the end.
sizeOffsetY = expanded and collapseButtonSize + 1 or 0,
},
mapValuesToProps = function(values)
return {
Size = UDim2.new(0, collapseButtonSize, 0, values.sizeOffsetY),
}
end,
regularProps = {
BackgroundTransparency = 1,
ClipsDescendants = true,
[Roact.Children] = {
ButtonMoveContainer = Roact.createElement(SpringAnimatedItem.AnimatedFrame, {
springOptions = ANIMATION_SPRING_SETTINGS,
animatedValues = {
positionOffsetY = expanded and 0 or collapseButtonSize / 2,
},
mapValuesToProps = function(values)
return {
Position = UDim2.new(0, 0, 0, values.positionOffsetY),
}
end,
regularProps = {
Size = UDim2.new(0, collapseButtonSize, 0, collapseButtonSize),
BackgroundTransparency = 1,
[Roact.Children] = {
Button = renderCollapseButton(self.onCollapseButtonActivated),
},
},
}),
},
},
}),
}
for index, _ in ipairs(items) do
local layout = layoutData[index]
accordionContent["Item" .. tostring(index)] = Roact.createElement(SpringAnimatedItem.AnimatedFrame, {
springOptions = ANIMATION_SPRING_SETTINGS,
animatedValues = {
width = layout.width,
height = layout.height,
},
mapValuesToProps = function(values)
return {
Size = UDim2.new(0, values.width, 0, values.height),
}
end,
regularProps = {
Size = UDim2.new(1, 0, 0, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
LayoutOrder = index + 1,
ZIndex = totalNumberOfItems + 1 - index;
ClipsDescendants = true,
[Roact.Children] = {
Item = renderItem(items[index], layout.itemTransparency, ANIMATION_SPRING_SETTINGS),
Placeholder = Roact.createElement(SpringAnimatedItem.AnimatedFrame, {
springOptions = ANIMATION_SPRING_SETTINGS,
animatedValues = {
transparency = layout.placeholderTransparency,
},
mapValuesToProps = function(values)
return {
BackgroundTransparency = values.transparency,
}
end,
regularProps = {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = placeholderColor,
BorderSizePixel = 0,
},
}),
},
},
})
end
local canExpand = (totalNumberOfItems > 1)
local clickToExpand = canExpand and not expanded
return Roact.createElement("Frame", {
Size = UDim2.new(0, itemWidth, 0, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
[Roact.Ref] = self.rootFrameRef,
}, {
ContentFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
},
accordionContent
),
ClickToExpandButton = clickToExpand and Roact.createElement("TextButton", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = totalNumberOfItems + 1,
Text = "",
[Roact.Event.Activated] = self.onExpandButtonActivated,
[Roact.Event.InputBegan] = self.onExpandButtonInputBegan,
[Roact.Event.InputEnded] = self.onExpandButtonInputEnded,
}),
})
end
return AccordionView
@@ -0,0 +1,72 @@
return function()
local AccordionRoot = script.Parent
local AppRoot = AccordionRoot.Parent
local UIBloxRoot = AppRoot.Parent
local Packages = UIBloxRoot.Parent
local AccordionView = require(AccordionRoot.AccordionView)
local Roact = require(Packages.Roact)
describe("AccordionView", function()
it("should mount correctly", function()
local element = Roact.createElement(AccordionView, {
items = {"test", "test2"},
itemWidth = 355,
itemHeight = 188,
renderItem = function(item, transparency)
return Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = item,
BackgroundTransparency = transparency,
})
end,
placeholderColor = Color3.fromRGB(255, 255, 255),
placeholderBaseTransparency = 0.5,
collapseButtonSize = 40,
renderCollapseButton = function(activatedCallback)
return Roact.createElement("TextButton", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Text = "close",
AutoButtonColor = false,
[Roact.Event.Activated] = activatedCallback,
})
end,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should mount correctly with empty items", function()
local element = Roact.createElement(AccordionView, {
items = {},
itemWidth = 355,
itemHeight = 188,
renderItem = function(item, transparency)
return Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = item,
BackgroundTransparency = transparency,
})
end,
placeholderColor = Color3.fromRGB(255, 255, 255),
placeholderBaseTransparency = 0.5,
collapseButtonSize = 40,
renderCollapseButton = function(activatedCallback)
return Roact.createElement("TextButton", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Text = "close",
AutoButtonColor = false,
[Roact.Event.Activated] = activatedCallback,
})
end,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,221 @@
local Bar = script.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Otter = require(Packages.Otter)
local t = require(Packages.t)
local IconButton = require(App.Button.IconButton)
local Images = require(App.ImageSet.Images)
local IconSize = require(App.ImageSet.Enum.IconSize)
local getIconSize = require(App.ImageSet.getIconSize)
local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState)
local withStyle = require(UIBlox.Core.Style.withStyle)
local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar)
local lerp = require(Packages.UIBlox.Utility.lerp)
local divideTransparency = require(Packages.UIBlox.Utility.divideTransparency)
local TITLE_BAR_HEIGHT = 64
local SHADOW_HEIGHT = 24
local MARGIN = 20
local PADDING_BETWEEN = 12
local TITLE_BAR_OFF_POS = UDim2.new(0, 0, 0, -(TITLE_BAR_HEIGHT + SHADOW_HEIGHT))
local TITLE_BAR_ON_POS = UDim2.fromOffset(0, 0)
local EXIT_BUTTON_IMAGE_ID = "icons/actions/previewShrink"
local CLOSE_BUTTON_IMAGE_ID = "icons/navigation/close"
local GRADIENT_OPACITY = 0.25
local MOTOR_OPTIONS = {
frequency = 5,
}
local FullscreenTitleBar = Roact.PureComponent:extend("FullscreenTitleBar")
FullscreenTitleBar.validateProps = t.strictInterface({
title = t.string,
isTriggered = t.optional(t.boolean),
onDisappear = t.optional(t.callback),
exitFullscreen = t.optional(t.callback),
closeRoblox = t.optional(t.callback),
})
function FullscreenTitleBar:init()
local initProgress = self.props.isTriggered and 1 or 0
local setProgress
self.progress, setProgress = Roact.createBinding(initProgress)
self.exitControlState, self.setExitControlState = Roact.createBinding(self.state.exitControlState)
self.closeControlState, self.setCloseControlState = Roact.createBinding(self.state.closeControlState)
self.titleBarPosition = self.progress:map(function(value)
return TITLE_BAR_OFF_POS:lerp(TITLE_BAR_ON_POS, value)
end)
self.progressMotor = Otter.createSingleMotor(initProgress)
self.progressMotor:onStep(setProgress)
end
function FullscreenTitleBar:render()
return withStyle(function(style)
local theme = style.Theme
local font = style.Font
local backgroundStyle = theme.BackgroundUIDefault
local textColorStyle = theme.TextEmphasis
local centerTextFont = font.Header2
local centerTextSize = centerTextFont.RelativeSize * font.BaseSize
local titleBarTransparency = self.progress:map(function(value)
local baseTransparency = backgroundStyle.Transparency
return lerp(1, baseTransparency, value)
end)
local exitButtonTransparency = Roact.joinBindings({
progress = self.progress,
controlState = self.exitControlState,
}):map(function(values)
local baseTransparency = theme.ContextualPrimaryDefault.Transparency
local transparencyDivisor = values.controlState == ControlState.Pressed and 2 or 1
return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress)
end)
local closeButtonTransparency = Roact.joinBindings({
progress = self.progress,
controlState = self.closeControlState,
}):map(function(values)
local baseTransparency = theme.ContextualPrimaryDefault.Transparency
local transparencyDivisor = values.controlState == ControlState.Pressed and 2 or 1
return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress)
end)
local textTransparency = self.progress:map(function(value)
local baseTransparency = textColorStyle.Transparency
return lerp(1, baseTransparency, value)
end)
local function renderCenterText()
return Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Font = centerTextFont.Font,
Size = UDim2.new(1, 0, 0, centerTextSize),
Text = self.props.title,
TextColor3 = textColorStyle.Color,
TextSize = centerTextSize,
TextTransparency = textTransparency,
TextTruncate = Enum.TextTruncate.AtEnd,
TextWrapped = false,
})
end
local function renderRightButtons()
return Roact.createFragment({
ExitButton = Roact.createElement(IconButton, {
icon = Images[EXIT_BUTTON_IMAGE_ID],
iconSize = IconSize.Medium,
iconTransparency = exitButtonTransparency,
onActivated = self.props.exitFullscreen,
layoutOrder = 1,
onStateChanged = function(oldState, newState)
self.setExitControlState(newState)
end
}),
CloseButton = Roact.createElement(IconButton, {
icon = Images[CLOSE_BUTTON_IMAGE_ID],
iconSize = IconSize.Medium,
iconTransparency = closeButtonTransparency,
onActivated = self.props.closeRoblox,
layoutOrder = 2,
onStateChanged = function(oldState, newState)
self.setCloseControlState(newState)
end
}),
})
end
local function renderMirrorButtons()
local iconSize = getIconSize(IconSize.Medium)
return Roact.createFragment({
Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(iconSize, iconSize),
}),
Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(iconSize, iconSize),
}),
})
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Position = self.titleBarPosition,
Size = UDim2.new(1, 0, 0, TITLE_BAR_HEIGHT + SHADOW_HEIGHT),
}, {
BarFrame = Roact.createElement("Frame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Position = UDim2.fromOffset(0, 0),
Size = UDim2.new(1, 0, 0, TITLE_BAR_HEIGHT),
[Roact.Event.MouseLeave] = self.props.onDisappear,
}, {
ThreeSectionBar = Roact.createElement(ThreeSectionBar, {
BackgroundColor3 = backgroundStyle.Color,
BackgroundTransparency = titleBarTransparency,
barHeight = TITLE_BAR_HEIGHT,
marginLeft = MARGIN,
contentPaddingLeft = UDim.new(0, PADDING_BETWEEN),
renderLeft = renderMirrorButtons,
renderCenter = renderCenterText,
marginRight = MARGIN,
contentPaddingRight = UDim.new(0, PADDING_BETWEEN),
renderRight = renderRightButtons,
}),
}),
ShadowFrame = Roact.createElement("Frame", {
BackgroundTransparency = backgroundStyle.Transparency,
BackgroundColor3 = backgroundStyle.Color,
BorderSizePixel = 0,
Position = UDim2.fromOffset(0, TITLE_BAR_HEIGHT),
Size = UDim2.new(1, 0, 0, SHADOW_HEIGHT),
}, {
UIGradient = Roact.createElement("UIGradient", {
Rotation = 90,
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(0, 0, 0)),
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 0)),
}),
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1 - GRADIENT_OPACITY),
NumberSequenceKeypoint.new(1, 1.0),
}),
})
})
})
end)
end
function FullscreenTitleBar:didMount()
self.progressMotor:start()
end
function FullscreenTitleBar:didUpdate(prevProps, prevState)
if prevProps.isTriggered ~= self.props.isTriggered then
local newProgress = self.props.isTriggered and 1 or 0
self.progressMotor:setGoal(Otter.spring(newProgress, MOTOR_OPTIONS))
end
end
function FullscreenTitleBar:willUnmount()
self.progressMotor:destroy()
end
return FullscreenTitleBar
@@ -0,0 +1,33 @@
return function()
local Packages = script.Parent.Parent.Parent.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent)
local FullscreenTitleBar = require(script.Parent.FullscreenTitleBar)
describe("lifecycle", function()
it("should mount and unmount without issue", function()
local element = mockStyleComponent({
TestTitleBar = Roact.createElement(FullscreenTitleBar, {
title = "",
})
})
local instance = Roact.mount(element, nil, "FullscreenTitleBar")
Roact.unmount(instance)
end)
it("should mount and unmount without issue with valid properties", function()
local element = mockStyleComponent({
TestTitleBar = Roact.createElement(FullscreenTitleBar, {
title = "",
onDisappear = function() end,
isTriggered = false,
exitFullscreen = function() end,
closeRoblox = function() end,
})
})
local instance = Roact.mount(element, nil, "FullscreenTitleBar")
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,160 @@
local Bar = script.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Images = require(App.ImageSet.Images)
local IconSize = require(App.ImageSet.Enum.IconSize)
local getPageMargin = require(App.Container.getPageMargin)
local withStyle = require(UIBlox.Core.Style.withStyle)
local IconButton = require(UIBlox.App.Button.IconButton)
local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel)
local GetTextSize = require(UIBlox.Core.Text.GetTextSize)
local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar)
local HeaderBar = Roact.PureComponent:extend("HeaderBar")
HeaderBar.renderLeft = {
backButton = function(onActivated)
return function(_)
return Roact.createElement(IconButton, {
size = UDim2.fromOffset(0, 0),
iconSize = IconSize.Medium,
icon = Images["icons/navigation/pushBack"],
onActivated = onActivated,
})
end
end,
}
HeaderBar.validateProps = t.strictInterface({
-- The title of the screen
title = t.string,
-- Use the left side root title style
isRootTitle = t.boolean,
-- How tall the bar is
barHeight = t.optional(t.number),
-- How much spacing between elements to allow on the right side of the bar
contentPaddingRight = t.optional(t.UDim),
-- A function that returns a Roact Component, used for customizing buttons on the right side of the bar
renderRight = t.optional(t.callback),
-- A function that returns a Roact Component, used for customizing, e.g. back button, on the left side of the bar
renderLeft = t.optional(t.callback),
-- A function that returns a Roact Component, used for containing, e.g. search bar, on the center of the bar
renderCenter = t.optional(t.callback),
-- Background transparency
backgroundTransparency = t.optional(t.number),
-- Left side margin
marginLeft = t.optional(t.number),
-- Right side margin
marginRight = t.optional(t.number),
})
-- default values are taken from Abstract
HeaderBar.defaultProps = {
title = "",
isRootTitle = false,
barHeight = 48,
contentPaddingRight = UDim.new(0, 0),
}
function HeaderBar:init()
self.state = {
margin = 0
}
self.onResize = function(rbx)
local margin = getPageMargin(rbx.AbsoluteSize.X)
self:setState({
margin = margin
})
end
end
function HeaderBar:render()
return withStyle(function(style)
local theme = style.Theme
local font = style.Font
local isRootTitle = self.props.isRootTitle
local renderLeft = self.props.renderLeft
local renderCenter = self.props.renderCenter
local renderRight = self.props.renderRight
local estimatedCenterWidth = math.huge
if not renderLeft and isRootTitle then
renderLeft = function()
return Roact.createElement(GenericTextLabel, {
fluidSizing = true,
Text = self.props.title,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
fontStyle = font.Title,
colorStyle = theme.TextEmphasis,
})
end
end
-- Make center fixed-width components in the center, e.g search bar
if renderLeft and renderCenter and renderRight then
estimatedCenterWidth = 0
end
if not renderCenter and not isRootTitle then
local centerTextFontStyle = font.Header1
local centerTextSize = centerTextFontStyle.RelativeSize * font.BaseSize
renderCenter = function()
return Roact.createElement(GenericTextLabel, {
ClipsDescendants = true,
Size = UDim2.new(1, 0, 0, centerTextSize),
Text = self.props.title,
TextTruncate = Enum.TextTruncate.AtEnd,
TextWrapped = false,
fontStyle = centerTextFontStyle,
colorStyle = theme.TextEmphasis,
})
end
estimatedCenterWidth = GetTextSize(
self.props.title,
centerTextSize,
centerTextFontStyle.Font,
Vector2.new(1000, 1000)
).X
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, self.props.barHeight),
[Roact.Change.AbsoluteSize] =
(self.props.marginLeft == nil and self.props.marginRight == nil) and self.onResize or nil,
}, {
ThreeSectionBar = Roact.createElement(ThreeSectionBar, {
BackgroundTransparency = self.props.backgroundTransparency or theme.BackgroundDefault.Transparency,
BackgroundColor3 = theme.BackgroundDefault.Color,
barHeight = self.props.barHeight,
marginLeft = self.props.marginLeft or self.state.margin,
renderLeft = renderLeft,
renderCenter = renderCenter,
estimatedCenterWidth = estimatedCenterWidth,
marginRight = self.props.marginRight or self.state.margin,
contentPaddingRight = self.props.contentPaddingRight,
renderRight = renderRight,
})
})
end)
end
return HeaderBar
@@ -0,0 +1,134 @@
return function()
local Bar = script.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local HeaderBar = require(UIBlox.App.Bar.HeaderBar)
local BARSIZE_SMALL = UDim2.new(0, 320, 0, 40)
local BARSIZE_MEDIUM = UDim2.new(0, 480, 0, 40)
local BARSIZE_LARGE = UDim2.new(0, 600, 0, 40)
local MARGIN_SMALL = 12
local MARGIN_MEDIUM = 24
local MARGIN_LARGE = 48
describe("lifecycle", function()
it("should mount and unmount without issues", function()
local element = mockStyleComponent({
bar = Roact.createElement(HeaderBar, {
title = "Header Bar",
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
describe("renderCenter", function()
it("should mount things correctly", function()
local frame = Instance.new("Frame")
local element = mockStyleComponent({
barFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, 0, 0, 0),
}, {
bar = Roact.createElement(HeaderBar, {
title = "Header Bar",
renderCenter = function()
return Roact.createElement("TextBox", {
Size = UDim2.fromOffset(200, 36),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
Text = "Search Box Text",
})
end,
}),
})
})
local instance = Roact.mount(element, frame, "Frame")
local barFrame = frame:FindFirstChild("barFrame", true)
local bar = barFrame:FindFirstChild("bar")
local centerFrame = bar:FindFirstChild("centerFrame", true)
local centerContent = centerFrame:FindFirstChild("centerContent")
expect(centerContent.Text).to.equal("Search Box Text")
Roact.unmount(instance)
end)
end)
describe("margin logic", function()
it("should have correct left margin on different sized screens with renderLeft", function()
local frame = Instance.new("Frame")
local element = mockStyleComponent({
barFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, 0, 0, 0),
}, {
bar = Roact.createElement(HeaderBar, {
title = "Header Bar",
isRootTitle = true,
}),
})
})
local instance = Roact.mount(element, frame, "Frame")
local barFrame = frame:FindFirstChild("barFrame", true)
local bar = barFrame:FindFirstChild("bar")
local leftFrame = bar:FindFirstChild("leftFrame", true)
local margin = leftFrame:FindFirstChild("$margin", true)
expect(margin).to.be.ok()
barFrame.Size = BARSIZE_SMALL
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(margin.PaddingLeft.Offset).to.equal(MARGIN_SMALL)
barFrame.Size = BARSIZE_MEDIUM
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(margin.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM)
barFrame.Size = BARSIZE_LARGE
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(margin.PaddingLeft.Offset).to.equal(MARGIN_LARGE)
Roact.unmount(instance)
end)
it("should have correct left margin on different sized screens without renderLeft", function()
local frame = Instance.new("Frame")
local element = mockStyleComponent({
barFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, 0, 0, 0),
}, {
bar = Roact.createElement(HeaderBar, {
title = "Header Bar",
isRootTitle = false,
}),
})
})
local instance = Roact.mount(element, frame, "Frame")
local barFrame = frame:FindFirstChild("barFrame", true)
local bar = barFrame:FindFirstChild("bar")
local centerFrame = bar:FindFirstChild("centerFrame", true)
local UIPadding = centerFrame:FindFirstChild("UIPadding", true)
expect(UIPadding).to.be.ok()
barFrame.Size = BARSIZE_SMALL
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(UIPadding.PaddingLeft.Offset).to.equal(MARGIN_SMALL)
barFrame.Size = BARSIZE_MEDIUM
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(UIPadding.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM)
barFrame.Size = BARSIZE_LARGE
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(UIPadding.PaddingLeft.Offset).to.equal(MARGIN_LARGE)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,91 @@
local Bar = script.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local withStyle = require(UIBlox.Core.Style.withStyle)
local getPageMargin = require(App.Container.getPageMargin)
local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel)
local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar)
local RootHeaderBar = Roact.PureComponent:extend("HeaderBar")
RootHeaderBar.validateProps = t.strictInterface({
-- The title of the screen
title = t.string,
-- How tall the bar is
barHeight = t.optional(t.number),
-- A function that returns a Roact Component, used for containing, e.g. search bar, on the center of the bar
renderCenter = t.optional(t.callback),
-- A function that returns a Roact Component, used for customizing buttons on the right side of the bar
renderRight = t.optional(t.callback),
backgroundTransparency = t.optional(t.number),
})
RootHeaderBar.defaultProps = {
barHeight = 64,
renderRight = function()
return nil
end,
}
function RootHeaderBar:init()
self:setState({
margin = 0,
})
self.setPageMargin = function(rbx)
local margin = getPageMargin(rbx.AbsoluteSize.X)
self:setState({
margin = margin
})
end
end
function RootHeaderBar:render()
return withStyle(function(style)
local theme = style.Theme
local font = style.Font
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, self.props.barHeight),
[Roact.Change.AbsoluteSize] = self.setPageMargin,
}, {
ThreeSectionBar = Roact.createElement(ThreeSectionBar, {
BackgroundTransparency = self.props.backgroundTransparency or theme.BackgroundDefault.Transparency,
BackgroundColor3 = theme.BackgroundDefault.Color,
barHeight = self.props.barHeight,
contentPaddingRight = UDim.new(0, 0),
marginLeft = self.state.margin,
marginRight = self.state.margin,
renderCenter = self.props.renderCenter,
renderRight = self.props.renderRight,
renderLeft = function(props)
return Roact.createFragment({
Text = Roact.createElement(GenericTextLabel, {
fluidSizing = true,
Text = self.props.title,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
fontStyle = font.Title,
colorStyle = theme.TextEmphasis,
}, props[Roact.Children])
})
end,
})
})
end)
end
return RootHeaderBar
@@ -0,0 +1,142 @@
return function()
local Bar = script.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local RootHeaderBar = require(UIBlox.App.Bar.RootHeaderBar)
local BARSIZE_SMALL = UDim2.new(0, 320, 0, 40)
local BARSIZE_MEDIUM = UDim2.new(0, 480, 0, 40)
local BARSIZE_LARGE = UDim2.new(0, 600, 0, 40)
local MARGIN_SMALL = 12
local MARGIN_MEDIUM = 24
local MARGIN_LARGE = 48
describe("lifecycle", function()
it("should mount and unmount without issues", function()
local element = mockStyleComponent({
bar = Roact.createElement(RootHeaderBar, {
title = "Root Header Bar",
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
describe("renderCenter", function()
it("should mount things correctly", function()
local frame = Instance.new("Frame")
local element = mockStyleComponent({
barFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, 0, 0, 0),
}, {
bar = Roact.createElement(RootHeaderBar, {
title = "Root Header Bar",
renderCenter = function()
return Roact.createElement("TextBox", {
Size = UDim2.fromOffset(200, 36),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
Text = "Search Box Text",
})
end,
}),
})
})
local instance = Roact.mount(element, frame, "Frame")
local barFrame = frame:FindFirstChild("barFrame", true)
local bar = barFrame:FindFirstChild("bar")
local centerFrame = bar:FindFirstChild("centerFrame", true)
local centerContent = centerFrame:FindFirstChild("centerContent")
expect(centerContent.Text).to.equal("Search Box Text")
Roact.unmount(instance)
end)
end)
describe("margin logic", function()
it("should have margin of 12 on small screens", function()
local frame = Instance.new("Frame")
local element = mockStyleComponent({
barFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, 0, 0, 0),
}, {
bar = Roact.createElement(RootHeaderBar, {
title = "Root Header Bar",
}),
})
})
local instance = Roact.mount(element, frame, "Frame")
local barFrame = frame:FindFirstChild("barFrame", true)
local bar = barFrame:FindFirstChild("bar")
local leftFrame = bar:FindFirstChild("leftFrame", true)
local margin = leftFrame:FindFirstChild("$margin", true)
expect(margin).to.be.ok()
barFrame.Size = BARSIZE_SMALL
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(margin.PaddingLeft.Offset).to.equal(MARGIN_SMALL)
Roact.unmount(instance)
end)
it("should have margin of 24 on medium screens", function()
local frame = Instance.new("Frame")
local element = mockStyleComponent({
barFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, 0, 0, 0),
}, {
bar = Roact.createElement(RootHeaderBar, {
title = "Root Header Bar",
}),
})
})
local instance = Roact.mount(element, frame, "Frame")
local barFrame = frame:FindFirstChild("barFrame", true)
local bar = barFrame:FindFirstChild("bar")
local leftFrame = bar:FindFirstChild("leftFrame", true)
local margin = leftFrame:FindFirstChild("$margin", true)
expect(margin).to.be.ok()
barFrame.Size = BARSIZE_MEDIUM
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(margin.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM)
Roact.unmount(instance)
end)
it("should have margin of 48 on large screens", function()
local frame = Instance.new("Frame")
local element = mockStyleComponent({
barFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, 0, 0, 0),
}, {
bar = Roact.createElement(RootHeaderBar, {
title = "Root Header Bar",
}),
})
})
local instance = Roact.mount(element, frame, "Frame")
local barFrame = frame:FindFirstChild("barFrame", true)
local bar = barFrame:FindFirstChild("bar")
local leftFrame = bar:FindFirstChild("leftFrame", true)
local margin = leftFrame:FindFirstChild("$margin", true)
expect(margin).to.be.ok()
barFrame.Size = BARSIZE_LARGE
local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize]
expect(margin.PaddingLeft.Offset).to.equal(MARGIN_LARGE)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,110 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView)
local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem)
local Bar = script.Parent.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local FullscreenTitleBar = require(script.Parent.Parent.FullscreenTitleBar)
local DISAPPEAR_DELAY = 0.5
local TitleBarStory = Roact.PureComponent:extend("TitleBarStory")
function TitleBarStory:init()
self:setState({
isTriggered = false,
})
self.triggerTitleBar = function()
print("Mouse entering trigger area")
if not self.state.isTriggered then
self:setState({
isTriggered = true,
})
end
end
self.hideTitleBar = function()
print("Mouse leaving Title Bar area")
if self.state.isTriggered then
delay(DISAPPEAR_DELAY, function()
self:setState({
isTriggered = false,
})
end)
end
end
self.buttonControl = function()
self:setState(function(prevState)
return {
isTriggered = not prevState.isTriggered,
}
end)
end
end
function TitleBarStory:render()
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Vertical,
}),
ControlsFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 50),
LayoutOrder = 1,
}, {
TestButton = Roact.createElement("TextButton", {
Text = self.state.isTriggered and "Dismiss" or "Activate",
Size = UDim2.fromOffset(200, 50),
[Roact.Event.Activated] = self.buttonControl,
}),
}),
StoryItem = Roact.createElement(StoryItem, {
size = UDim2.fromScale(1, 1),
title = "FullscreenTitleBar",
subTitle = "App.Bar.FullscreenTitleBar",
layoutOrder = 2,
showDivider = true,
}, {
Demo = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
}, {
TriggerArea = Roact.createElement("Frame", {
BackgroundColor3 = Color3.fromRGB(0, 255, 255),
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 1),
[Roact.Event.MouseEnter] = self.triggerTitleBar,
}),
TitleBar = Roact.createElement(FullscreenTitleBar, {
title = "Roblox",
isTriggered = self.state.isTriggered,
onDisappear = self.hideTitleBar,
exitFullscreen = function()
print "Exit Fullscreen"
end,
closeRoblox = function()
print "Close Roblox"
end,
}),
})
}),
})
end
return function(target)
local handle = Roact.mount(Roact.createElement(StoryView, {}, {
Story = Roact.createElement(TitleBarStory),
}), target, "FullscreenTitleBar")
return function()
Roact.unmount(handle)
end
end
@@ -0,0 +1,264 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView)
local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem)
local Bar = script.Parent.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(App.ImageSet.Images)
local IconSize = require(App.ImageSet.Enum.IconSize)
local HeaderBar = require(Bar.HeaderBar)
local IconButton = require(UIBlox.App.Button.IconButton)
local TextButton = require(UIBlox.App.Button.TextButton)
local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent)
local renderRightIcons = function()
return Roact.createFragment({
search = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/search"],
onActivated = function()
print("Opening Search!")
end,
layoutOrder = 1,
}),
premium = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/goldrobux"],
onActivated = function()
print("Oooh Shiny!")
end,
layoutOrder = 2,
}),
alert = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/notificationOn"],
onActivated = function()
print("Alert!")
end,
layoutOrder = 3,
}),
})
end
local function BarDemo()
return Roact.createElement(HeaderBar, {
title = string.rep("Header Bar Story", 1),
renderLeft = HeaderBar.renderLeft.backButton(function()
print("navProps.navigation.pop()")
end),
renderRight = renderRightIcons,
})
end
local function BarWithTextButtonsDemo()
return Roact.createElement(HeaderBar, {
title = string.rep("Header Bar Story", 1),
renderLeft = function()
return Roact.createFragment({
search = Roact.createElement(TextButton, {
text = "Action 1",
onActivated = function()
print("Opening Search!")
end,
layoutOrder = 1,
}),
})
end,
renderRight = function()
return Roact.createFragment({
search = Roact.createElement(TextButton, {
text = "Action 2",
onActivated = function()
print("Opening Search!")
end,
layoutOrder = 1,
}),
})
end,
})
end
local function HeaderBarWithSearchBox()
return Roact.createElement(HeaderBar, {
title = "",
renderLeft = HeaderBar.renderLeft.backButton(function()
print("navProps.navigation.pop()")
end),
renderCenter = function()
return Roact.createFragment({
searchBoxMock = Roact.createElement(ImageSetComponent.Label, {
Image = Images["component_assets/circle_17_stroke_1"],
SliceCenter = Rect.new(8, 8, 9, 9),
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.new(1, 0, 0, 36),
BackgroundTransparency = 1,
}),
})
end,
})
end
local function HeaderBarWithOnlySearchBox()
return Roact.createElement(HeaderBar, {
title = "",
renderLeft = nil,
renderCenter = function()
return Roact.createFragment({
searchBoxMock = Roact.createElement(ImageSetComponent.Label, {
Image = Images["component_assets/circle_17_stroke_1"],
SliceCenter = Rect.new(8, 8, 9, 9),
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.new(1, 0, 0, 36),
BackgroundTransparency = 1,
}),
})
end,
})
end
local function HeaderBarWithRootTitle()
return Roact.createElement(HeaderBar, {
title = "Avatar",
isRootTitle = true,
renderRight = renderRightIcons,
})
end
local function HeaderBarWithRootTitleAndSearchBoxForTablet()
return Roact.createElement(HeaderBar, {
title = "Discover",
isRootTitle = true,
renderCenter = function()
return Roact.createFragment({
searchBoxMock = Roact.createElement(ImageSetComponent.Label, {
Image = Images["component_assets/circle_17_stroke_1"],
SliceCenter = Rect.new(8, 8, 9, 9),
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.new(0, 250, 0, 36),
BackgroundTransparency = 1,
}),
})
end,
renderRight = renderRightIcons,
})
end
local function HeaderBarWithBackButtonAndSearchBoxForTablet()
return Roact.createElement(HeaderBar, {
renderLeft = HeaderBar.renderLeft.backButton(function()
print("navProps.navigation.pop()")
end),
renderCenter = function()
return Roact.createFragment({
searchBoxMock = Roact.createElement(ImageSetComponent.Label, {
Image = Images["component_assets/circle_17_stroke_1"],
SliceCenter = Rect.new(8, 8, 9, 9),
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.new(0, 250, 0, 36),
BackgroundTransparency = 1,
}),
})
end,
renderRight = renderRightIcons,
})
end
return function(target)
local handle = Roact.mount(Roact.createElement(StoryView, {}, {
Story = Roact.createElement(StoryItem, {
size = UDim2.fromScale(1, 1),
title = "HeaderBar",
subTitle = "App.Bar.HeaderBar",
}, {
layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 15),
}),
frame = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(700, 45),
LayoutOrder = 1,
}, {
headerBar = Roact.createElement(BarDemo)
}),
frame2 = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(361, 45),
LayoutOrder = 2,
}, {
headerBar = Roact.createElement(BarDemo)
}),
frame3 = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(300, 45),
LayoutOrder = 3,
}, {
headerBar = Roact.createElement(BarDemo)
}),
frame4 = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(700, 45),
LayoutOrder = 4,
}, {
headerBar = Roact.createElement(BarWithTextButtonsDemo)
}),
frame5 = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(361, 45),
LayoutOrder = 5,
}, {
headerBar = Roact.createElement(BarWithTextButtonsDemo)
}),
frame6 = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(300, 45),
LayoutOrder = 6,
}, {
headerBar = Roact.createElement(BarWithTextButtonsDemo)
}),
frameHeaderBarWithSearchBoxForPhone = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(450, 45),
LayoutOrder = 7,
}, {
headerBar = Roact.createElement(HeaderBarWithSearchBox)
}),
frameHeaderBarWithOnlySearchBoxForPhone = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(450, 45),
LayoutOrder = 8,
}, {
headerBar = Roact.createElement(HeaderBarWithOnlySearchBox)
}),
frameHeaderBarWithRootTitleForPhone = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(450, 45),
LayoutOrder = 9,
}, {
headerBar = Roact.createElement(HeaderBarWithRootTitle)
}),
frameHeaderBarWithRootTitleAndSearchBoxForTablet = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(950, 45),
LayoutOrder = 10,
}, {
headerBar = Roact.createElement(HeaderBarWithRootTitleAndSearchBoxForTablet)
}),
frameHeaderBarWithBackButtonAndSearchBoxForTablet = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(950, 45),
LayoutOrder = 11,
}, {
headerBar = Roact.createElement(HeaderBarWithBackButtonAndSearchBoxForTablet)
}),
}),
}), target, "HeaderBar")
return function()
Roact.unmount(handle)
end
end
@@ -0,0 +1,124 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView)
local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem)
local Bar = script.Parent.Parent
local App = Bar.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local IconSize = require(App.ImageSet.Enum.IconSize)
local Images = require(App.ImageSet.Images)
local RootHeaderBar = require(Bar.RootHeaderBar)
local IconButton = require(App.Button.IconButton)
local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent)
local function RootHeaderBarWithSearchBox()
return Roact.createElement(RootHeaderBar, {
title = "Hello",
renderCenter = function()
return Roact.createFragment({
searchBoxMock = Roact.createElement(ImageSetComponent.Label, {
Image = Images["component_assets/circle_17_stroke_1"],
SliceCenter = Rect.new(8, 8, 9, 9),
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.fromOffset(200, 36),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
}),
})
end,
renderRight = function()
return Roact.createFragment({
search = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/search"],
onActivated = function()
print("Opening Search!")
end,
layoutOrder = 1,
}),
premium = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/goldrobux"],
onActivated = function()
print("Oooh Shiny!")
end,
layoutOrder = 2,
}),
alert = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/notificationOn"],
onActivated = function()
print("Alert!")
end,
layoutOrder = 3,
}),
})
end,
})
end
local function RootHeaderBarForPhone()
return Roact.createElement(RootHeaderBar, {
title = "Hello",
renderRight = function()
return Roact.createFragment({
search = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/search"],
onActivated = function()
print("Opening Search!")
end,
layoutOrder = 1,
}),
premium = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/goldrobux"],
onActivated = function()
print("Oooh Shiny!")
end,
layoutOrder = 2,
}),
alert = Roact.createElement(IconButton, {
iconSize = IconSize.Medium,
icon = Images["icons/common/notificationOn"],
onActivated = function()
print("Alert!")
end,
layoutOrder = 3,
}),
})
end,
})
end
return function(target)
local handle = Roact.mount(Roact.createElement(StoryView, {}, {
Story = Roact.createElement(StoryItem, {
size = UDim2.new(1, 0, 1, 0),
title = "RootHeaderBar",
subTitle = "App.Bar.RootHeaderBar",
}, {
layout = Roact.createElement("UIListLayout"),
frame1 = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(800, 45),
}, {
headerBar = Roact.createElement(RootHeaderBarWithSearchBox)
}),
frame2 = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(350, 45),
}, {
headerBar = Roact.createElement(RootHeaderBarForPhone)
}),
})
}), target, "HeaderBar")
return function()
Roact.unmount(handle)
end
end
@@ -0,0 +1,181 @@
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Cryo = require(Packages.Cryo)
local RoactGamepad = require(Packages.RoactGamepad)
local t = require(Packages.t)
local FitFrame = require(Packages.FitFrame)
local FitFrameOnAxis = FitFrame.FitFrameOnAxis
local PrimaryContextualButton = require(Button.PrimaryContextualButton)
local PrimarySystemButton = require(Button.PrimarySystemButton)
local IconButton = require(Button.IconButton)
local withStyle = require(UIBlox.Core.Style.withStyle)
local IconSize = require(App.ImageSet.Enum.IconSize)
local getPageMargin = require(App.Container.getPageMargin)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local validateButtonProps = require(Button.validateButtonProps)
local validateIconButtonProps = IconButton.validateProps
local ActionBar = Roact.PureComponent:extend("ActionBar")
local BUTTON_PADDING = 12
local BUTTON_HEIGHT = 48
local ICON_SIZE = 36
function ActionBar:init()
self.buttonRefs = RoactGamepad.createRefCache()
self.state = {
frameWidth = 0
}
self.updateFrameSize = function(rbx)
local frameWidth = rbx.AbsoluteSize.X
if frameWidth ~= self.state.frameWidth then
self:setState({
frameWidth = frameWidth,
})
end
end
end
ActionBar.validateProps = t.strictInterface({
-- buttons: A table of button tables that contain props that PrimaryContextualButton allow.
button = t.optional(t.strictInterface({
props = validateButtonProps,
})),
-- icons: A table of button tables that contain props that IconButton allow.
icons = t.optional(t.array(t.strictInterface({
props = validateIconButtonProps
}))),
-- Children
[Roact.Children] = t.optional(t.table),
-- optional parameters for RoactGamepad
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionDown = t.optional(t.table),
[Roact.Ref] = t.optional(t.table),
})
function ActionBar:render()
return withStyle(function(stylePalette)
local margin = getPageMargin(self.state.frameWidth)
local contentWidth = self.state.frameWidth - margin * 2
local iconSize = IconSize.Medium
local iconNumber = 0
if self.props.icons and #self.props.icons then
iconNumber = #self.props.icons
end
local buttonTable = {}
if iconNumber ~= 0 then
for iconButtonIndex, iconButton in ipairs(self.props.icons) do
local newProps = {
layoutOrder = iconButtonIndex,
iconSize = iconSize,
}
local iconButtonProps = Cryo.Dictionary.join(newProps, iconButton.props)
if UIBloxConfig.enableExperimentalGamepadSupport then
local gamepadFrameProps = {
Size = UDim2.fromOffset(ICON_SIZE,ICON_SIZE),
BackgroundTransparency = 1,
[Roact.Ref] = self.buttonRefs[iconButtonIndex],
NextSelectionUp = nil,
NextSelectionDown = nil,
NextSelectionLeft = iconButtonIndex > 1 and self.buttonRefs[iconButtonIndex - 1] or nil,
NextSelectionRight = iconButtonIndex < iconNumber and self.buttonRefs[iconButtonIndex + 1] or nil,
inputBindings = {
[Enum.KeyCode.ButtonA] = iconButtonProps.onActivated,
},
}
table.insert(buttonTable, Roact.createElement(RoactGamepad.Focusable.Frame, gamepadFrameProps, {
Roact.createElement(IconButton, iconButtonProps)
}))
else
table.insert(buttonTable, Roact.createElement(IconButton, iconButtonProps))
end
end
end
if self.props.button then
local button = self.props.button
local buttonSize = UDim2.fromOffset(contentWidth - iconNumber * (ICON_SIZE + BUTTON_PADDING), BUTTON_HEIGHT)
local newProps = {
layoutOrder = iconNumber + 1,
size = buttonSize,
}
local buttonProps = Cryo.Dictionary.join(newProps, button.props)
if UIBloxConfig.enableExperimentalGamepadSupport then
local gamepadFrameProps = {
Size = buttonSize,
BackgroundTransparency = 1,
[Roact.Ref] = self.buttonRefs[iconNumber + 1],
NextSelectionUp = nil,
NextSelectionDown = nil,
NextSelectionLeft = iconNumber and self.buttonRefs[iconNumber] or nil,
NextSelectionRight = nil,
inputBindings = {
[Enum.KeyCode.ButtonA] = buttonProps.onActivated,
},
}
table.insert(buttonTable, Roact.createElement(RoactGamepad.Focusable.Frame, gamepadFrameProps, {
Roact.createElement(iconNumber == 0 and PrimarySystemButton or PrimaryContextualButton, buttonProps)
}))
else
table.insert(buttonTable,
Roact.createElement(iconNumber == 0 and PrimarySystemButton or PrimaryContextualButton, buttonProps))
end
end
if self.props[Roact.Children] then
buttonTable = self.props[Roact.Children]
end
return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, {
BackgroundTransparency = 1,
minimumSize = UDim2.new(1, 0, 0, BUTTON_HEIGHT),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Position = UDim2.new(0, 0, 1, -24),
AnchorPoint = Vector2.new(0, 1),
contentPadding = UDim.new(0, BUTTON_PADDING),
[Roact.Ref] = self.props[Roact.Ref],
[Roact.Change.AbsoluteSize] = self.updateFrameSize,
margin = {
left = margin,
right = margin,
top = 0,
bottom = 0
},
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
},
buttonTable
)
end)
end
return ActionBar
@@ -0,0 +1,111 @@
return function()
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(App.ImageSet.Images)
local icon = Images["icons/common/robux_small"]
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ActionBar = require(Button.ActionBar)
it("should create and destroy ActionBar with one button without errors", function()
local element = mockStyleComponent({
ActionBar = Roact.createElement(ActionBar, {
button = {
props = {
onActivated = function() end,
text = "Button",
},
}
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy ActionBar with one button and one icon button without errors", function()
local element = mockStyleComponent({
ActionBar = Roact.createElement(ActionBar, {
button = {
props = {
onActivated = function() end,
text = "Button",
icon = icon,
},
},
icons = {
{
props = {
anchorPoint = Vector2.new(0.5, 0.5),
position = UDim2.fromScale(0.5, 0.5),
icon = icon,
userInteractionEnabled = true,
onActivated = function()
print("Text Button Clicked!")
end,
}
}
}
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy ActionBar with one button and two icon button without errors", function()
local element = mockStyleComponent({
ActionBar = Roact.createElement(ActionBar, {
button = {
props = {
onActivated = function() end,
text = "Button",
icon = icon,
},
},
icons = {
{
props = {
anchorPoint = Vector2.new(0.5, 0.5),
position = UDim2.fromScale(0.5, 0.5),
icon = icon,
userInteractionEnabled = true,
onActivated = function()
print("Text Button Clicked!")
end,
}
},
{
props = {
anchorPoint = Vector2.new(0.5, 0.5),
position = UDim2.fromScale(0.5, 0.5),
icon = icon,
userInteractionEnabled = true,
onActivated = function()
print("Text Button Clicked!")
end,
}
}
}
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy a ActionBar with children without errors", function()
local element = mockStyleComponent({
ActionBar = Roact.createElement(ActionBar, {}, {
ChildFrame = Roact.createElement("Frame", {})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,65 @@
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local RoactGamepad = require(Packages.RoactGamepad)
local Images = require(App.ImageSet.Images)
local CursorKind = require(App.SelectionImage.CursorKind)
local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider)
local validateButtonProps = require(Button.validateButtonProps)
local GenericButton = require(UIBlox.Core.Button.GenericButton)
local ControlState = require(UIBlox.Core.Control.Enum.ControlState)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local AlertButton = Roact.PureComponent:extend("AlertButton")
local BUTTON_STATE_COLOR = {
[ControlState.Default] = "Alert",
}
local CONTENT_STATE_COLOR = {
[ControlState.Default] = "Alert",
}
AlertButton.defaultProps = {
isDisabled = false,
isLoading = false,
}
function AlertButton:render()
assert(validateButtonProps(self.props))
local image = Images["component_assets/circle_17_stroke_1"]
local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[GenericButton] or GenericButton
return withSelectionCursorProvider(function(getSelectionCursor)
return Roact.createElement(genericButtonComponent, {
Size = self.props.size,
AnchorPoint = self.props.anchorPoint,
Position = self.props.position,
LayoutOrder = self.props.layoutOrder,
SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset),
icon = self.props.icon,
text = self.props.text,
isDisabled = self.props.isDisabled,
isLoading = self.props.isLoading,
onActivated = self.props.onActivated,
onStateChanged = self.props.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
buttonImage = image,
buttonStateColorMap = BUTTON_STATE_COLOR,
contentStateColorMap = CONTENT_STATE_COLOR,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
[Roact.Ref] = self.props[Roact.Ref],
})
end)
end
return AlertButton
@@ -0,0 +1,62 @@
return function()
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(App.ImageSet.Images)
local icon = Images["icons/common/robux_small"]
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local AlertButton = require(Button.AlertButton)
it("should create and destroy Alert Button with text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(AlertButton, {
text = "Button",
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Alert Button with text icon only without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(AlertButton, {
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Alert Button with text and text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(AlertButton, {
text = "Button",
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy a blank Alert Button without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(AlertButton, {
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,142 @@
local ButtonRoot = script.Parent
local AppRoot = ButtonRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Cryo = require(Packages.Cryo)
local RoactGamepad = require(Packages.RoactGamepad)
local AlertButton = require(ButtonRoot.AlertButton)
local PrimaryContextualButton = require(ButtonRoot.PrimaryContextualButton)
local PrimarySystemButton = require(ButtonRoot.PrimarySystemButton)
local SecondaryButton = require(ButtonRoot.SecondaryButton)
local GetTextSize = require(UIBlox.Core.Text.GetTextSize)
local withStyle = require(UIBlox.Core.Style.withStyle)
local FitFrame = require(Packages.FitFrame)
local FitFrameOnAxis = FitFrame.FitFrameOnAxis
local ButtonType = require(ButtonRoot.Enum.ButtonType)
local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local BUTTON_HEIGHT = 36
local ButtonStack = Roact.PureComponent:extend("ButtonStack")
ButtonStack.defaultProps = {
buttonHeight = BUTTON_HEIGHT,
marginBetween = 12,
minHorizontalButtonPadding = 8,
}
function ButtonStack:init()
self.buttonRefs = RoactGamepad.createRefCache()
self.state = {
frameWidth = 0
}
self.updateFrameSize = function(rbx)
local frameWidth = rbx.AbsoluteSize.X
if frameWidth ~= self.state.frameWidth then
self:setState({
frameWidth = frameWidth,
})
end
end
end
function ButtonStack:render()
assert(validateButtonStack(self.props))
return withStyle(function(stylePalette)
local font = stylePalette.Font
local textSize = font.Body.RelativeSize * font.BaseSize
local buttons = self.props.buttons
local paddingBetween = #buttons > 1 and self.props.marginBetween or 0
local nonStackedButtonWidth = (self.state.frameWidth / #buttons) - (paddingBetween * (#buttons - 1) / #buttons)
local isButtonStacked = false
local fillDirection
if self.props.forcedFillDirection then
isButtonStacked = self.props.forcedFillDirection == Enum.FillDirection.Vertical
fillDirection = self.props.forcedFillDirection
else
for _, button in ipairs(buttons) do
local buttonTextWidth = GetTextSize(
button.props.text or "",
textSize,
font.Body.Font,
Vector2.new(self.state.frameWidth, self.props.buttonHeight)
)
if buttonTextWidth.X > (nonStackedButtonWidth - (2 * self.props.minHorizontalButtonPadding)) then
isButtonStacked = true
break
end
end
fillDirection = isButtonStacked and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal
end
local buttonSize = isButtonStacked and UDim2.new(1, 0, 0, self.props.buttonHeight)
or UDim2.new(0, nonStackedButtonWidth, 0, self.props.buttonHeight)
local buttonTable = {}
for colIndex, button in ipairs(buttons) do
local newProps = {
layoutOrder = isButtonStacked and (#buttons - colIndex) or colIndex,
size = buttonSize,
}
local buttonProps = Cryo.Dictionary.join(newProps, button.props)
local buttonComponent
if button.buttonType == ButtonType.PrimaryContextual then
buttonComponent = PrimaryContextualButton
elseif button.buttonType == ButtonType.PrimarySystem then
buttonComponent = PrimarySystemButton
elseif button.buttonType == ButtonType.Alert then
buttonComponent = AlertButton
else
buttonComponent = SecondaryButton
end
if UIBloxConfig.enableExperimentalGamepadSupport then
local gamepadProps = {
[Roact.Ref] = self.buttonRefs[colIndex],
NextSelectionUp = (isButtonStacked and colIndex > 1) and self.buttonRefs[colIndex - 1] or nil,
NextSelectionDown = (isButtonStacked and colIndex < #buttons) and self.buttonRefs[colIndex + 1] or nil,
NextSelectionLeft = (not isButtonStacked and colIndex > 1) and self.buttonRefs[colIndex - 1] or nil,
NextSelectionRight = (not isButtonStacked and colIndex < #buttons) and self.buttonRefs[colIndex + 1] or nil,
}
local buttonPropsWithGamepad = Cryo.Dictionary.join(buttonProps, gamepadProps)
table.insert(buttonTable, Roact.createElement(buttonComponent, buttonPropsWithGamepad))
else
table.insert(buttonTable, Roact.createElement(buttonComponent, buttonProps))
end
end
return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, {
BackgroundTransparency = 1,
contentPadding = UDim.new(0, paddingBetween),
FillDirection = fillDirection,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
LayoutOrder = 3,
minimumSize = UDim2.new(1, 0, 0, self.props.buttonHeight),
[Roact.Ref] = self.props[Roact.Ref],
[Roact.Change.AbsoluteSize] = self.updateFrameSize,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
},
buttonTable
)
end)
end
return ButtonStack
@@ -0,0 +1,33 @@
local ButtonRoot = script.Parent
local AppRoot = ButtonRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ButtonStack = require(script.Parent.ButtonStack)
local DEFAULT_REQUIRED_PROPS = {
buttons = {
{
props = {
text = "test",
onActivated = function() end,
},
}
},
}
return function()
describe("lifecycle", function()
it("should mount and unmount button stacks without issue", function()
local tree = mockStyleComponent(
Roact.createElement(ButtonStack, DEFAULT_REQUIRED_PROPS)
)
local handle = Roact.mount(tree)
Roact.unmount(handle)
end)
end)
end
@@ -0,0 +1,13 @@
local ButtonRoot = script.Parent.Parent
local AppRoot = ButtonRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local enumerate = require(Packages.enumerate)
return enumerate("ButtonType", {
"Alert",
"PrimaryContextual",
"PrimarySystem",
"Secondary",
})
@@ -0,0 +1,157 @@
local App = script:FindFirstAncestor("App")
local UIBlox = App.Parent
local Core = UIBlox.Core
local Packages = UIBlox.Parent
local t = require(Packages.t)
local Roact = require(Packages.Roact)
local enumerate = require(Packages.enumerate)
local Interactable = require(Core.Control.Interactable)
local ControlState = require(Core.Control.Enum.ControlState)
local getContentStyle = require(Core.Button.getContentStyle)
local getIconSize = require(App.ImageSet.getIconSize)
local enumerateValidator = require(UIBlox.Utility.enumerateValidator)
local bindingValidator = require(Core.Utility.bindingValidator)
local validateImage = require(Core.ImageSet.Validator.validateImage)
local withStyle = require(Core.Style.withStyle)
local HoverButtonBackground = require(Core.Button.HoverButtonBackground)
local ImageSetComponent = require(Core.ImageSet.ImageSetComponent)
local IconSize = require(App.ImageSet.Enum.IconSize)
local IconButton = Roact.PureComponent:extend("IconButton")
IconButton.debugProps = enumerate("debugProps", {
"controlState",
})
IconButton.validateProps = t.strictInterface({
-- The state change callback for the button
onStateChanged = t.optional(t.callback),
-- Is the button visually disabled
isDisabled = t.optional(t.boolean),
colorStyleDefault = t.optional(t.string),
colorStyleHover = t.optional(t.string),
--A Boolean value that determines whether user events are ignored and sink input
userInteractionEnabled = t.optional(t.boolean),
-- The activated callback for the button
onActivated = t.optional(t.callback),
anchorPoint = t.optional(t.Vector2),
layoutOrder = t.optional(t.number),
position = t.optional(t.UDim2),
size = t.optional(t.UDim2),
icon = t.optional(validateImage),
iconSize = t.optional(enumerateValidator(IconSize)),
iconColor3 = t.optional(t.Color3),
iconTransparency = t.optional(t.union(t.number, bindingValidator(t.number))),
[Roact.Children] = t.optional(t.table),
-- Override the default controlState
[IconButton.debugProps.controlState] = t.optional(enumerateValidator(ControlState)),
})
IconButton.defaultProps = {
anchorPoint = Vector2.new(0, 0),
layoutOrder = 0,
position = UDim2.new(0, 0, 0, 0),
size = nil,
icon = "",
iconSize = IconSize.Medium,
colorStyleDefault = "SystemPrimaryDefault",
colorStyleHover = "SystemPrimaryDefault",
iconColor3 = nil,
iconTransparency = nil,
isDisabled = false,
userInteractionEnabled = true,
[IconButton.debugProps.controlState] = nil,
}
function IconButton:init()
self:setState({
controlState = ControlState.Initialize
})
self.onStateChanged = function(oldState, newState)
self:setState({
controlState = newState,
})
if self.props.onStateChanged then
self.props.onStateChanged(oldState, newState)
end
end
local iconSizeToSizeScale = {
[IconSize.Small] = 1,
[IconSize.Medium] = 2,
[IconSize.Large] = 3,
[IconSize.XLarge] = 4,
[IconSize.XXLarge] = 5,
}
self.getSize = function(iconSizeMeasurement)
if self.props.size then
return self.props.size
end
local iconSize = self.props.iconSize
local extents = iconSizeMeasurement + 4 * iconSizeToSizeScale[iconSize]
return UDim2.fromOffset(extents, extents)
end
end
function IconButton:render()
return withStyle(function(style)
local iconSizeMeasurement = getIconSize(self.props.iconSize)
local size = self.getSize(iconSizeMeasurement)
local currentState = self.props[IconButton.debugProps.controlState] or self.state.controlState
local iconStateColorMap = {
[ControlState.Default] = self.props.colorStyleDefault,
[ControlState.Hover] = self.props.colorStyleHover,
}
local iconStyle = getContentStyle(iconStateColorMap, currentState, style)
return Roact.createElement(Interactable, {
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
Position = self.props.position,
Size = size,
isDisabled = self.props.isDisabled,
onStateChanged = self.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
BackgroundTransparency = 1,
AutoButtonColor = false,
[Roact.Event.Activated] = self.props.onActivated,
}, {
sizeConstraint = Roact.createElement("UISizeConstraint", {
MinSize = Vector2.new(iconSizeMeasurement, iconSizeMeasurement),
}),
imageLabel = Roact.createElement(ImageSetComponent.Label, {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(iconSizeMeasurement, iconSizeMeasurement),
BackgroundTransparency = 1,
Image = self.props.icon,
ImageColor3 = self.props.iconColor3 or iconStyle.Color,
ImageTransparency = self.props.iconTransparency or iconStyle.Transparency,
},
self.props[Roact.Children]
),
background = currentState == ControlState.Hover and Roact.createElement(HoverButtonBackground),
})
end)
end
return IconButton
@@ -0,0 +1,293 @@
return function()
local IconButton = require(script.Parent.IconButton)
local App = script:FindFirstAncestor("App")
local UIBlox = App.Parent
local Core = UIBlox.Core
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ControlState = require(Core.Control.Enum.ControlState)
local IconSize = require(App.ImageSet.Enum.IconSize)
describe("props", function()
local BUTTON_NAME = "test:" .. tostring(math.random(0, 999))
local runTest = function(props)
local folder = Instance.new("Folder")
local element = mockStyleComponent({
[BUTTON_NAME] = Roact.createElement(IconButton, props),
})
local instance = Roact.mount(element, folder)
return folder, function()
Roact.unmount(instance)
folder:Destroy()
end
end
local function getImageLabel(folder)
return folder:FindFirstChild("imageLabel", true)
end
local function getGuiObjectRoot(folder)
return folder:FindFirstChild(BUTTON_NAME, true)
end
local function getIconColor3(folder)
return getImageLabel(folder).ImageColor3
end
local function getIconTransparency(folder)
return getImageLabel(folder).ImageTransparency
end
local function getGuiObjectRootAbsoluteSize(folder)
return getGuiObjectRoot(folder).AbsoluteSize
end
local function getGuiObjectRootSize(folder)
return getGuiObjectRoot(folder).Size
end
describe("iconSize", function()
it("SHOULD resize gui object root AbsoluteSize", function()
local smallFolder, smallCleanup = runTest({
iconSize = IconSize.Small,
})
local mediumFolder, mediumCleanup = runTest({
iconSize = IconSize.Medium,
})
expect(getGuiObjectRootAbsoluteSize(smallFolder)).to.never.equal(getGuiObjectRootAbsoluteSize(mediumFolder))
smallCleanup()
mediumCleanup()
end)
end)
describe("appearance override props", function()
it("SHOULD override ImageLabel.ImageColor3 with iconColor3", function()
for _ = 1, 50 do
local randomColor = BrickColor.random().Color
local folder, cleanup = runTest({
iconColor3 = randomColor,
})
expect(getIconColor3(folder)).to.equal(randomColor)
cleanup()
end
end)
it("SHOULD override ImageLabel.ImageTransparency with iconTransparency", function()
for transparency = 0, 1, 0.1 do
local folder, cleanup = runTest({
iconTransparency = transparency,
})
expect(getIconTransparency(folder)).to.be.near(transparency, 0.001)
cleanup()
end
end)
it("SHOULD override ImageLabel.ImageTransparency with iconTransparency from RoactBinding value", function()
for transparency = 0, 1, 0.1 do
local folder, cleanup = runTest({
iconTransparency = Roact.createBinding(transparency),
})
expect(getIconTransparency(folder)).to.be.near(transparency, 0.001)
cleanup()
end
end)
it("SHOULD override root guiObject.AbsoluteSize with size", function()
local testSizes = {
UDim2.fromScale(0.5, 0.5),
UDim2.fromScale(1, 1),
UDim2.fromOffset(1000, 10),
UDim2.fromOffset(0, 100),
}
for _, size in ipairs(testSizes) do
local controlGroupFolder, cleanupControlGroup = runTest({
size = nil,
})
local variableGroupFolder, cleanupVariableGroup = runTest({
size = size,
})
local controlSize = getGuiObjectRootAbsoluteSize(controlGroupFolder)
local variableSize = getGuiObjectRootAbsoluteSize(variableGroupFolder)
expect(controlSize).to.never.equal(variableSize)
expect(getGuiObjectRootSize(variableGroupFolder)).to.equal(size)
cleanupControlGroup()
cleanupVariableGroup()
end
end)
end)
describe("positional props", function()
it("SHOULD respect AnchorPoint", function()
local function testAnchorPoint(anchorPoint)
local folder, cleanup = runTest({
anchorPoint = anchorPoint,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.AnchorPoint).to.equal(anchorPoint)
cleanup()
end
testAnchorPoint(Vector2.new(0, 0))
testAnchorPoint(Vector2.new(0, 1))
testAnchorPoint(Vector2.new(1, 0))
testAnchorPoint(Vector2.new(1, 1))
testAnchorPoint(Vector2.new(0.5, 0.5))
end)
it("SHOULD respect Position", function()
local function testPosition(position)
local folder, cleanup = runTest({
position = position,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.Position).to.equal(position)
cleanup()
end
testPosition(UDim2.new(0, 0, 0, 0))
testPosition(UDim2.new(0.5, 10, 1, 20))
testPosition(UDim2.fromScale(1, 1))
testPosition(UDim2.fromOffset(100, 000))
end)
it("SHOULD respect LayoutOrder", function()
local function testLayoutOrder(layoutOrder)
local folder, cleanup = runTest({
layoutOrder = layoutOrder,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.LayoutOrder).to.equal(layoutOrder)
cleanup()
end
testLayoutOrder(0)
testLayoutOrder(1)
testLayoutOrder(2)
end)
it("SHOULD respect Size", function()
local function testSize(size)
local folder, cleanup = runTest({
size = size,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.Size).to.equal(size)
cleanup()
end
testSize(UDim2.new(0, 0, 0, 0))
testSize(UDim2.new(0.5, 10, 1, 20))
testSize(UDim2.fromScale(1, 1))
testSize(UDim2.fromOffset(100, 000))
end)
end)
describe("debugProps.controlState", function()
local function isShowingBackground(folder)
return folder:FindFirstChild("background", true) ~= nil
end
local function isImageTransparent(folder)
local imageLabel = folder:FindFirstChild("imageLabel", true)
assert(imageLabel, "imageLabel never mounted")
return imageLabel.ImageTransparency > 0
end
it("SHOULD render ControlState.Default with no issues", function()
local folder, cleanup = runTest({
[IconButton.debugProps.controlState] = ControlState.Default,
})
expect(isShowingBackground(folder)).to.equal(false)
expect(isImageTransparent(folder)).to.equal(false)
cleanup()
end)
it("SHOULD render ControlState.Hover with no issues", function()
local folder, cleanup = runTest({
[IconButton.debugProps.controlState] = ControlState.Hover,
})
expect(isShowingBackground(folder)).to.equal(true)
expect(isImageTransparent(folder)).to.equal(false)
cleanup()
end)
it("SHOULD render ControlState.Pressed with no issues", function()
local folder, cleanup = runTest({
[IconButton.debugProps.controlState] = ControlState.Pressed,
})
expect(isShowingBackground(folder)).to.equal(false)
expect(isImageTransparent(folder)).to.equal(true)
cleanup()
end)
it("SHOULD render ControlState.Disabled with no issues", function()
local folder, cleanup = runTest({
[IconButton.debugProps.controlState] = ControlState.Disabled,
})
expect(isShowingBackground(folder)).to.equal(false)
expect(isImageTransparent(folder)).to.equal(true)
cleanup()
end)
end)
end)
describe("children", function()
it("SHOULD render children passed to it", function()
local BUTTON_NAME = "test:" .. tostring(math.random(0, 999))
local COLOR = Color3.new(1, 0.5, 0)
local folder = Instance.new("Folder")
local element = mockStyleComponent({
[BUTTON_NAME] = Roact.createElement(IconButton, {}, {
childFrameElement = Roact.createElement("Frame", {
Size = UDim2.new(0, 16, 0, 16),
BackgroundColor3 = COLOR,
}),
}),
})
local instance = Roact.mount(element, folder)
local childElement = folder:FindFirstChild("childFrameElement", true)
expect(childElement).to.be.ok()
expect(childElement.BackgroundColor3).to.equal(COLOR)
Roact.unmount(instance)
folder:Destroy()
end)
end)
end
@@ -0,0 +1,65 @@
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local RoactGamepad = require(Packages.RoactGamepad)
local Images = require(App.ImageSet.Images)
local CursorKind = require(App.SelectionImage.CursorKind)
local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider)
local validateButtonProps = require(Button.validateButtonProps)
local GenericButton = require(UIBlox.Core.Button.GenericButton)
local ControlState = require(UIBlox.Core.Control.Enum.ControlState)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local PrimaryContextualButton = Roact.PureComponent:extend("PrimaryContextualButton")
local BUTTON_STATE_COLOR = {
[ControlState.Default] = "ContextualPrimaryDefault",
[ControlState.Hover] = "ContextualPrimaryOnHover",
}
local CONTENT_STATE_COLOR = {
[ControlState.Default] = "ContextualPrimaryContent",
}
PrimaryContextualButton.defaultProps = {
isDisabled = false,
isLoading = false,
}
function PrimaryContextualButton:render()
assert(validateButtonProps(self.props))
local image = Images["component_assets/circle_17"]
local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[GenericButton] or GenericButton
return withSelectionCursorProvider(function(getSelectionCursor)
return Roact.createElement(genericButtonComponent, {
Size = self.props.size,
AnchorPoint = self.props.anchorPoint,
Position = self.props.position,
LayoutOrder = self.props.layoutOrder,
SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset),
icon = self.props.icon,
text = self.props.text,
isDisabled = self.props.isDisabled,
isLoading = self.props.isLoading,
onActivated = self.props.onActivated,
onStateChanged = self.props.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
buttonImage = image,
buttonStateColorMap = BUTTON_STATE_COLOR,
contentStateColorMap = CONTENT_STATE_COLOR,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
[Roact.Ref] = self.props[Roact.Ref],
})
end)
end
return PrimaryContextualButton
@@ -0,0 +1,62 @@
return function()
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(App.ImageSet.Images)
local icon = Images["icons/common/robux_small"]
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local PrimaryContextualButton = require(Button.PrimaryContextualButton)
it("should create and destroy Primary Contextual Button with text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimaryContextualButton, {
text = "Button",
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Primary Contextual Button with text icon only without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimaryContextualButton, {
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Primary Contextual Button with text and text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimaryContextualButton, {
text = "Button",
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy a blank Primary Contextual Button without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimaryContextualButton, {
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,66 @@
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local RoactGamepad = require(Packages.RoactGamepad)
local Images = require(App.ImageSet.Images)
local CursorKind = require(App.SelectionImage.CursorKind)
local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider)
local validateButtonProps = require(Button.validateButtonProps)
local GenericButton = require(UIBlox.Core.Button.GenericButton)
local ControlState = require(UIBlox.Core.Control.Enum.ControlState)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local PrimarySystemButton = Roact.PureComponent:extend("PrimarySystemButton")
local BUTTON_STATE_COLOR = {
[ControlState.Default] = "SystemPrimaryDefault",
[ControlState.Hover] = "SystemPrimaryOnHover",
}
local CONTENT_STATE_COLOR = {
[ControlState.Default] = "SystemPrimaryContent",
}
PrimarySystemButton.defaultProps = {
isDisabled = false,
isLoading = false,
}
function PrimarySystemButton:render()
assert(validateButtonProps(self.props))
local image = Images["component_assets/circle_17"]
local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[GenericButton] or GenericButton
return withSelectionCursorProvider(function(getSelectionCursor)
return Roact.createElement(genericButtonComponent, {
Size = self.props.size,
AnchorPoint = self.props.anchorPoint,
Position = self.props.position,
LayoutOrder = self.props.layoutOrder,
SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset),
icon = self.props.icon,
text = self.props.text,
isDisabled = self.props.isDisabled,
isLoading = self.props.isLoading,
onActivated = self.props.onActivated,
onStateChanged = self.props.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
buttonImage = image,
buttonStateColorMap = BUTTON_STATE_COLOR,
contentStateColorMap = CONTENT_STATE_COLOR,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
[Roact.Ref] = self.props[Roact.Ref],
})
end)
end
return PrimarySystemButton
@@ -0,0 +1,62 @@
return function()
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(App.ImageSet.Images)
local icon = Images["icons/common/robux_small"]
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local PrimarySystemButton = require(Button.PrimarySystemButton)
it("should create and destroy Primary System Button with text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimarySystemButton, {
text = "Button",
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Primary System Button with text icon only without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimarySystemButton, {
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Primary System Button with text and text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimarySystemButton, {
text = "Button",
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy a blank Primary System Button without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(PrimarySystemButton, {
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,66 @@
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local RoactGamepad = require(Packages.RoactGamepad)
local Images = require(App.ImageSet.Images)
local CursorKind = require(App.SelectionImage.CursorKind)
local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider)
local validateButtonProps = require(Button.validateButtonProps)
local GenericButton = require(UIBlox.Core.Button.GenericButton)
local ControlState = require(UIBlox.Core.Control.Enum.ControlState)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local SecondaryButton = Roact.PureComponent:extend("SecondaryButton")
local BUTTON_STATE_COLOR = {
[ControlState.Default] = "SecondaryDefault",
[ControlState.Hover] = "SecondaryOnHover",
}
local CONTENT_STATE_COLOR = {
[ControlState.Default] = "SecondaryContent",
[ControlState.Hover] = "SecondaryOnHover",
}
SecondaryButton.defaultProps = {
isDisabled = false,
isLoading = false,
}
function SecondaryButton:render()
assert(validateButtonProps(self.props))
local image = Images["component_assets/circle_17_stroke_1"]
local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[GenericButton] or GenericButton
return withSelectionCursorProvider(function(getSelectionCursor)
return Roact.createElement(genericButtonComponent, {
Size = self.props.size,
AnchorPoint = self.props.anchorPoint,
Position = self.props.position,
LayoutOrder = self.props.layoutOrder,
SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset),
icon = self.props.icon,
text = self.props.text,
isDisabled = self.props.isDisabled,
isLoading = self.props.isLoading,
onActivated = self.props.onActivated,
onStateChanged = self.props.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
buttonImage = image,
buttonStateColorMap = BUTTON_STATE_COLOR,
contentStateColorMap = CONTENT_STATE_COLOR,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
[Roact.Ref] = self.props[Roact.Ref],
})
end)
end
return SecondaryButton
@@ -0,0 +1,62 @@
return function()
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(App.ImageSet.Images)
local icon = Images["icons/common/robux_small"]
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local SecondaryButton = require(Button.SecondaryButton)
it("should create and destroy Secondary Button with text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(SecondaryButton, {
text = "Button",
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Secondary Button with text icon only without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(SecondaryButton, {
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Secondary Button with text and text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(SecondaryButton, {
text = "Button",
icon = icon,
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy a blank Secondary Button without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(SecondaryButton, {
onActivated = function()end,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,145 @@
local App = script:FindFirstAncestor("App")
local UIBlox = App.Parent
local Core = UIBlox.Core
local Packages = UIBlox.Parent
local t = require(Packages.t)
local Roact = require(Packages.Roact)
local enumerate = require(Packages.enumerate)
local Interactable = require(Core.Control.Interactable)
local ControlState = require(Core.Control.Enum.ControlState)
local getContentStyle = require(Core.Button.getContentStyle)
local GetTextSize = require(Core.Text.GetTextSize)
local enumerateValidator = require(UIBlox.Utility.enumerateValidator)
local withStyle = require(Core.Style.withStyle)
local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel)
local HoverButtonBackground = require(Core.Button.HoverButtonBackground)
local VERTICAL_PADDING = 8
local HORIZONTAL_PADDING = 11
local TextButton = Roact.PureComponent:extend("TextButton")
TextButton.debugProps = enumerate("debugProps", {
"getTextSize",
"controlState",
})
TextButton.validateProps = t.strictInterface({
-- The state change callback for the button
onStateChanged = t.optional(t.callback),
-- Is the button visually disabled
isDisabled = t.optional(t.boolean),
fontStyle = t.optional(t.string),
colorStyleDefault = t.optional(t.string),
colorStyleHover = t.optional(t.string),
hoverBackgroundEnabled = t.optional(t.boolean),
richText = t.optional(t.boolean),
--A Boolean value that determines whether user events are ignored and sink input
userInteractionEnabled = t.optional(t.boolean),
-- The activated callback for the button
onActivated = t.optional(t.callback),
anchorPoint = t.optional(t.Vector2),
layoutOrder = t.optional(t.number),
position= t.optional(t.UDim2),
size = t.optional(t.UDim2),
text = t.optional(t.string),
-- A callback that replaces getTextSize implementation
[TextButton.debugProps.getTextSize] = t.optional(t.callback),
-- Override the default controlState
[TextButton.debugProps.controlState] = t.optional(enumerateValidator(ControlState)),
})
TextButton.defaultProps = {
anchorPoint = Vector2.new(0, 0),
layoutOrder = 0,
position = UDim2.new(0, 0, 0, 0),
size = UDim2.fromScale(0, 0),
text = "",
fontStyle = "Header2",
colorStyleDefault = "SystemPrimaryDefault",
colorStyleHover = "SystemPrimaryDefault",
hoverBackgroundEnabled = true,
richText = false,
isDisabled = false,
userInteractionEnabled = true,
[TextButton.debugProps.getTextSize] = GetTextSize,
[TextButton.debugProps.controlState] = nil,
}
function TextButton:init()
self:setState({
controlState = ControlState.Initialize
})
self.onStateChanged = function(oldState, newState)
self:setState({
controlState = newState,
})
if self.props.onStateChanged then
self.props.onStateChanged(oldState, newState)
end
end
end
function TextButton:render()
return withStyle(function(style)
local currentState = self.props[TextButton.debugProps.controlState] or self.state.controlState
local textStateColorMap = {
[ControlState.Default] = self.props.colorStyleDefault,
[ControlState.Hover] = self.props.colorStyleHover,
}
local textStyle = getContentStyle(textStateColorMap, currentState, style)
local fontStyle = style.Font[self.props.fontStyle]
local fontSize = fontStyle.RelativeSize * style.Font.BaseSize
local getTextSize = self.props[TextButton.debugProps.getTextSize]
local textWidth = getTextSize(self.props.text, fontSize, fontStyle.Font, Vector2.new(10000, 0)).X
return Roact.createElement(Interactable, {
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
Position = self.props.position,
Size = self.props.size,
isDisabled = self.props.isDisabled,
onStateChanged = self.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
BackgroundTransparency = 1,
AutoButtonColor = false,
[Roact.Event.Activated] = self.props.onActivated,
}, {
sizeConstraint = Roact.createElement("UISizeConstraint", {
MinSize = Vector2.new(textWidth + VERTICAL_PADDING*2, fontSize + HORIZONTAL_PADDING*2),
}),
textLabel = Roact.createElement(GenericTextLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
BackgroundTransparency = 1,
Text = self.props.text,
fontStyle = fontStyle,
colorStyle = textStyle,
RichText = self.props.richText,
}),
background = self.props.hoverBackgroundEnabled and currentState == ControlState.Hover
and Roact.createElement(HoverButtonBackground)
})
end)
end
return TextButton
@@ -0,0 +1,288 @@
return function()
local TextButton = require(script.Parent.TextButton)
local App = script:FindFirstAncestor("App")
local UIBlox = App.Parent
local Core = UIBlox.Core
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ControlState = require(Core.Control.Enum.ControlState)
local noOpt = function()
end
local text = "Button"
it("should create and destroy a button without errors", function()
local folder = Instance.new("Folder")
local element = mockStyleComponent({
button = Roact.createElement(TextButton, {
text = text,
onActivated = noOpt,
fontStyle = "Body",
colorStyleDefault = "UIDefault",
colorStyleHover = "UIDefault",
}),
})
local instance = Roact.mount(element, folder)
local label = folder:FindFirstChildWhichIsA("TextLabel", true)
expect(label.Text).to.equal(text)
Roact.unmount(instance)
end)
it("should create and destroy a button that is disabled without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(TextButton, {
text = text,
onActivated = noOpt,
isDisabled = true,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy a button without text without errors", function()
local element = mockStyleComponent({
button = Roact.createElement(TextButton, {
onActivated = noOpt,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should be created as a disabled button", function()
local buttonState = nil
local element = mockStyleComponent({
button = Roact.createElement(TextButton, {
onActivated = noOpt,
onStateChanged = function(_, newState)
buttonState = newState
end,
isDisabled = true,
}),
})
local instance = Roact.mount(element)
expect(buttonState).to.equal(ControlState.Disabled)
Roact.unmount(instance)
end)
describe("text prop", function()
local BUTTON_NAME = "test:" .. tostring(math.random(0, 999))
local runTest = function(props)
local folder = Instance.new("Folder")
local element = mockStyleComponent({
[BUTTON_NAME] = Roact.createElement(TextButton, props),
})
local instance = Roact.mount(element, folder)
return folder, function()
Roact.unmount(instance)
folder:Destroy()
end
end
it("SHOULD resize to text when not given size property", function()
local folder1, cleanup1 = runTest({
text = string.rep("!", 1),
[TextButton.debugProps.getTextSize] = function()
return Vector2.new(1, 1)
end,
})
local folder2, cleanup2 = runTest({
text = string.rep("!", 10),
[TextButton.debugProps.getTextSize] = function()
return Vector2.new(10, 1)
end,
})
local firstSize = folder1:FindFirstChild(BUTTON_NAME, true).AbsoluteSize
local secondSize = folder2:FindFirstChild(BUTTON_NAME, true).AbsoluteSize
expect(firstSize).to.never.equal(secondSize)
cleanup1()
cleanup2()
end)
end)
describe("positional props", function()
local BUTTON_NAME = "test:" .. tostring(math.random(0, 999))
local runTest = function(props)
local folder = Instance.new("Folder")
local element = mockStyleComponent({
[BUTTON_NAME] = Roact.createElement(TextButton, props),
})
local instance = Roact.mount(element, folder)
return folder, function()
Roact.unmount(instance)
folder:Destroy()
end
end
it("SHOULD respect AnchorPoint", function()
local function testAnchorPoint(anchorPoint)
local folder, cleanup = runTest({
anchorPoint = anchorPoint,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.AnchorPoint).to.equal(anchorPoint)
cleanup()
end
testAnchorPoint(Vector2.new(0, 0))
testAnchorPoint(Vector2.new(0, 1))
testAnchorPoint(Vector2.new(1, 0))
testAnchorPoint(Vector2.new(1, 1))
testAnchorPoint(Vector2.new(0.5, 0.5))
end)
it("SHOULD respect Position", function()
local function testPosition(position)
local folder, cleanup = runTest({
position = position,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.Position).to.equal(position)
cleanup()
end
testPosition(UDim2.new(0, 0, 0, 0))
testPosition(UDim2.new(0.5, 10, 1, 20))
testPosition(UDim2.fromScale(1, 1))
testPosition(UDim2.fromOffset(100, 000))
end)
it("SHOULD respect LayoutOrder", function()
local function testLayoutOrder(layoutOrder)
local folder, cleanup = runTest({
layoutOrder = layoutOrder,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.LayoutOrder).to.equal(layoutOrder)
cleanup()
end
testLayoutOrder(0)
testLayoutOrder(1)
testLayoutOrder(2)
end)
it("SHOULD respect Size", function()
local function testSize(size)
local folder, cleanup = runTest({
size = size,
})
local guiObject = folder:FindFirstChild(BUTTON_NAME, true)
expect(guiObject.Size).to.equal(size)
cleanup()
end
testSize(UDim2.new(0, 0, 0, 0))
testSize(UDim2.new(0.5, 10, 1, 20))
testSize(UDim2.fromScale(1, 1))
testSize(UDim2.fromOffset(100, 000))
end)
end)
describe("debugProps.controlState", function()
local BUTTON_NAME = "test:" .. tostring(math.random(0, 999))
local runTest = function(props)
local folder = Instance.new("Folder")
local element = mockStyleComponent({
[BUTTON_NAME] = Roact.createElement(TextButton, props),
})
local instance = Roact.mount(element, folder)
return folder, function()
Roact.unmount(instance)
folder:Destroy()
end
end
local function isShowingBackground(folder)
return folder:FindFirstChild("background", true) ~= nil
end
local function isTextTransparent(folder)
local textLabel = folder:FindFirstChild("textLabel", true)
assert(textLabel, "textLabel never mounted")
return textLabel.TextTransparency > 0
end
it("SHOULD render ControlState.Default with no issues", function()
local folder, cleanup = runTest({
[TextButton.debugProps.controlState] = ControlState.Default,
})
expect(isShowingBackground(folder)).to.equal(false)
expect(isTextTransparent(folder)).to.equal(false)
cleanup()
end)
it("SHOULD render ControlState.Hover with no issues", function()
local folder, cleanup = runTest({
[TextButton.debugProps.controlState] = ControlState.Hover,
})
expect(isShowingBackground(folder)).to.equal(true)
expect(isTextTransparent(folder)).to.equal(false)
cleanup()
end)
it("SHOULD render ControlState.Hover when background disabled with no issues", function()
local folder, cleanup = runTest({
[TextButton.debugProps.controlState] = ControlState.Hover,
hoverBackgroundEnabled = false,
})
expect(isShowingBackground(folder)).to.equal(false)
expect(isTextTransparent(folder)).to.equal(false)
cleanup()
end)
it("SHOULD render ControlState.Pressed with no issues", function()
local folder, cleanup = runTest({
[TextButton.debugProps.controlState] = ControlState.Pressed,
})
expect(isShowingBackground(folder)).to.equal(false)
expect(isTextTransparent(folder)).to.equal(true)
cleanup()
end)
it("SHOULD render ControlState.Disabled with no issues", function()
local folder, cleanup = runTest({
[TextButton.debugProps.controlState] = ControlState.Disabled,
})
expect(isShowingBackground(folder)).to.equal(false)
expect(isTextTransparent(folder)).to.equal(true)
cleanup()
end)
end)
end
@@ -0,0 +1,46 @@
local validatorRoot = script.Parent
local ButtonRoot = validatorRoot.Parent
local AppRoot = ButtonRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local t = require(Packages.t)
local Roact = require(Packages.Roact)
local enumerateValidator = require(UIBlox.Utility.enumerateValidator)
local validateButtonProps = require(ButtonRoot.validateButtonProps)
local ButtonType = require(ButtonRoot.Enum.ButtonType)
return t.strictInterface({
-- buttons: A table of button tables that contain props that PrimaryContextualButton,
-- AlertButton, PrimarySystemButton, or SecondaryButton allow. Also contains a prop "buttonType"
-- to determine which of these button types to use.
buttons = t.array(t.strictInterface({
buttonType = t.optional(enumerateValidator(ButtonType)),
props = validateButtonProps,
})),
buttonHeight = t.optional(t.numberMin(0)),
-- forceFillDirection: What fill direction to force into. If nil, then the fillDirection
-- will be Vertical and automatically change to Horizontal if any button's text is
-- too long.
forcedFillDirection = t.optional(t.enum(Enum.FillDirection)),
-- marginBetween: the margin between each button.
marginBetween = t.optional(t.numberMin(0)),
-- minHorizontalButtonPadding: The minimum left and right padding used to calculate
-- the when the button text overflows and automatically changes fillDirection.
-- The overflow calculation will be if the length of the button text is over
-- the button size - (2 * minHorizontalButtonPadding).
minHorizontalButtonPadding = t.optional(t.numberMin(0)),
-- optional parameters for RoactGamepad
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionDown = t.optional(t.table),
[Roact.Ref] = t.optional(t.table),
})
@@ -0,0 +1,49 @@
local Button = script.Parent
local App = Button.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Core = UIBlox.Core
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local validateImage = require(Core.ImageSet.Validator.validateImage)
return t.strictInterface({
--The size of the button
size = t.optional(t.UDim2),
--The anchor point of the button
anchorPoint = t.optional(t.Vector2),
--The position of the button
position = t.optional(t.UDim2),
--The layout order of the button
layoutOrder = t.optional(t.number),
--The icon of the button
icon = t.optional(validateImage),
--The text of the button
text = t.optional(t.string),
--Is the button disabled
isDisabled = t.optional(t.boolean),
--Is the button loading
isLoading = t.optional(t.boolean),
--The activated callback for the button
onActivated = t.callback,
--A Boolean value that determines whether user events are ignored and sink input
userInteractionEnabled = t.optional(t.boolean),
-- Gamepad Support props
NextSelectionDown = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
[Roact.Ref] = t.optional(t.table),
})
@@ -0,0 +1,94 @@
local SelectionGroup = script.Parent
local Small = SelectionGroup.Parent
local Cell = Small.Parent
local App = Cell.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Images = require(Packages.UIBlox.App.ImageSet.Images)
local GenericSelectionCell = require(Packages.UIBlox.Core.Cell.GenericSelectionCell)
local DEFAULT_IMAGE = Images["component_assets/circle_24_stroke_1"]
local SELECTED_IMAGE = Images["component_assets/circle_16"]
local DEFAULT_IMAGE_SIZE = 24
local SELECTED_IMAGE_SIZE = 16
local CELL_SIZE = 56
local SmallRadioButtonCell = Roact.PureComponent:extend("SmallRadioButtonCell")
SmallRadioButtonCell.validateProps = t.strictInterface({
-- Unique key to identify this selection.
key = t.string,
-- Text to display
text = t.optional(t.string),
-- Callback for when this selection is activated.
onActivated = t.optional(t.callback),
-- Whether this selection is selected or not.
isSelected = t.optional(t.boolean),
-- If this cell is disabled
isDisabled = t.optional(t.boolean),
-- If this cell should use the default control state
useDefaultControlState = t.optional(t.boolean),
-- The LayoutOrder.
layoutOrder = t.optional(t.number),
-- optional parameters for RoactGamepad
[Roact.Ref] = t.optional(t.table),
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionDown = t.optional(t.table),
SelectionImageObject = t.optional(t.table),
})
SmallRadioButtonCell.defaultProps = {
text = "",
isSelected = false,
}
function SmallRadioButtonCell:init()
self.onSetValue = function()
self.props.onActivated(self.props.key)
end
end
function SmallRadioButtonCell:render()
assert(self.validateProps(self.props))
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, CELL_SIZE),
BorderSizePixel = 0,
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
}, {
GenericSelectionCell = Roact.createElement(GenericSelectionCell, {
isSelected = self.props.isSelected,
isDisabled = self.props.isDisabled,
defaultImage = DEFAULT_IMAGE,
selectedImage = SELECTED_IMAGE,
defaultImageSize = DEFAULT_IMAGE_SIZE,
selectedImageSize = SELECTED_IMAGE_SIZE,
text = self.props.text,
onActivated = self.onSetValue,
useDefaultControlState = self.props.useDefaultControlState,
[Roact.Ref] = self.props[Roact.Ref],
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
SelectionImageObject = self.props.SelectionImageObject,
}),
})
end
return SmallRadioButtonCell
@@ -0,0 +1,25 @@
return function()
local SelectionGroup = script.Parent
local Small = SelectionGroup.Parent
local Cell = Small.Parent
local App = Cell.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local SmallRadioButtonCell = require(script.Parent.SmallRadioButtonCell)
it("should create and destroy SmallRadioButtonCell without errors", function()
local element = mockStyleComponent({
smallRadioButtonCell = Roact.createElement(SmallRadioButtonCell, {
key = "1",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,112 @@
local SelectionGroup = script.Parent
local Small = SelectionGroup.Parent
local Cell = Small.Parent
local App = Cell.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local RoactGamepad = require(Packages.RoactGamepad)
local t = require(Packages.t)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local withSelectionCursorProvider = require(UIBlox.App.SelectionImage.withSelectionCursorProvider)
local CursorKind = require(UIBlox.App.SelectionImage.CursorKind)
local SmallRadioButtonCell = require(UIBlox.App.Cell.Small.SelectionGroup.SmallRadioButtonCell)
local SmallRadioButtonGroup = Roact.PureComponent:extend("SmallRadioButtonGroup")
local buttonInterface = t.strictInterface({
text = t.string,
key = t.string,
isDisabled = t.optional(t.boolean),
})
SmallRadioButtonGroup.validateProps = t.strictInterface({
-- List of text, key pairs that will be used for each radio button.
items = t.optional(t.array(t.tuple(buttonInterface))),
-- Which key is currently selected.
selectedValue = t.optional(t.string),
-- Callback for when a cell is activated.
onActivated = t.callback,
-- Layout order for this component.
layoutOrder = t.optional(t.number),
-- If this cell should use the default control state
useDefaultControlState = t.optional(t.boolean),
-- optional parameters for RoactGamepad
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionDown = t.optional(t.table),
[Roact.Ref] = t.optional(t.table),
})
SmallRadioButtonGroup.defaultProps = {
selectedValue = nil,
}
function SmallRadioButtonGroup:init()
self.gamepadRefs = RoactGamepad.createRefCache()
end
function SmallRadioButtonGroup:render()
assert(self.validateProps(self.props))
local smallRadioButtonCellGroup = {}
smallRadioButtonCellGroup.layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 1),
})
for index, button in ipairs(self.props.items) do
if UIBloxConfig.enableExperimentalGamepadSupport then
smallRadioButtonCellGroup["smallRadioButtonCell"..button.key] =
withSelectionCursorProvider(function(getSelectionCursor)
return Roact.createElement(RoactGamepad.Focusable[SmallRadioButtonCell], {
key = button.key,
text = button.text,
onActivated = self.props.onActivated,
isSelected = self.props.selectedValue == button.key,
isDisabled = button.isDisabled,
layoutOrder = index,
useDefaultControlState = self.props.useDefaultControlState,
[Roact.Ref] = self.gamepadRefs[index],
NextSelectionUp = index > 1 and self.gamepadRefs[index - 1] or nil,
NextSelectionDown = index < #self.props.items and self.gamepadRefs[index + 1] or nil,
SelectionImageObject = getSelectionCursor(CursorKind.SelectionCell),
})
end)
else
smallRadioButtonCellGroup["smallRadioButtonCell"..button.key] = Roact.createElement(SmallRadioButtonCell, {
key = button.key,
text = button.text,
onActivated = self.props.onActivated,
isSelected = self.props.selectedValue == button.key,
isDisabled = button.isDisabled,
layoutOrder = index,
})
end
end
local gamepadEnabled = (UIBloxConfig.enableExperimentalGamepadSupport and self.props.items and #self.props.items > 0)
return Roact.createElement(gamepadEnabled and RoactGamepad.Focusable.Frame or "Frame", {
defaultChild = gamepadEnabled and self.gamepadRefs[1] or nil,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
NextSelectionDown = self.props.NextSelectionDown,
NextSelectionUp = self.props.NextSelectionUp,
[Roact.Ref] = self.props[Roact.Ref],
}, smallRadioButtonCellGroup)
end
return SmallRadioButtonGroup
@@ -0,0 +1,47 @@
return function()
local SelectionGroup = script.Parent
local Small = SelectionGroup.Parent
local Cell = Small.Parent
local App = Cell.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local SmallRadioButtonGroup = require(script.Parent.SmallRadioButtonGroup)
local ITEMS = {
{ text = "Selection 1", key = "1" },
{ text = "Selection 3", key = "3" },
{ text = "Selection 2", key = "2" },
{ text = "Disabled Cell", key = "4", isDisabled = true }
}
it("should create and destroy SmallRadioButtonGroup without errors", function()
local element = mockStyleComponent({
smallRadioButtonGroup = Roact.createElement(SmallRadioButtonGroup, {
onActivated = function() end,
items = ITEMS,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy SmallRadioButtonGroup without errors with all optional props used", function()
local element = mockStyleComponent({
smallRadioButtonGroup = Roact.createElement(SmallRadioButtonGroup, {
onActivated = function() end,
items = ITEMS,
selectedValue = "1",
layoutOrder = 1,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end

Some files were not shown because too many files have changed in this diff Show More