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,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
@@ -0,0 +1,7 @@
return {
Small = 16,
Regular = 36,
Large = 48,
XLarge = 96,
XXLarge = 192,
}
@@ -0,0 +1,106 @@
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local withStyle = require(UIBlox.Style.withStyle)
local GetTextSize = require(UIBlox.Core.Text.GetTextSize)
local IconSize = require(App.ImageSet.Enum.IconSize)
local getIconSize = require(App.ImageSet.getIconSize)
local Images = require(App.ImageSet.Images)
local Core = UIBlox.Core
local Interactable = require(Core.Control.Interactable)
local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel)
local ImageSetComponent = require(Core.ImageSet.ImageSetComponent)
local MAX_BOUND = 10000
local SEE_ALL_ARROW = Images["icons/navigation/pushRight_small"]
local TEXT_ICON_PADDING = 4
local CarouselHeader = Roact.PureComponent:extend("CarouselHeader")
CarouselHeader.validateProps = t.strictInterface({
-- The header text for the carousel
headerText = t.optional(t.string),
-- The callback for the see all arrow. if nil, the arrow won't be shown
onSeeAll = t.optional(t.callback),
-- The carousel left margin
carouselMargin = t.optional(t.number),
-- The layout order
layoutOrder = t.optional(t.number),
})
CarouselHeader.defaultProps = {
headerText = "",
carouselMargin = 0,
}
function CarouselHeader:render()
local headerText = self.props.headerText
local onSeeAll = self.props.onSeeAll
local layoutOrder = self.props.layoutOrder
local carouselMargin = self.props.carouselMargin
return withStyle(function(style)
local fontStyle = style.Font.Header1
local baseSize = style.Font.BaseSize
local fontSize = fontStyle.RelativeSize * baseSize
local textFont = fontStyle.Font
local textboxBounds = GetTextSize(headerText, fontSize, textFont, Vector2.new(MAX_BOUND, MAX_BOUND))
local textboxSize = UDim2.fromOffset(textboxBounds.X + TEXT_ICON_PADDING + getIconSize(IconSize.Small),
textboxBounds.Y)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, textboxBounds.Y),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
CarouselHeaderButton = Roact.createElement(Interactable, {
Position = UDim2.fromOffset(carouselMargin, 0),
Size = textboxSize,
AutoButtonColor = false,
BackgroundTransparency = 1,
[Roact.Event.Activated] = onSeeAll,
--Note State change is not being used right now.
onStateChanged = function()end,
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, TEXT_ICON_PADDING),
}),
HeaderText = Roact.createElement(GenericTextLabel, {
Text = headerText,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
LayoutOrder = 1,
fontStyle = fontStyle,
colorStyle = style.Theme.TextEmphasis,
}),
SeeAllArrow = onSeeAll and Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.fromOffset(getIconSize(IconSize.Small), getIconSize(IconSize.Small)),
BackgroundTransparency = 1,
Image = SEE_ALL_ARROW,
ImageColor3 = style.Theme.TextEmphasis.Color,
ImageTransparency = style.Theme.TextEmphasis.Transparency,
LayoutOrder = 2,
}) or nil,
})
})
end)
end
return CarouselHeader
@@ -0,0 +1,40 @@
return function()
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local CarouselHeader = require(script.Parent.CarouselHeader)
describe("should create and destroy CarouselHeader with default props without errors", function()
it("should mount and unmount without issue", function()
local element = mockStyleComponent({
Item = Roact.createElement(CarouselHeader)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
describe("should create and destroy CarouselHeader without errors", function()
it("should mount and unmount without issue", function()
local element = mockStyleComponent({
Item = Roact.createElement(CarouselHeader, {
headerText = "test header",
onSeeAll = function() end,
carouselMargin = 12,
layoutOrder = 1,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,96 @@
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local FitFrame = require(Packages.FitFrame)
local FitFrameOnAxis = FitFrame.FitFrameOnAxis
local CarouselHeader = require(Carousel.CarouselHeader)
local HorizontalCarousel = require(Carousel.HorizontalCarousel)
local DEFAULT_INNER_PADDING = 12
local DEFAULT_ITEM_PADDING = 12
local DEFAULT_MARGIN = 24
local FreeFlowCarousel = Roact.PureComponent:extend("FreeFlowCarousel")
FreeFlowCarousel.validateProps = t.strictInterface({
-- A function to uniquely identify list items. Calling this on the same item twice
-- should give the same result according to ==.
-- See infinite scroller for more details.
identifier = t.optional(t.callback),
-- The header text for the carousel
headerText = t.optional(t.string),
-- The callback for the see all arrow. if nil, the arrow won't be shown
onSeeAll = t.optional(t.callback),
-- The list for the items in the carousel
itemList = t.array(t.any),
-- A callback function, called with each visible item in the itemList when the list is rendered.
renderItem = t.callback,
-- The size of the item
itemSize = t.optional(t.Vector2),
-- The padding between items
itemPadding = t.optional(t.number),
-- The carousel margin
carouselMargin = t.optional(t.number),
-- The inner padding between the header and the carousel
innerPadding = t.optional(t.number),
-- The layoutOrder
layoutOrder = t.optional(t.integer),
-- A callback function, called when the infinite scroll reaches the leading end of the itemList (index
-- #itemList).
loadNext = t.optional(t.callback),
})
FreeFlowCarousel.defaultProps = {
headerText = "",
innerPadding = DEFAULT_INNER_PADDING,
itemPadding = DEFAULT_ITEM_PADDING,
carouselMargin = DEFAULT_MARGIN,
}
function FreeFlowCarousel:render()
local innerPadding = self.props.innerPadding
local carouselMargin = self.props.carouselMargin
return Roact.createElement(FitFrameOnAxis, {
axis = FitFrameOnAxis.Axis.Vertical,
minimumSize = UDim2.fromScale(1, 0),
LayoutOrder = self.props.layoutOrder,
contentPadding = UDim.new(0, innerPadding),
BackgroundTransparency = 1,
}, {
CarouselHeader = Roact.createElement(CarouselHeader, {
headerText = self.props.headerText,
onSeeAll = self.props.onSeeAll,
carouselMargin = carouselMargin,
layoutOrder = 1,
}),
Carousel = Roact.createElement(HorizontalCarousel, {
identifier = self.props.identifier,
itemList = self.props.itemList,
renderItem = self.props.renderItem,
itemSize = self.props.itemSize,
itemPadding = innerPadding,
carouselMargin = carouselMargin,
layoutOrder = 2,
loadNext = self.props.loadNext,
}),
})
end
return FreeFlowCarousel
@@ -0,0 +1,84 @@
return function()
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local FreeFlowCarousel = require(script.Parent.FreeFlowCarousel)
describe("should create and destroy with required props without errors", function()
it("should mount and unmount without issue", function()
local items = {}
for i=1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
local renderItem = function(props)
return Roact.createElement("TextLabel", props)
end
local element = mockStyleComponent({
Item = Roact.createElement(FreeFlowCarousel, {
itemList = items,
renderItem = renderItem,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
describe("should create and destroy FreeFlowCarousel without errors", function()
it("should mount and unmount without issue", function()
local items = {}
for i=1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
local renderItem = function(props)
return Roact.createElement("TextLabel", props)
end
local loadNext = function()
for i=1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
end
local element = mockStyleComponent({
Item = Roact.createElement(FreeFlowCarousel, {
identifier = function(item)
return tostring(item)
end,
headerText = "test header",
onSeeAll = function()end,
itemList = items,
renderItem = renderItem,
itemSize = Vector2.new(100, 100),
itemPadding = 12,
carouselMargin = 36,
layoutOrder = 1,
loadNext = loadNext,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,269 @@
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.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 ScrollButton = require(Carousel.ScrollButton)
local Core = UIBlox.Core
local Scroller = require(Core.InfiniteScroller).Scroller
local DEFAULT_ITEM_PADDING = 12
local LEFT_ICON = Images["icons/actions/cycleLeft"]
local RIGHT_ICON = Images["icons/actions/cycleRight"]
local MOTOR_OPTIONS = {
frequency = 2,
dampingRatio = 0.9,
restingPositionLimit = 0.5,
restingVelocityLimit = 0.1,
}
local HorizontalCarousel = Roact.PureComponent:extend("HorizontalCarousel")
HorizontalCarousel.validateProps = t.strictInterface({
-- Required. The list of items to scroll through.
itemList = t.array(t.any),
-- A callback function, called with each visible item in the itemList when the list is rendered.
renderItem = t.callback,
-- A function to uniquely identify list items. Calling this on the same item twice
-- should give the same result according to ==.
-- See infinite scroller for more details.
identifier = t.optional(t.callback),
-- The size of the item
itemSize = t.optional(t.Vector2),
-- The padding between items
itemPadding = t.optional(t.number),
-- The carousel margin
carouselMargin = t.optional(t.number),
-- The layoutOrder
layoutOrder = t.optional(t.integer),
-- A callback function, called when the carousel reaches the leading end of the itemList (index
-- #itemList).
loadNext = t.optional(t.callback),
-- A callback function, called when the carousel reaches the trailing end of the itemList (index 1).
loadPrevious = t.optional(t.callback),
-- Animate the scrolling
animateScrolling = t.optional(t.boolean),
})
HorizontalCarousel.defaultProps = {
itemSize = Vector2.new(1, 1),
itemPadding = DEFAULT_ITEM_PADDING,
}
local function updateScrollState(newIndex, numberOfItemsShown, numOfItems, scrollerFocusLock)
if newIndex == nil then
return {}
end
--Disable the buttons because there is nothing to show
if numOfItems == nil or numOfItems == 0 then
return {
showLeftButton = false,
showRightButton = false,
}
end
local targetIndex = newIndex
local showLeftButton = true
local showRightButton = true
if newIndex <= 1 then
-- If scrolling pass the 1st element then reset the target index to the first item
targetIndex = 1
scrollerFocusLock = scrollerFocusLock + 1
showLeftButton = false
elseif newIndex > numOfItems then
-- If scrolling pass the last element then reset the target index to the last item
targetIndex = numOfItems
scrollerFocusLock = scrollerFocusLock + 1
showRightButton = false
elseif newIndex + numberOfItemsShown > numOfItems then
-- There is no more items outside of the carousel then hide the scroll button
-- There is also no need to update the scrollerFocusLock or target in this case
showRightButton = false
end
return {
scrollerFocusLock = scrollerFocusLock,
index = targetIndex,
showLeftButton = showLeftButton,
showRightButton = showRightButton,
numOfItems = numOfItems,
}
end
function HorizontalCarousel.getDerivedStateFromProps(nextProps, lastState)
local numOfItems = #nextProps.itemList
if lastState.numOfItems ~= numOfItems then
return updateScrollState(lastState.index, lastState.numberOfItemsShown, numOfItems, lastState.scrollerFocusLock)
end
return nil
end
function HorizontalCarousel:init()
self.frameRef = Roact.createRef()
local carouselMetaData = {}
self:setState({
scrollerFocusLock = 0,
index = 1,
hovering = false,
showLeftButton = false,
showRightButton = false,
numberOfItemsShown = 0,
numOfItems = 0,
})
self.onMouseEnter = function(gui, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
local anchorIndex = carouselMetaData.anchorIndex
local newState = updateScrollState(anchorIndex,
self.state.numberOfItemsShown,
self.state.numOfItems,
self.state.scrollerFocusLock)
newState.hovering = true
self:setState(newState)
end
end
self.onMouseLeave = function(gui, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
self:setState({
hovering = false,
})
end
end
self.onResize = function(rbx)
local totalWidth = rbx.AbsoluteSize.X
local numberOfItemsShown = math.floor(totalWidth / (self.props.itemSize.X + self.props.itemPadding))
self:setState({
numberOfItemsShown = numberOfItemsShown,
})
end
self.onScrollUpdate = function(data)
carouselMetaData = data
end
self.scrollLeft = function()
if carouselMetaData.animationActive then
return
end
local newIndex = carouselMetaData.anchorIndex - self.state.numberOfItemsShown
self:setState(
updateScrollState(newIndex, self.state.numberOfItemsShown, self.state.numOfItems, self.state.scrollerFocusLock + 1)
)
end
self.scrollRight = function()
if carouselMetaData.animationActive then
return
end
local newIndex = carouselMetaData.anchorIndex + self.state.numberOfItemsShown
self:setState(
updateScrollState(newIndex, self.state.numberOfItemsShown, self.state.numOfItems, self.state.scrollerFocusLock + 1)
)
end
end
function HorizontalCarousel:render()
local itemList = self.props.itemList
local itemSize = self.props.itemSize
local renderItem = self.props.renderItem
local itemPadding = self.props.itemPadding
local carouselMargin = self.props.carouselMargin
local layoutOrder = self.props.layoutOrder
local loadNext = self.props.loadNext
local loadPrevious = self.props.loadPrevious
local scrollLeftButton
if self.state.hovering and self.state.showLeftButton then
scrollLeftButton = Roact.createElement(ScrollButton, {
icon = LEFT_ICON,
callback = self.scrollLeft,
})
end
local scrollRightButton
if self.state.hovering and self.state.showRightButton then
scrollRightButton = Roact.createElement(ScrollButton, {
icon = RIGHT_ICON,
callback = self.scrollRight,
})
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, itemSize.Y),
LayoutOrder = layoutOrder,
BackgroundTransparency = 1,
[Roact.Ref] = self.frameRef,
[Roact.Event.InputBegan] = self.onMouseEnter,
[Roact.Event.InputEnded] = self.onMouseLeave,
[Roact.Change.AbsoluteSize] = self.onResize,
},{
LeftMargin = Roact.createElement("Frame", {
Position = UDim2.fromScale(0, 0),
AnchorPoint = Vector2.new(0, 0),
Size = UDim2.new(0, carouselMargin, 1, 0),
BackgroundTransparency = 1,
ZIndex = 2,
},{
ScrollLeftButton = scrollLeftButton,
}),
InfiniteScrollerCarousel = self.state.numberOfItemsShown > 0 and Roact.createElement(Scroller, {
identifier = self.props.identifier,
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromOffset(carouselMargin, 0),
ScrollBarThickness = 0,
ClipsDescendants = false,
padding = UDim.new(0, itemPadding),
orientation = Scroller.Orientation.Right,
itemList = itemList,
loadingBuffer = 1,
mountingBuffer = self.state.numberOfItemsShown * 3 * itemSize.X,
loadNext = loadNext,
loadPrevious = loadPrevious,
focusLock = self.state.scrollerFocusLock,
focusIndex = self.state.index,
anchorLocation = UDim.new(1, 0),
estimatedItemSize = itemSize.X,
renderItem = renderItem,
onScrollUpdate = self.onScrollUpdate,
animateScrolling = true,
animateOptions = MOTOR_OPTIONS,
}) or nil,
RightMargin = Roact.createElement("Frame", {
Position = UDim2.fromScale(1, 0),
AnchorPoint = Vector2.new(1, 0),
Size = UDim2.new(0, carouselMargin, 1, 0),
BackgroundTransparency = 1,
ZIndex = 2,
},{
ScrollRightButton = scrollRightButton,
}),
})
end
return HorizontalCarousel
@@ -0,0 +1,93 @@
return function()
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local HorizontalCarousel = require(script.Parent.HorizontalCarousel)
describe("should create and destroy with required props without errors", function()
it("should mount and unmount without issue", function()
local items = {}
for i=1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
local renderItem = function(props)
return Roact.createElement("TextLabel", props)
end
local element = mockStyleComponent({
Item = Roact.createElement(HorizontalCarousel, {
itemList = items,
renderItem = renderItem,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
describe("should create and destroy HorizontalCarousel without errors", function()
it("should mount and unmount without issue", function()
local items = {}
for i=1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
local renderItem = function(props)
return Roact.createElement("TextLabel", props)
end
local loadNext = function()
for i=1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
end
local loadPrevious = function()
items = {}
for i=1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
end
local element = mockStyleComponent({
Item = Roact.createElement(HorizontalCarousel, {
identifier = function(item)
return tostring(item)
end,
itemList = items,
renderItem = renderItem,
itemSize = Vector2.new(100, 100),
itemPadding = 12,
carouselMargin = 36,
layoutOrder = 1,
loadNext = loadNext,
loadPrevious = loadPrevious,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,53 @@
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local withStyle = require(UIBlox.Style.withStyle)
local Core = UIBlox.Core
local getIconSize = require(App.ImageSet.getIconSize)
local IconSize = require(App.ImageSet.Enum.IconSize)
local Interactable = require(Core.Control.Interactable)
local ImageSetComponent = require(Core.ImageSet.ImageSetComponent)
local ScrollButton = Roact.PureComponent:extend("ScrollButton")
ScrollButton.validateProps = t.strictInterface({
-- The icon of the button
icon = t.table,
-- Callback action
callback = t.callback,
})
function ScrollButton:render()
return withStyle(function(style)
return Roact.createElement(Interactable, {
AutoButtonColor = false,
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = style.Theme.BackgroundUIContrast.Color,
BackgroundTransparency = style.Theme.BackgroundUIContrast.Transparency,
BorderSizePixel = 0,
[Roact.Event.Activated] = self.props.callback,
--Note State change is not being used right now.
onStateChanged = function()end,
}, {
Icon = Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.fromOffset(getIconSize(IconSize.Medium), getIconSize(IconSize.Medium)),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = self.props.icon,
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = style.Theme.IconEmphasis.Transparency,
}),
})
end)
end
return ScrollButton
@@ -0,0 +1,29 @@
return function()
local Carousel = script.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local Images = require(App.ImageSet.Images)
local ScrollButton = require(script.Parent.ScrollButton)
local icon = Images["icons/actions/cycleLeft"]
it("should create and destroy ScrollButton without errors", function()
it("should mount and unmount without issue", function()
local element = mockStyleComponent({
Item = Roact.createElement(ScrollButton, {
icon = icon,
callback = function()end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,64 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView)
local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem)
local Carousel = script.Parent.Parent
local Container = Carousel.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local FreeFlowCarousel = require(Carousel.FreeFlowCarousel)
return function(target)
local items = {}
for i = 1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
local renderItem = function(props)
return Roact.createElement("TextLabel", props)
end
local loadNext = function()
for i = 1, 10 do
table.insert(items, {
Text = i,
Size = UDim2.fromOffset(100, 100),
})
end
end
local element = Roact.createElement(StoryItem, {
size = UDim2.fromScale(1, 1),
title = "Carousel",
subTitle = "App.Container.Carousel.FreeFlowCarousel",
}, {
Roact.createElement(FreeFlowCarousel, {
identifier = function(item)
return tostring(item)
end,
headerText = "test header",
onSeeAll = function()end,
itemList = items,
renderItem = renderItem,
itemSize = Vector2.new(100, 100),
itemPadding = 12,
carouselMargin = 36,
layoutOrder = 1,
loadNext = loadNext,
})
})
local handle = Roact.mount(Roact.createElement(StoryView, {}, {
Story = element
}), target, "Carousel")
return function()
Roact.unmount(handle)
end
end
@@ -0,0 +1,61 @@
local Indicator = script.Parent
local App = Indicator.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Images = require(UIBlox.App.ImageSet.Images)
local EmptyState = require(UIBlox.App.Indicator.EmptyState)
local SecondaryButton = require(UIBlox.App.Button.SecondaryButton)
local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle)
local RETRY_BUTTON_HEIGHT = 44
local RETRY_BUTTON_WIDTH = 44
local RETRY_BACKGROUND_IMAGE = "icons/common/refresh"
local ICON = 'icons/status/noconnection_large'
local FailedStatePage = Roact.PureComponent:extend("FailedStatePage")
FailedStatePage.validateProps = t.strictInterface({
-- The onRetry function is called when a button is pressed
onRetry = t.optional(t.callback),
-- The renderOnFailed renders a page from a RenderOnFailedStyle enum
renderOnFailed = t.optional(RenderOnFailedStyle.isEnumValue),
-- text for emptystate
text = t.string,
})
FailedStatePage.defaultProps = {
renderOnFailed = RenderOnFailedStyle.RetryButton,
}
function FailedStatePage:render()
if self.props.renderOnFailed == RenderOnFailedStyle.EmptyStatePage then
return Roact.createElement(EmptyState, {
position = UDim2.fromScale(0.5, 0.5),
anchorPoint = Vector2.new(0.5, 0.5),
onActivated = self.props.onRetry,
icon = Images[ICON],
text = self.props.text,
})
elseif self.props.renderOnFailed == RenderOnFailedStyle.RetryButton then
if self.props.onRetry then
return Roact.createElement(SecondaryButton, {
size = UDim2.fromOffset(RETRY_BUTTON_HEIGHT, RETRY_BUTTON_WIDTH),
position = UDim2.fromScale(0.5, 0.5),
anchorPoint = Vector2.new(0.5, 0.5),
onActivated = self.props.onRetry,
icon = Images[RETRY_BACKGROUND_IMAGE],
})
else
error("OnRetry callback empty. OnRetry needs to be a function to render the RetryButton")
end
else
error("Failed to provide proper RenderOnFailedStyle")
end
end
return FailedStatePage
@@ -0,0 +1,36 @@
return function()
local Container = script.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local FailedStatePage = require(script.Parent.FailedStatePage)
local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent)
local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle)
describe("lifecycle", function()
local frame = Instance.new("Frame")
it("should mount and unmount with the optional props", function()
local element = mockStyleComponent({
failedStatePage = Roact.createElement(FailedStatePage, {
onRetry = function() end,
renderOnFailed = RenderOnFailedStyle.RetryButton,
text = "Failed"
})
})
local instance = Roact.mount(element, frame, "FailedStatePage")
Roact.unmount(instance)
end)
it("should error when RetryButton passed without onRetry callback", function()
local element = mockStyleComponent({
failedStatePage = Roact.createElement(FailedStatePage, {
renderOnFailed = RenderOnFailedStyle.RetryButton,
text = "Failed"
})
})
expect(function()
return Roact.mount(element, frame, "FailedStatePage")
end).to.throw()
end)
end)
end
@@ -0,0 +1,110 @@
local Container = script.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus)
local LoadingStateEnum = require(UIBlox.App.Loading.Enum.LoadingState)
local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle)
local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle)
local LoadingStatePage = require(UIBlox.App.Container.LoadingStatePage)
local FailedStatePage = require(UIBlox.App.Container.FailedStatePage)
local LoadingStateTables = require(UIBlox.App.Container.LoadingStateTables)
local INITIAL_RELOADING_STYLE = ReloadingStyle.AllowReload
local INITIAL_LOADING_STATE = LoadingStateEnum.Loading
local LoadingStateContainer = Roact.PureComponent:extend("LoadingStateContainer")
LoadingStateContainer.validateProps = t.strictInterface({
-- The dataStatus is a retrieval status enum
dataStatus = RetrievalStatus.isEnumValue,
-- Text for FailedStatePage EmptyState
failedText = t.string,
-- The onRetry function is called when a button is pressed
onRetry = t.optional(t.callback),
-- The renderOnLoaded is rendered if loading state is LoadingStateEnum.Loaded
renderOnLoaded = t.callback,
-- The renderOnFailed is rendered if loading state is LoadingStateEnum.Failed or is a RenderOnFailedStyle Enum.
renderOnFailed = t.optional(t.union(RenderOnFailedStyle.isEnumValue, t.callback)),
-- The renderOnLoading is rendered if loading state is LoadingStateEnum.Loading
renderOnLoading = t.optional(t.callback),
-- The reloadingStyle is the style of state table
reloadingStyle = t.optional(ReloadingStyle.isEnumValue),
})
LoadingStateContainer.defaultProps = {
renderOnFailed = RenderOnFailedStyle.RetryButton,
reloadingStyle = INITIAL_RELOADING_STYLE,
}
function LoadingStateContainer:init()
self.onStateChange = function(oldState, newState)
self:setState({
loadingState = newState,
})
end
self.updateState = function()
self.state.currentReloadingStyle:onStateChange(self.onStateChange)
self.state.currentReloadingStyle.events[self.props.dataStatus]()
end
self:setState({
loadingState = INITIAL_LOADING_STATE,
currentReloadingStyle = LoadingStateTables[INITIAL_RELOADING_STYLE](),
})
self.statePages = {
[LoadingStateEnum.Loading] = function()
if self.props.renderOnLoading then
return self.props.renderOnLoading()
else
return Roact.createElement(LoadingStatePage)
end
end,
[LoadingStateEnum.Failed] = function()
if t.callback(self.props.renderOnFailed) then
return self.props.renderOnFailed()
else
return Roact.createElement(FailedStatePage, {
onRetry = self.props.onRetry,
renderOnFailed = self.props.renderOnFailed,
text = self.props.failedText,
})
end
end,
[LoadingStateEnum.Loaded] = function()
return self.props.renderOnLoaded()
end,
}
end
function LoadingStateContainer.getDerivedStateFromProps(nextProps, lastState)
if lastState.currentReloadingStyle ~= nil and lastState.currentReloadingStyle ~= nextProps.reloadingStyle then
return {
--reloadingStyle = nextProps.reloadingStyle,
currentReloadingStyle = LoadingStateTables[nextProps.reloadingStyle]()
}
end
end
function LoadingStateContainer:render()
return self.statePages[self.state.loadingState]()
end
function LoadingStateContainer:didMount()
self.updateState()
end
function LoadingStateContainer:didUpdate(prevProps)
if prevProps.dataStatus ~= self.props.dataStatus or prevProps.reloadingStyle ~= self.props.reloadingStyle then
self.updateState()
end
end
return LoadingStateContainer
@@ -0,0 +1,99 @@
return function()
local Container = script.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus)
local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle)
local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle)
local LoadingStateContainer = require(script.Parent.LoadingStateContainer)
local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent)
describe("lifecycle", function()
local frame = Instance.new("Frame")
it("should mount and unmount with only required props", function()
local element = mockStyleComponent({
loadingStateContainer = Roact.createElement(LoadingStateContainer, {
renderOnLoaded = function() end,
dataStatus = RetrievalStatus.NotStarted,
failedText = "failed",
})
})
local instance = Roact.mount(element, frame, "LoadingStateContainer")
Roact.unmount(instance)
end)
it("should mount and unmount with all props", function()
local element = mockStyleComponent({
loadingStateContainer = Roact.createElement(LoadingStateContainer, {
dataStatus = RetrievalStatus.NotStarted,
onRetry = function() end,
renderOnLoaded = function() end,
renderOnFailed = function() end,
renderOnLoading = function() end,
reloadingStyle = ReloadingStyle.AllowReload,
failedText = "failed",
})
})
local instance = Roact.mount(element, frame, "LoadingStateContainer")
Roact.unmount(instance)
end)
it("should render loading state page", function()
local element = mockStyleComponent({
loadingStateContainer = Roact.createElement(LoadingStateContainer, {
dataStatus = RetrievalStatus.Fetching,
onRetry = function() end,
renderOnLoaded = function() end,
renderOnFailed = RenderOnFailedStyle.EmptyStatePage,
reloadingStyle = ReloadingStyle.AllowReload,
failedText = "failed",
})
})
local instance = Roact.mount(element, frame, "LoadingStateContainer")
local loadingIcon = frame:FindFirstChild("inner", true)
expect(loadingIcon).to.be.ok()
Roact.unmount(instance)
end)
it("should render failed state page emptyState", function()
local element = mockStyleComponent({
loadingStateContainer = Roact.createElement(LoadingStateContainer, {
dataStatus = RetrievalStatus.Failed,
onRetry = function() end,
renderOnLoaded = function() end,
renderOnFailed = RenderOnFailedStyle.EmptyStatePage,
reloadingStyle = ReloadingStyle.AllowReload,
failedText = "failed",
})
})
local instance = Roact.mount(element, frame, "LoadingStateContainer")
local content = frame:FindFirstChild("Content", true)
local buttonFrame = content:FindFirstChild("buttonFrame", true)
expect(buttonFrame).to.be.ok()
Roact.unmount(instance)
end)
it("should render failed state page RetryButton", function()
local element = mockStyleComponent({
loadingStateContainer = Roact.createElement(LoadingStateContainer, {
dataStatus = RetrievalStatus.Failed,
onRetry = function() end,
renderOnLoaded = function() end,
renderOnFailed = RenderOnFailedStyle.RetryButton,
reloadingStyle = ReloadingStyle.AllowReload,
failedText = "failed",
})
})
local instance = Roact.mount(element, frame, "LoadingStateContainer")
local ButtonContent = frame:FindFirstChild("ButtonContent", true)
expect(ButtonContent).to.be.ok()
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,20 @@
local Indicator = script.Parent
local App = Indicator.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local LoadingSpinner = require(UIBlox.App.Loading.LoadingSpinner)
local LoadingStatePage = Roact.PureComponent:extend("LoadingStatePage")
function LoadingStatePage:render()
return Roact.createElement(LoadingSpinner, {
size = UDim2.fromOffset(42, 42),
position = UDim2.fromScale(0.5, 0.5),
anchorPoint = Vector2.new(0.5, 0.5),
})
end
return LoadingStatePage
@@ -0,0 +1,21 @@
return function()
local Container = script.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local LoadingStatePage = require(script.Parent.LoadingStatePage)
local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent)
describe("lifecycle", function()
local frame = Instance.new("Frame")
it("should mount and unmount", function()
local element = mockStyleComponent({
loadingStatePage = Roact.createElement(LoadingStatePage)
})
local instance = Roact.mount(element, frame, "LoadingStatePage")
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,62 @@
local Container = script.Parent
local App = Container.Parent
local UIBlox = App.Parent
local StateTable = require(UIBlox.StateTable.StateTable)
local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle)
local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus)
local LoadingStateEnum = require(UIBlox.App.Loading.Enum.LoadingState)
local INITIAL_LOADING_STATE = LoadingStateEnum.Loading
local LoadingStateTables = {}
-- This state table allows reloading after it has loaded
LoadingStateTables[ReloadingStyle.AllowReload] = function()
return StateTable.new('AllowReload', INITIAL_LOADING_STATE, {}, {
[LoadingStateEnum.Loading] = {
[RetrievalStatus.NotStarted] = {},
[RetrievalStatus.Fetching] = {},
[RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded },
[RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed },
},
[LoadingStateEnum.Loaded] = {
[RetrievalStatus.NotStarted] = {},
[RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading },
[RetrievalStatus.Done] = {},
[RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed },
},
[LoadingStateEnum.Failed] = {
[RetrievalStatus.NotStarted] = {},
[RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading },
[RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded },
[RetrievalStatus.Failed] = {},
},
})
end
-- This state table locks reloading after it has loaded.
LoadingStateTables[ReloadingStyle.LockReload] = function()
return StateTable.new('LockReload', INITIAL_LOADING_STATE, {}, {
[LoadingStateEnum.Loading] = {
[RetrievalStatus.NotStarted] = {},
[RetrievalStatus.Fetching] = {},
[RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded },
[RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed },
},
[LoadingStateEnum.Loaded] = {
[RetrievalStatus.NotStarted] = {},
[RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Failed },
[RetrievalStatus.Done] = {},
[RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed },
},
[LoadingStateEnum.Failed] = {
[RetrievalStatus.NotStarted] = {},
[RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading },
[RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded },
[RetrievalStatus.Failed] = {},
},
})
end
return LoadingStateTables
@@ -0,0 +1,176 @@
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local App = script.Parent.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Cryo = require(Packages.Cryo)
local Otter = require(Packages.Otter)
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local withStyle = require(Packages.UIBlox.Core.Style.withStyle)
local PADDING_HORIZONTAL = 24
local SCROLL_BAR_RIGHT_PADDING = 4
local MOUSE_SCROLL_BAR_THICKNESS = 8
local TOUCH_OR_CONTROLLER_SCROLL_BAR_THICKNESS = 2
local CANVAS_SIZE_X = UDim.new(0, 0)
local HIDE_SIDEBAR_AFTER_IN_SECONDS = 0.70
local SPRING_PARAMETERS = {
frequency = 3,
dampingRatio = 1.5,
}
local VerticalScrollView = Roact.Component:extend()
VerticalScrollView.defaultProps = {
-- Frame Props
size = UDim2.new(1, 0, 1, 0),
-- ScrollingFrame Props
canvasSizeY = UDim.new(2, 0),
paddingHorizontal = PADDING_HORIZONTAL,
}
VerticalScrollView.validateProps = t.strictInterface({
-- Frame Props
size = t.optional(t.UDim2),
position = t.optional(t.UDim2),
elasticBehavior = t.optional(t.EnumItem),
-- ScrollingFrame Props
canvasSizeY = t.optional(t.UDim),
paddingHorizontal = t.optional(t.numberMin(PADDING_HORIZONTAL/2)),
-- Optional passthrough props for the scrolling frame
[Roact.Change.CanvasPosition] = t.optional(t.callback),
[Roact.Change.CanvasSize] = t.optional(t.callback),
[Roact.Ref] = t.optional(t.table),
-- Children
[Roact.Children] = t.optional(t.table)
})
function VerticalScrollView:init()
self:setState({
scrollBarThickness = 0,
scrollingWithTouch = false,
})
self.scrollBarImageTransparency, self.updateScrollBarImageTransparency = Roact.createBinding(0)
self.scrollBarImageTransparencyMotor = Otter.createSingleMotor(0)
self.scrollBarImageTransparencyMotor:onStep(self.updateScrollBarImageTransparency)
self.lastTimeCanvasPositionChanged = tick()
self.waitToHideSidebarConnection = nil
self.waitToHideSidebar = function()
local currentTime = tick()
local delta = currentTime - self.lastTimeCanvasPositionChanged
if delta > HIDE_SIDEBAR_AFTER_IN_SECONDS then
self.scrollBarImageTransparencyMotor:setGoal(Otter.spring(1, SPRING_PARAMETERS))
self.disconnectWaitToHideSidebar()
end
end
self.disconnectWaitToHideSidebar = function()
if self.waitToHideSidebarConnection then
self.waitToHideSidebarConnection:Disconnect()
self.waitToHideSidebarConnection = nil
end
end
self.inputBegan = function(instance, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
self.disconnectWaitToHideSidebar()
self:setState({
scrollBarThickness = MOUSE_SCROLL_BAR_THICKNESS,
})
self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(0))
end
end
self.inputEnded = function(instance, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
self.disconnectWaitToHideSidebar()
self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(1))
end
end
self.canvasPosition = function(rbx)
self.lastTimeCanvasPositionChanged = tick()
if not self.waitToHideSidebarConnection and
UserInputService:GetLastInputType() == Enum.UserInputType.Touch
then
self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(0))
self:setState({
scrollBarThickness = TOUCH_OR_CONTROLLER_SCROLL_BAR_THICKNESS,
})
self.waitToHideSidebarConnection = RunService.Heartbeat:Connect(self.waitToHideSidebar)
end
if self.props[Roact.Change.CanvasPosition] then
self.props[Roact.Change.CanvasPosition](rbx)
end
end
end
function VerticalScrollView:render()
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
local canvasSizeY = self.props.canvasSizeY
local children = self.props[Roact.Children] or {}
local position = self.props.position
local size = self.props.size
local paddingHorizontal = self.props.paddingHorizontal
local scrollBarThickness = self.state.scrollBarThickness
local scrollingFrameChildren = Cryo.Dictionary.join({
scrollingFrameInnerMargin = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0,paddingHorizontal),
PaddingRight = UDim.new(0, paddingHorizontal - SCROLL_BAR_RIGHT_PADDING), }),
},
children
)
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Position = position,
Size = size,
}, {
scrollingFrameOuterMargins = Roact.createElement("UIPadding", {
PaddingRight = UDim.new(0, SCROLL_BAR_RIGHT_PADDING),
}),
scrollingFrame = Roact.createElement("ScrollingFrame", {
Active = true,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
ElasticBehavior = self.props.elasticBehavior,
-- ScrollingFrame Specific
CanvasSize = UDim2.new(CANVAS_SIZE_X, canvasSizeY),
ScrollBarImageColor3 = theme.UIEmphasis.Color,
ScrollBarImageTransparency = self.scrollBarImageTransparency,
ScrollBarThickness = scrollBarThickness,
ScrollingDirection = Enum.ScrollingDirection.Y,
-- https://jira.rbx.com/browse/MOBLUAPP-2451
-- TODO: 1.) Currently code assumes that Mouse is on desktop and touch is on mobile
-- On a mac touch pad is reported as mouse not as touch
-- No sure how many users use mouse on a phone
-- TODO: 2.) how to handle controller actions - when we do this,
-- we should make this part of the code platform specific
[Roact.Event.InputBegan] = self.inputBegan,
[Roact.Event.InputEnded] = self.inputEnded,
[Roact.Change.CanvasPosition] = self.canvasPosition,
[Roact.Change.CanvasSize] = self.props[Roact.Change.CanvasSize],
[Roact.Ref] = self.props[Roact.Ref],
}, scrollingFrameChildren)
})
end)
end
function VerticalScrollView:willUnmount()
self.disconnectWaitToHideSidebar()
end
return VerticalScrollView
@@ -0,0 +1,57 @@
return function()
local Container = script.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local VerticalScrollView = require(Container.VerticalScrollView)
describe("mount/unmount", function()
it("should mount and unmount with default properties", function()
local verticalScrollViewWithStyle = mockStyleComponent({
verticalScrollView = Roact.createElement(VerticalScrollView)
})
local handle = Roact.mount(verticalScrollViewWithStyle)
expect(handle).to.be.ok()
Roact.unmount(handle)
end)
it("should mount and unmount with valid properties", function()
local verticalScrollViewWithStyle = mockStyleComponent({
verticalScrollView = Roact.createElement(VerticalScrollView, {
position = UDim2.new(0, 50, 0,100),
size = UDim2.new(1, 30, 1, 50),
canvasSizeY = UDim.new(2, 0),
paddingHorizontal = 12,
elasticBehavior = Enum.ElasticBehavior.Always,
[Roact.Change.CanvasPosition] = function()end,
[Roact.Change.CanvasSize] = function()end,
[Roact.Ref] = Roact.createRef(),
})
})
local handle = Roact.mount(verticalScrollViewWithStyle)
expect(handle).to.be.ok()
Roact.unmount(handle)
end)
-- skipping this until https://jira.rbx.com/browse/MOBLUAPP-2424 is merged to CI
itSKIP("mount should throw when created with invalid properties", function()
local function expectToThrowForInvalidProps(props)
local verticalScrollViewWithStyle = mockStyleComponent({
verticalScrollView = Roact.createElement(VerticalScrollView, props)
})
expect(function()
Roact.mount(verticalScrollViewWithStyle)
end).to.throw()
end
expectToThrowForInvalidProps({ position = 3 })
expectToThrowForInvalidProps({ size = 3 })
expectToThrowForInvalidProps({ canvasSizeY = 3 })
expectToThrowForInvalidProps({ paddingHorizontal = 3 })
expectToThrowForInvalidProps({ NotInTheInterface = "Really it is not there" })
end)
end)
end
@@ -0,0 +1,17 @@
local Container = script.Parent
local App = Container.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local t = require(Packages.t)
return function(containerWidth)
assert(t.number(containerWidth))
if containerWidth <= 360 then
return 12
elseif containerWidth > 360 and containerWidth <= 599 then
return 24
else
return 48
end
end
@@ -0,0 +1,22 @@
return function()
local getPageMargin = require(script.Parent.getPageMargin)
describe("getPageMargin()", function()
it("should return the number 12", function()
local pageMargin = getPageMargin(312)
expect(pageMargin).to.equal(12)
end)
it("should return the number 24", function()
local pageMargin = getPageMargin(400)
expect(pageMargin).to.equal(24)
end)
it("should return the number 48", function()
local pageMargin = getPageMargin(700)
expect(pageMargin).to.equal(48)
end)
it("it should error", function()
expect(function()
return getPageMargin("String")
end).to.throw()
end)
end)
end
@@ -0,0 +1,6 @@
local ContentProvider = game:GetService("ContentProvider")
local Packages = script.Parent.Parent.Parent.Parent
local Roact = require(Packages.Roact)
return Roact.createContext(ContentProvider)
@@ -0,0 +1,191 @@
local Knob = script.Parent
local Control = Knob.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local RoactGamepad = require(Packages.RoactGamepad)
local Images = require(App.ImageSet.Images)
local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent)
local Interactable = require(UIBlox.Core.Control.Interactable)
local ControlState = require(UIBlox.Core.Control.Enum.ControlState)
local withStyle = require(UIBlox.Core.Style.withStyle)
local validateColor = require(UIBlox.Core.Style.Validator.validateColorInfo)
local BaseKnob = Roact.Component:extend("BaseKnob")
-- local validateColor = t.interface({
-- Color = t.Color3,
-- Transparency = t.number,
-- })
local ShadowColorMap = {
[ControlState.Default] = {
Color = Color3.fromRGB(0,0,0),
Transparency = 0.7
},
[ControlState.Hover] = {
Color = Color3.fromRGB(0,0,0),
Transparency = 0.5
}
}
local ShadowSizeMap = {
[ControlState.Default] = UDim2.fromOffset(48, 48),
[ControlState.Hover] = UDim2.fromOffset(48, 48),
[ControlState.Pressed] = UDim2.fromOffset(0, 0),
[ControlState.Disabled] = UDim2.fromOffset(0, 0),
[ControlState.Selected] = UDim2.fromOffset(52, 52),
[ControlState.SelectedPressed] = UDim2.fromOffset(42, 42),
}
local ShadowImageMap = {
[ControlState.Default] = "component_assets/dropshadow_28",
[ControlState.Hover] = "component_assets/dropshadow_28",
[ControlState.Selected] = "component_assets/circle_52_stroke_3",
[ControlState.SelectedPressed] = "component_assets/circle_42_stroke_3",
}
BaseKnob.validateProps = t.interface({
-- The state change callback for the button
onStateChanged = t.optional(t.callback),
-- Is the button visually disabled
isDisabled = 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),
colorMap = t.strictInterface({
[ControlState.Default] = validateColor,
[ControlState.Hover] = validateColor,
[ControlState.Pressed] = validateColor,
[ControlState.Disabled] = validateColor,
[ControlState.Selected] = validateColor,
[ControlState.SelectedPressed] = validateColor,
}),
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),
})
BaseKnob.defaultProps = {
anchorPoint = Vector2.new(0.5, 0.5),
userInteractionEnabled = true,
isDisabled = false
}
function BaseKnob: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 BaseKnob:render()
return withStyle(function(style)
local color = self.props.colorMap[self.state.controlState] or self.props.colorMap[ControlState.Default]
local shadowSize = ShadowSizeMap[self.state.controlState] or ShadowSizeMap[ControlState.Default]
local shadowImage = ShadowImageMap[self.state.controlState] or ShadowImageMap[ControlState.Default]
local isGamepadSelected = self.state.controlState == ControlState.Selected or
self.state.controlState == ControlState.SelectedPressed
local shadowColor = ShadowColorMap[self.state.controlState] or ShadowColorMap[ControlState.Default]
if isGamepadSelected then
shadowColor = style.Theme.SelectionCursor
end
if UIBloxConfig.enableExperimentalGamepadSupport then
return Roact.createElement("Frame",{
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
Position = self.props.position,
BackgroundTransparency = 1,
Size = UDim2.fromOffset(28, 28)
}, {
KnobShadow = Roact.createElement(ImageSetComponent.Label, {
Size = shadowSize,
Position = UDim2.new(0.5, 0, 0.5, not isGamepadSelected and 2 or 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Image = Images[shadowImage],
ImageColor3 = shadowColor.Color,
ImageTransparency = shadowColor.Transparency,
Active = true,
BackgroundTransparency = 1,
}),
KnobButton = Roact.createElement(RoactGamepad.Focusable[Interactable], {
Size = UDim2.fromScale(1,1),
isDisabled = self.props.isDisabled,
onStateChanged = self.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
BackgroundTransparency = 1,
Image = Images["component_assets/circle_29"],
ImageColor3 = color.Color,
ImageTransparency = color.Transparency,
[Roact.Ref] = self.props[Roact.Ref],
[Roact.Event.Activated] = self.props.onActivated,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
}),
})
else
return Roact.createElement("Frame",{
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
Position = self.props.position,
BackgroundTransparency = 1,
Size = UDim2.fromOffset(28, 28)
}, {
KnobShadow = Roact.createElement(ImageSetComponent.Label, {
Size = shadowSize,
Position = UDim2.new(0.5, 0, 0.5, not isGamepadSelected and 2 or 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Image = Images[shadowImage],
ImageColor3 = shadowColor.Color,
ImageTransparency = shadowColor.Transparency,
BackgroundTransparency = 1,
}),
KnobButton = Roact.createElement(Interactable, {
Size = UDim2.fromScale(1,1),
isDisabled = self.props.isDisabled,
onStateChanged = self.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
BackgroundTransparency = 1,
Image = Images["component_assets/circle_29"],
ImageColor3 = color.Color,
ImageTransparency = color.Transparency,
[Roact.Event.Activated] = self.props.onActivated,
}),
})
end
end)
end
return BaseKnob
@@ -0,0 +1,82 @@
local Knob = script.Parent
local Control = Knob.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Cryo = require(Packages.Cryo)
local BaseKnob = require(Knob.BaseKnob)
local ControlState = require(UIBlox.Core.Control.Enum.ControlState)
local Colors = require(App.Style.Colors)
local colorMap = {
contextual = {
[ControlState.Default] = {
Color = Colors.White,
Transparency = 0,
},
[ControlState.Hover] = {
Color = Colors.White,
Transparency = 0,
},
[ControlState.Pressed] = {
Color = Colors.Green,
Transparency = 0,
},
[ControlState.Disabled] = {
Color = Colors.Pumice,
Transparency = 0,
},
[ControlState.Selected] = {
Color = Colors.White,
Transparency = 0,
},
[ControlState.SelectedPressed] = {
Color = Colors.White,
Transparency = 0,
},
},
system = {
[ControlState.Default] = {
Color = Colors.White,
Transparency = 0,
},
[ControlState.Hover] = {
Color = Colors.White,
Transparency = 0,
},
[ControlState.Pressed] = {
Color = Colors.Pumice,
Transparency = 0,
},
[ControlState.Disabled] = {
Color = Colors.Pumice,
Transparency = 0,
},
[ControlState.Selected] = {
Color = Colors.White,
Transparency = 0,
},
[ControlState.SelectedPressed] = {
Color = Colors.White,
Transparency = 0,
},
}
}
local function buildKnob(styleName)
return function(props)
local currentColorMap = colorMap[styleName]
local newProps = Cryo.Dictionary.join({},props,{
colorMap = currentColorMap
})
return Roact.createElement(BaseKnob,newProps)
end
end
return {
ContextualKnob = buildKnob("contextual"),
SystemKnob = buildKnob("system"),
}
@@ -0,0 +1,30 @@
return function()
local KnobFolder = script.Parent
local Control = KnobFolder.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Roact = require(UIBlox.Parent.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local Knob = require(script.Parent.Knob)
describe("lifecycle", function()
it("should mount and unmount without issue with only required props", function()
local element = mockStyleComponent({
Knob1 = Roact.createElement(Knob.ContextualKnob,{
onActivated = print
}),
Knob2 = Roact.createElement(Knob.SystemKnob,{
onActivated = print
}),
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
Roact.unmount(instance)
folder:Destroy()
end)
end)
end
@@ -0,0 +1,269 @@
local Control = script.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Core = UIBlox.Core
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Interactable = require(Core.Control.Interactable)
local ControlState = require(Core.Control.Enum.ControlState)
local getContentStyle = require(Core.Button.getContentStyle)
local withStyle = require(Core.Style.withStyle)
local ImageSetComponent = require(Core.ImageSet.ImageSetComponent)
local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel)
local Images = require(App.ImageSet.Images)
local IconSize = require(App.ImageSet.Enum.IconSize)
local getIconSize = require(App.ImageSet.getIconSize)
local TooltipContainer = require(App.Dialog.Tooltip.TooltipContainer)
local GetTextSize = require(UIBlox.Core.Text.GetTextSize)
local CONTENT_PADDING = 8
local DEFAULT_BORDER_CIRCLE = Images["component_assets/circle_17_stroke_1"]
local DEFAULT_BACKGROUND_CIRCLE = Images["component_assets/circle_17"]
local DEFAULT_SLICE_CENTER = Rect.new(8, 8, 9, 9)
local SELECTED_BORDER_CIRCLE = Images["component_assets/circle_22_stroke_3"]
local SELECTED_SLICE_CENTER = Rect.new(11, 11, 12, 12)
local DEFAULT_ICON = Images["icons/common/goldrobux_small"]
local DELAY_TIME = 1
local DEFAULT_PADDING = 8
local MIN_WIDTH = 56
local MAX_WIDTH = 108
local TRIGGER_AREA_HEIGHT = 44
local BUTTON_AREA_HEIGHT = 28
local BUTTON_STATE_COLOR_MAP = {
[ControlState.Default] = "Divider",
[ControlState.Hover] = "SystemPrimaryOnHover",
[ControlState.Selected] = "SystemPrimaryOnHover",
}
local TEXT_STATE_COLOR_MAP = {
[ControlState.Default] = "TextEmphasis"
}
local ICON_STATE_COLOR_MAP = {
[ControlState.Default] = "TextEmphasis"
}
local BACKGROUND_STATE_COLOR_MAP = {
[ControlState.Default] = "BackgroundUIDefault"
}
local RobuxBalance = Roact.PureComponent:extend("RobuxBalance")
function RobuxBalance:init()
self.triggerRef = Roact.createRef()
self.toolTipRef = Roact.createRef()
self.isInHover = false
self.tooltipWidth, self.setTooltipWidth = Roact.createBinding(0)
self.state = {
controlState = ControlState.Initialize,
triggerPosition = Vector2.new(0, 0),
triggerSize = Vector2.new(0, 0),
showToolTip = false
}
self.onStateChanged = function(oldState, newState)
self:setState({
controlState = newState,
})
local isFocused = newState == ControlState.Hover or newState == ControlState.Pressed
if self.props.fullText and isFocused then
self.isInHover = true
delay(DELAY_TIME, self.showToolTip)
else
self.isInHover = false
self.hideToolTip()
end
if self.props.onStateChanged then
self.props.onStateChanged(oldState, newState)
end
end
self.showToolTip = function()
if self.isInHover then
self:setState({
showToolTip = true,
})
end
end
self.hideToolTip = function()
self:setState({
showToolTip = false,
})
end
self.setPosition = function(rbx)
self:setState({
triggerPosition = rbx.AbsolutePosition,
})
end
self.setSize = function(rbx)
self:setState({
triggerSize = rbx.AbsoluteSize,
})
end
self.onActivated = function(value)
if self.props.onActivated then
self.props.onActivated(value)
end
end
end
RobuxBalance.validateProps = t.strictInterface({
--The text of the button
displayText = t.optional(t.string),
--The text of the tooltip when hovered over 2 seconds
fullText = t.optional(t.string),
--The position of this component
position = t.optional(t.UDim2),
--The activated callback for the button
onActivated = t.optional(t.callback),
--The state change callback for the button
onStateChanged = t.optional(t.callback),
--The state change callback for the button
tooltipPosition = t.optional(t.UDim2),
--A Boolean value that determines whether user events are ignored and sink input
userInteractionEnabled = t.optional(t.boolean),
})
function RobuxBalance:render()
return withStyle(function(style)
local currentState = self.state.controlState
local showToolTip = self.state.showToolTip
local text = self.props.displayText or "-"
local hasFullText = self.props.fullText
local icon = DEFAULT_ICON
local buttonStyle = getContentStyle(BUTTON_STATE_COLOR_MAP, currentState, style)
local textStyle = getContentStyle(TEXT_STATE_COLOR_MAP, currentState, style)
local iconStyle = getContentStyle(ICON_STATE_COLOR_MAP, currentState, style)
local backgroundStyle = getContentStyle(BACKGROUND_STATE_COLOR_MAP, currentState, style)
local buttonImage = self.state.controlState == ControlState.Selected and
SELECTED_BORDER_CIRCLE or DEFAULT_BORDER_CIRCLE
local sliceCenter = self.state.controlState == ControlState.Selected and
SELECTED_SLICE_CENTER or DEFAULT_SLICE_CENTER
local fontStyle = style.Font.CaptionHeader
local fontSize = fontStyle.RelativeSize * style.Font.BaseSize
local estimatedTextWidth = GetTextSize(
text,
fontSize,
fontStyle.Font,
Vector2.new(1000, 1000)
).X
local buttonWidth = estimatedTextWidth + getIconSize(IconSize.Small) + DEFAULT_PADDING * 3
return Roact.createElement(Interactable, {
Position = self.props.position or UDim2.fromScale(0, 0),
isDisabled = false,
onStateChanged = self.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
BackgroundTransparency = 1,
[Roact.Event.Activated] = self.onActivated,
Size = UDim2.fromOffset(buttonWidth, TRIGGER_AREA_HEIGHT),
AnchorPoint = Vector2.new(1, 0),
[Roact.Ref] = self.triggerRef,
[Roact.Change.AbsolutePosition] = self.setPosition,
[Roact.Change.AbsoluteSize] = self.setSize,
}, {
sizeConstraint = Roact.createElement("UISizeConstraint", {
MinSize = Vector2.new(MIN_WIDTH, TRIGGER_AREA_HEIGHT),
MaxSize = Vector2.new(MAX_WIDTH, TRIGGER_AREA_HEIGHT),
}),
UIPadding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, (TRIGGER_AREA_HEIGHT - BUTTON_AREA_HEIGHT) / 2),
PaddingBottom = UDim.new(0, (TRIGGER_AREA_HEIGHT - BUTTON_AREA_HEIGHT) / 2),
}),
ButtonContent = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, BUTTON_AREA_HEIGHT),
BackgroundTransparency = 1,
}, {
ButtonBackground = Roact.createElement(ImageSetComponent.Label, {
Image = DEFAULT_BACKGROUND_CIRCLE,
ScaleType = Enum.ScaleType.Slice,
ImageColor3 = backgroundStyle.Color,
ImageTransparency = backgroundStyle.Transparency,
BackgroundTransparency = 1,
SliceCenter = DEFAULT_SLICE_CENTER,
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromOffset(0, 0),
}),
ButtonContent = Roact.createElement(ImageSetComponent.Label, {
Image = buttonImage,
ScaleType = Enum.ScaleType.Slice,
ImageColor3 = buttonStyle.Color,
ImageTransparency = buttonStyle.Transparency,
BackgroundTransparency = 1,
SliceCenter = sliceCenter,
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromOffset(0, 0),
}, {
UIListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, CONTENT_PADDING),
}),
Icon = Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.fromOffset(getIconSize(IconSize.Small), getIconSize(IconSize.Small)),
BackgroundTransparency = 1,
Image = icon,
ImageColor3 = iconStyle.Color,
ImageTransparency = iconStyle.Transparency,
LayoutOrder = 1,
}),
Text = Roact.createElement(GenericTextLabel, {
BackgroundTransparency = 1,
Text = text,
fontStyle = fontStyle,
colorStyle = textStyle,
LayoutOrder = 2,
maxSize = Vector2.new(estimatedTextWidth, BUTTON_AREA_HEIGHT),
TextTruncate = Enum.TextTruncate.AtEnd,
TextWrapped = false,
TextXAlignment = Enum.TextXAlignment.Left
}),
}),
}),
TooltipContainer = hasFullText and Roact.createElement("Frame", {
Visible = showToolTip,
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromOffset(0, 0),
BackgroundTransparency = 1,
}, {
TooltipContent = Roact.createElement(TooltipContainer, {
triggerPosition = self.state.triggerPosition or Vector2.new(0 ,0),
triggerSize = self.state.triggerSize or Vector2.new(0 ,0),
position = self.props.tooltipPosition or UDim2.new(1, -self.tooltipWidth:getValue(), 1, 0),
bodyText = self.props.fullText or "",
isDirectChild = true,
}),
})
})
end)
end
function RobuxBalance:didUpdate()
local hasFullText = self.props.fullText
self.setTooltipWidth((self.triggerRef.current and hasFullText) and
self.triggerRef.current.TooltipContainer.TooltipContent.Size.X.Offset or 0)
end
return RobuxBalance
@@ -0,0 +1,39 @@
return function()
local Control = script.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local RobuxBalance = require(Control.RobuxBalance)
it("should create and destroy Robux Balance with no props without errors", function()
local element = mockStyleComponent({
RobuxBalance = Roact.createElement(RobuxBalance),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy Robux Balance with all props without errors", function()
local element = mockStyleComponent({
RobuxBalance = Roact.createElement(RobuxBalance, {
displayText = "9.1K Robux",
fullText = "9107 Robux",
position = UDim2.new(1, 0, 0, 0),
onActivated = function() end,
onStateChanged = function() end,
tooltipPosition = UDim2.new(1, 0, 0, 0),
userInteractionEnabled = true,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,425 @@
local Control = script.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Core = UIBlox.Core
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Cryo = require(Packages.Cryo)
local withStyle = require(UIBlox.Core.Style.withStyle)
local Otter = require(Packages.Otter)
local RoactGamepad = require(Packages.RoactGamepad)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local ControlState = require(Core.Control.Enum.ControlState)
local SegmentedControlTabName = require(script.Parent.SegmentedControlTabName)
local ImageSetComponent = require(Core.ImageSet.ImageSetComponent)
local Images = require(App.ImageSet.Images)
local getContentStyle = require(Core.Button.getContentStyle)
local FRAME_PADDING = 4
local MIN_TAB_WIDTH = 108
local MAX_WIDTH = 640
local INTERACTION_HEIGHT = 44
local BACKGROUND_HEIGHT = 36
local TAB_HEIGHT = 28
local SPRING_PARAMS = {
frequency = 10,
dampingRatio = 1,
}
local BACKGROUND_IMAGE = "component_assets/circle_17"
local SHADOW_IMAGE = "component_assets/dropshadow_25"
local BACKGROUND_COLOR_STATE_MAP = {
[ControlState.Default] = "BackgroundUIDefault"
}
local DIVIDER_COLOR_STATE_MAP = {
[ControlState.Default] = "Divider"
}
local SELECTED_BACKGROUND_COLOR_STATE_MAP = {
[ControlState.Default] = "UIDefault"
}
local DROPSHADOW_COLOR_STATE_MAP = {
[ControlState.Default] = "DropShadow"
}
local limitedLengthTabArray = function(array)
local typeChecker, typeCheckerMessage = t.array(t.strictInterface({
-- Callback when this tab is selected
onActivated = t.optional(t.callback),
-- The name and ID for this tab
tabName = t.string,
-- If this tab is disabled.
isDisabled = t.optional(t.boolean),
}))(array)
if not typeChecker then
return typeChecker, typeCheckerMessage
end
local minLengthChecker, minLengthCheckerMessage = t.numberMin(2)(#array)
if not minLengthChecker then
return minLengthChecker, minLengthCheckerMessage
end
local maxLengthChecker, maxLengthCheckerMessage = t.numberMax(4)(#array)
if not maxLengthChecker then
return maxLengthChecker, maxLengthCheckerMessage
end
return true
end
local SegmentedControl = Roact.Component:extend("SegmentedControl")
SegmentedControl.validateProps = t.strictInterface({
tabs = limitedLengthTabArray,
-- Width for this component
-- Will be restricted by the component's size constraint.
width = t.UDim,
defaultSelctedTabIndex = t.optional(t.number),
-- optional parameters for RoactGamepad
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionDown = t.optional(t.table),
})
function SegmentedControl:init()
self.rootRef = Roact.createRef()
self.tabRefs = RoactGamepad.createRefCache()
self.selectedBackgroundMotor = Otter.createSingleMotor(FRAME_PADDING)
self.selectedBackgroundMotor:onStep(function(selectedBackgroundPositionX)
self:setState({
selectedBackgroundPositionX = selectedBackgroundPositionX
})
end)
local defaultSelectedIndex = self.props.defaultSelctedTabIndex or 1
local defaultSelectedTab = self.props.tabs[defaultSelectedIndex]
self:setState({
selectedTab = defaultSelectedTab,
selectedIndex = defaultSelectedIndex,
selectedBackgroundPositionX = 0,
tabWidth = 0,
})
self.onTabActivated = function(index)
self.selectTab(self.props.tabs[index], index)
end
self.setSize = function(rbx)
local frameWidth = rbx.AbsoluteSize.X
local totalTabWidth = frameWidth - FRAME_PADDING * 2
local tabWidth = math.floor(totalTabWidth / #self.props.tabs)
self.selectedBackgroundMotor:setGoal(Otter.spring(FRAME_PADDING + tabWidth * (self.state.selectedIndex - 1),
SPRING_PARAMS))
self:setState({
tabWidth = tabWidth,
})
end
self.selectTab = function(activatedTab, selectedIndex)
self.selectedBackgroundMotor:setGoal(Otter.spring(FRAME_PADDING + self.state.tabWidth * (selectedIndex - 1),
SPRING_PARAMS))
self:setState({
selectedTab = activatedTab,
selectedIndex = selectedIndex
})
activatedTab.onActivated(activatedTab)
end
end
function SegmentedControl:renderDefault()
return withStyle(function(style)
-- render params
local currentState = self.state.controlState
local isDisabled = self.state.selectedTab.isDisabled
local forceSelectedBGState = isDisabled and ControlState.Disabled or currentState
local backgroundStyle = getContentStyle(BACKGROUND_COLOR_STATE_MAP, currentState, style)
local dividerStyle = getContentStyle(DIVIDER_COLOR_STATE_MAP, currentState, style)
local selectedBackgroundStyle = getContentStyle(SELECTED_BACKGROUND_COLOR_STATE_MAP, forceSelectedBGState, style)
local dropshadowStyle = getContentStyle(DROPSHADOW_COLOR_STATE_MAP, currentState, style)
local tabWidth = self.state.tabWidth
-- dividers between tabs
local dividers = {}
for i = 1, #self.props.tabs - 1, 1 do
if i ~= self.state.selectedIndex and i ~= self.state.selectedIndex - 1 then
table.insert(dividers, i, Roact.createElement("Frame", {
Size = UDim2.fromOffset(1, TAB_HEIGHT),
BackgroundTransparency = dividerStyle.Transparency,
BackgroundColor3 = dividerStyle.Color,
Position = UDim2.fromOffset(FRAME_PADDING + tabWidth * i, (INTERACTION_HEIGHT - TAB_HEIGHT) / 2)
}))
end
end
-- create the tabs as SegmentedControlTabName and add activated callback
local tabs = Cryo.List.map(self.props.tabs, function(tab, index)
return Roact.createElement("Frame",{
LayoutOrder = index,
Size = UDim2.fromOffset(tabWidth, INTERACTION_HEIGHT),
BackgroundTransparency = 1,
}, {
Tab = Roact.createElement(SegmentedControlTabName, {
text = tab.tabName,
onActivated = function() self.onTabActivated(index) end,
Size = UDim2.fromScale(1, 1),
isDisabled = tab.isDisabled,
isSelectedStyle = self.state.selectedIndex == index
})
})
end)
-- add UIListLayout for tabs
table.insert(tabs, Roact.createElement("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder
}))
return Roact.createElement("Frame", {
Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, INTERACTION_HEIGHT),
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = self.setSize
}, {
SizeConstraint = Roact.createElement("UISizeConstraint", {
MinSize = Vector2.new(MIN_TAB_WIDTH * #self.props.tabs + FRAME_PADDING * 2, INTERACTION_HEIGHT),
MaxSize = Vector2.new(MAX_WIDTH, INTERACTION_HEIGHT),
}),
-- tab group background
Background = Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.new(1, 0, 0, BACKGROUND_HEIGHT),
Position = UDim2.fromOffset(0, (INTERACTION_HEIGHT - BACKGROUND_HEIGHT) / 2),
BackgroundTransparency = 1,
Image = Images[BACKGROUND_IMAGE],
ImageColor3 = backgroundStyle.Color,
ImageTransparency = backgroundStyle.Transparency,
LayoutOrder = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
ZIndex = 1,
}),
-- put dividers in one frame
Dividers = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, INTERACTION_HEIGHT),
Position = UDim2.fromOffset(0, 0),
BackgroundTransparency = 1,
ZIndex = 2,
}, dividers),
-- the only selected tab background, used to make animation
SelectedBackground = Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.fromOffset(tabWidth, TAB_HEIGHT),
Position = UDim2.fromOffset(self.state.selectedBackgroundPositionX, (INTERACTION_HEIGHT - TAB_HEIGHT) / 2),
BackgroundTransparency = 1,
Image = BACKGROUND_IMAGE,
ImageColor3 = selectedBackgroundStyle.Color,
ImageTransparency = selectedBackgroundStyle.Transparency,
LayoutOrder = 2,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
ZIndex = 4,
}),
-- the shadow for selected tab background
SelectedBackgroundShadow = not isDisabled and Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.fromOffset(tabWidth + 6 * 2, TAB_HEIGHT + 6 * 2),
Position = UDim2.fromOffset(
self.state.selectedBackgroundPositionX - 6,
(INTERACTION_HEIGHT - TAB_HEIGHT) / 2 - 6 + 2
),
BackgroundTransparency = 1,
Image = Images[SHADOW_IMAGE],
ImageColor3 = dropshadowStyle.Color,
ImageTransparency = 0.3,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(18, 18, 19, 19),
ZIndex = 3,
}),
-- container for tabs
TabContainer = Roact.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Position = UDim2.fromScale(0, 0),
ZIndex = 5,
},tabs)
})
end)
end
function SegmentedControl:renderGamepadSupport()
return withStyle(function(style)
-- render params
local currentState = self.state.controlState
local isDisabled = self.state.selectedTab.isDisabled
local forceSelectedBGState = isDisabled and ControlState.Disabled or currentState
local backgroundStyle = getContentStyle(BACKGROUND_COLOR_STATE_MAP, currentState, style)
local dividerStyle = getContentStyle(DIVIDER_COLOR_STATE_MAP, currentState, style)
local selectedBackgroundStyle = getContentStyle(SELECTED_BACKGROUND_COLOR_STATE_MAP, forceSelectedBGState, style)
local dropshadowStyle = getContentStyle(DROPSHADOW_COLOR_STATE_MAP, currentState, style)
local tabWidth = self.state.tabWidth
-- dividers between tabs
local dividers = {}
for i = 1, #self.props.tabs - 1, 1 do
if i ~= self.state.selectedIndex and i ~= self.state.selectedIndex - 1 then
table.insert(dividers, i, Roact.createElement("Frame", {
Size = UDim2.fromOffset(1, TAB_HEIGHT),
BackgroundTransparency = dividerStyle.Transparency,
BackgroundColor3 = dividerStyle.Color,
Position = UDim2.fromOffset(FRAME_PADDING + tabWidth * i, (INTERACTION_HEIGHT - TAB_HEIGHT) / 2)
}))
end
end
return RoactGamepad.withFocusController(function(focusController)
local moveToPrevious = function(index)
if index > 1 then
focusController.moveFocusTo(self.tabRefs[index - 1])
end
end
local moveToNext = function(index)
if index < #self.props.tabs then
focusController.moveFocusTo(self.tabRefs[index + 1])
end
end
local moveToParent = function()
focusController.moveFocusTo(self.rootRef)
end
-- create the tabs as SegmentedControlTabName and add activated callback, adding gamepad support
-- gamepad focus is added to the frame, not SegmentedControlTabName
local tabs = Cryo.List.map(self.props.tabs, function(tab, index)
return Roact.createElement(RoactGamepad.Focusable.Frame,{
LayoutOrder = index,
Size = UDim2.fromOffset(tabWidth, INTERACTION_HEIGHT),
BackgroundTransparency = 1,
[Roact.Ref] = self.tabRefs[index],
NextSelectionLeft = index > 1 and self.tabRefs[index - 1] or nil,
NextSelectionRight = index < #self.props.tabs and self.tabRefs[index + 1] or nil,
onFocusGained = function()
if not tab.isDisabled then
self.selectTab(tab)
end
end,
inputBindings = {
LeaveA = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, moveToParent),
LeaveB = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonB, moveToParent),
SelectNext1 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonR1, function() moveToNext(index) end),
SelectPre1 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonL1, function() moveToPrevious(index) end),
SelectNext2 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonR2, function() moveToNext(index) end),
SelectPre2 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonL2, function() moveToPrevious(index) end),
SelectNext3 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonR3, function() moveToNext(index) end),
SelectPre3 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonL3, function() moveToPrevious(index) end)
}
}, {
Tab = Roact.createElement(SegmentedControlTabName, {
text = tab.tabName,
onActivated = function() self.onTabActivated(index) end,
Size = UDim2.fromScale(1, 1),
isDisabled = tab.isDisabled,
isSelectedStyle = self.state.selectedIndex == index
})
})
end)
-- add UIListLayout for tabs
table.insert(tabs, Roact.createElement("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder
}))
return Roact.createElement("Frame", {
Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, INTERACTION_HEIGHT),
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = self.setSize,
}, {
SizeConstraint = Roact.createElement("UISizeConstraint", {
MinSize = Vector2.new(MIN_TAB_WIDTH * #self.props.tabs + FRAME_PADDING * 2, INTERACTION_HEIGHT),
MaxSize = Vector2.new(MAX_WIDTH, INTERACTION_HEIGHT),
}),
-- tab group background
Background = Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.new(1, 0, 0, BACKGROUND_HEIGHT),
Position = UDim2.fromOffset(0, (INTERACTION_HEIGHT - BACKGROUND_HEIGHT) / 2),
BackgroundTransparency = 1,
Image = BACKGROUND_IMAGE,
ImageColor3 = backgroundStyle.Color,
ImageTransparency = backgroundStyle.Transparency,
LayoutOrder = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
ZIndex = 1,
}),
-- put dividers in one frame, use this frame as the "Main" gamepad selection.
Dividers = Roact.createElement(RoactGamepad.Focusable.Frame, {
Size = UDim2.new(1, 0, 0, INTERACTION_HEIGHT),
Position = UDim2.fromOffset(0, 0),
BackgroundTransparency = 1,
ZIndex = 2,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
[Roact.Ref] = self.rootRef,
inputBindings = {
Enter = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, function()
focusController.moveFocusTo(self.tabRefs[self.state.selectedIndex])
end)
}
}, dividers),
-- the only selected tab background, used to make animation
SelectedBackground = Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.fromOffset(tabWidth, TAB_HEIGHT),
Position = UDim2.fromOffset(
self.state.selectedBackgroundPositionX,
(INTERACTION_HEIGHT - TAB_HEIGHT) / 2
),
BackgroundTransparency = 1,
Image = BACKGROUND_IMAGE,
ImageColor3 = selectedBackgroundStyle.Color,
ImageTransparency = selectedBackgroundStyle.Transparency,
LayoutOrder = 2,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
ZIndex = 4,
}),
-- the shadow for selected tab background
SelectedBackgroundShadow = not isDisabled and Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.fromOffset(tabWidth + 6 * 2, TAB_HEIGHT + 6 * 2),
Position = UDim2.fromOffset(
self.state.selectedBackgroundPositionX - 6,
(INTERACTION_HEIGHT - TAB_HEIGHT) / 2 - 6 + 2
),
BackgroundTransparency = 1,
Image = SHADOW_IMAGE,
ImageColor3 = dropshadowStyle.Color,
ImageTransparency = 0.3,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(18, 18, 19, 19),
ZIndex = 3,
}),
-- container for tabs
TabContainer = Roact.createElement(RoactGamepad.Focusable.Frame, {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Position = UDim2.fromScale(0, 0),
ZIndex = 5,
defaultChild = self.tabRefs[self.state.selectedIndex]
},tabs)
})
end)
end)
end
function SegmentedControl:render()
if UIBloxConfig.enableExperimentalGamepadSupport then
return self:renderGamepadSupport()
else
return self:renderDefault()
end
end
return SegmentedControl
@@ -0,0 +1,42 @@
local Control = script.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local SegmentedControl = require(script.Parent.SegmentedControl)
local DEFAULT_REQUIRED_PROPS = {
tabs = {
{
onActivated = function(tab) print(tab.tabName) end,
tabName = "a",
},{
onActivated = function(tab) print(tab.tabName) end,
tabName = "b",
isDisabled = true
},{
onActivated = function(tab) print(tab.tabName) end,
tabName = "c",
},{
onActivated = function(tab) print(tab.tabName) end,
tabName = "d",
}
},
width = UDim.new(0,10000),
}
return function()
describe("lifecycle", function()
it("should mount and unmount SegmentedControl without issue", function()
local tree = mockStyleComponent(
Roact.createElement(SegmentedControl, DEFAULT_REQUIRED_PROPS)
)
local handle = Roact.mount(tree)
Roact.unmount(handle)
end)
end)
end
@@ -0,0 +1,195 @@
local Control = script.Parent
local App = Control.Parent
local UIBlox = App.Parent
local Core = UIBlox.Core
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Cryo = require(Packages.Cryo)
local Interactable = require(Core.Control.Interactable)
local ControlState = require(Core.Control.Enum.ControlState)
local getContentStyle = require(Core.Button.getContentStyle)
local withStyle = require(UIBlox.Core.Style.withStyle)
local ImageSetComponent = require(Core.ImageSet.ImageSetComponent)
local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel)
local IconSize = require(UIBlox.App.ImageSet.Enum.IconSize)
local getIconSize = require(UIBlox.App.ImageSet.getIconSize)
local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel)
local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo)
local validateImage = require(Core.ImageSet.Validator.validateImage)
local CONTENT_PADDING = 5
local SegmentedControlTabName = Roact.PureComponent:extend("SegmentedControlTabName")
function SegmentedControlTabName:init()
self.state = {
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
local colorStateMap = t.interface({
-- The default state theme color class
[ControlState.Default] = t.string,
})
local validateProps = t.interface({
--The icon of the button
icon = t.optional(validateImage),
--The text of the button
text = t.optional(t.string),
fontStyle = t.optional(validateFontInfo),
--The theme color class mapping for different text tates
textStateColorMap = t.optional(colorStateMap),
--The theme color class mapping for different icon tates
iconStateColorMap = t.optional(colorStateMap),
--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,
--The state change callback for the button
onStateChanged = t.optional(t.callback),
--A Boolean value that determines whether user events are ignored and sink input
userInteractionEnabled = t.optional(t.boolean),
isSelectedStyle = t.optional(t.boolean),
-- Note that this component can accept all valid properties of the Roblox ImageButton instance
})
SegmentedControlTabName.defaultProps = {
isDisabled = false,
isLoading = false,
isSelectedStyle = false,
SliceCenter = Rect.new(8, 8, 9, 9),
textStateColorMap = {
[ControlState.Default] = "SecondaryContent",
[ControlState.Hover] = "SecondaryOnHover",
},
buttonStateColorMap = {
[ControlState.Default] = "SecondaryContent",
}
}
SegmentedControlTabName.validateProps = validateProps
function SegmentedControlTabName:render()
return withStyle(function(style)
local currentState = self.state.controlState
local text = self.props.text
local icon = self.props.icon
local isLoading = self.props.isLoading
local isDisabled = self.props.isDisabled
local buttonStateColorMap = self.props.buttonStateColorMap
local contentStateColorMap = self.props.contentStateColorMap
local textStateColorMap = self.props.textStateColorMap or contentStateColorMap
local iconStateColorMap = self.props.iconStateColorMap or contentStateColorMap
if isLoading then
isDisabled = true
end
local textState = currentState
if self.props.isDisabled then
textState = ControlState.Disabled
elseif self.props.isSelectedStyle then
textState = ControlState.Hover
end
local buttonStyle = getContentStyle(buttonStateColorMap, currentState, style)
local textStyle = text and getContentStyle(textStateColorMap, textState, style)
local iconStyle = icon and getContentStyle(iconStateColorMap, currentState, style)
local fontStyle = self.props.fontStyle or style.Font.Header2
local buttonContentLayer
if isLoading then
buttonContentLayer = {
isLoadingShimmer = Roact.createElement(ShimmerPanel, {
Size = UDim2.new(1, 0, 1, 0),
})
}
else
buttonContentLayer = self.props[Roact.Children] or {
UIListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, CONTENT_PADDING),
}),
Icon = icon and Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.new(0, getIconSize(IconSize.Medium), 0, getIconSize(IconSize.Medium)),
BackgroundTransparency = 1,
Image = icon,
ImageColor3 = iconStyle.Color,
ImageTransparency = iconStyle.Transparency,
LayoutOrder = 1,
}) or nil,
Text = text and Roact.createElement(GenericTextLabel, {
BackgroundTransparency = 1,
Text = text,
fontStyle = fontStyle,
colorStyle = textStyle,
LayoutOrder = 2,
}) or nil,
}
end
return Roact.createElement(Interactable, Cryo.Dictionary.join(self.props, {
icon = Cryo.None,
text = Cryo.None,
buttonStateColorMap = Cryo.None,
contentStateColorMap = Cryo.None,
textStateColorMap = Cryo.None,
iconStateColorMap = Cryo.None,
onActivated = Cryo.None,
isLoading = Cryo.None,
isSelectedStyle = Cryo.None,
[Roact.Children] = Cryo.None,
isDisabled = isDisabled,
onStateChanged = self.onStateChanged,
userInteractionEnabled = self.props.userInteractionEnabled,
Image = Cryo.None,
ScaleType = Enum.ScaleType.Slice,
ImageColor3 = buttonStyle.Color,
ImageTransparency = buttonStyle.Transparency,
BackgroundTransparency = 1,
AutoButtonColor = false,
[Roact.Event.Activated] = self.props.onActivated,
}), {
ButtonContent = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, buttonContentLayer)
})
end)
end
return SegmentedControlTabName
@@ -0,0 +1,213 @@
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local RoactGamepad = require(Packages.RoactGamepad)
local Cryo = require(Packages.Cryo)
local enumerateValidator = require(UIBlox.Utility.enumerateValidator)
local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel)
local GetTextHeight = require(UIBlox.Core.Text.GetTextHeight)
local Images = require(AppRoot.ImageSet.Images)
local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent)
local withStyle = require(UIBlox.Core.Style.withStyle)
local ButtonStack = require(AppRoot.Button.ButtonStack)
local FitFrame = require(Packages.FitFrame)
local FitFrameOnAxis = FitFrame.FitFrameOnAxis
local AlertType = require(AlertRoot.Enum.AlertType)
local AlertTitle = require(AlertRoot.AlertTitle)
local BACKGROUND_IMAGE = "component_assets/circle_17"
local MARGIN = 24
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig
local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack)
local Alert = Roact.PureComponent:extend("Alert")
local validateProps = t.strictInterface({
alertType = enumerateValidator(AlertType),
anchorPoint = t.optional(t.Vector2),
bodyText = t.optional(t.string),
buttonStackInfo = t.optional(validateButtonStack),
margin = t.optional(t.table),
maxWidth = t.optional(t.number),
minWidth = t.optional(t.number),
middleContent = t.optional(t.callback),
middleContentPaddingBetweenBodyText = t.optional(t.number),
onMounted = t.optional(t.callback),
onAbsoluteSizeChanged = t.optional(t.callback),
paddingBetween = t.optional(t.number),
position = t.optional(t.UDim2),
screenSize = t.Vector2,
title = t.string,
titleIcon = enableAlertTitleIconConfig and t.optional(t.union(t.table, t.string)) or nil,
titleIconSize = enableAlertTitleIconConfig and t.optional(t.number) or nil,
titlePadding = t.optional(t.number),
titlePaddingWithIcon = t.optional(t.number),
--Gamepad props
defaultChildRef = t.optional(t.table),
isMiddleContentFocusable = t.optional(t.boolean),
})
Alert.defaultProps = {
anchorPoint = Vector2.new(0.5, 0.5),
margin = {
top = 0,
bottom = MARGIN,
left = MARGIN,
right = MARGIN,
},
maxWidth = 400,
middleContentPaddingBetweenBodyText = 12,
minWidth = 272,
paddingBetween = 24,
position = UDim2.new(0.5, 0, 0.5, 0),
}
function Alert:init()
self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0))
if UIBloxConfig.enableExperimentalGamepadSupport then
self.middleContentRef = Roact.createRef()
end
self.buttonStackRef = Roact.createRef()
end
function Alert:didMount()
if self.props.onMounted then
self.props.onMounted()
end
end
function Alert:render()
assert(validateProps(self.props))
local isMiddleContentFocusable = UIBloxConfig.enableExperimentalGamepadSupport and self.props.isMiddleContentFocusable
local totalWidth = math.clamp(self.props.screenSize.X - self.props.margin.left - self.props.margin.right,
self.props.minWidth, self.props.maxWidth)
local innerWidth = totalWidth - self.props.margin.left - self.props.margin.right
return withStyle(function(stylePalette)
local font = stylePalette.Font
local theme = stylePalette.Theme
local textFont = font.Body.Font
local fontSize = font.BaseSize * font.Body.RelativeSize
local fullTextHeight = self.props.bodyText
and GetTextHeight(self.props.bodyText, textFont, fontSize, innerWidth)
or 0
local backgroundTransparency
local imageColor
local imageTransparency
if self.props.alertType == AlertType.Interactive then
imageColor = theme.BackgroundUIDefault.Color
imageTransparency = theme.BackgroundUIDefault.Transparency
backgroundTransparency = 1
else
backgroundTransparency = theme.BackgroundUIContrast.Transparency
imageTransparency = 1
end
local buttonStackInfo = self.props.buttonStackInfo
if UIBloxConfig.enableExperimentalGamepadSupport and buttonStackInfo then
buttonStackInfo = Cryo.Dictionary.join(buttonStackInfo, {
[Roact.Ref] = self.buttonStackRef,
NextSelectionUp = isMiddleContentFocusable and self.middleContentRef or nil,
})
end
return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[ImageSetComponent.Button] or ImageSetComponent.Button, {
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
Size = self.contentSize:map(function(absoluteSize)
return UDim2.new(0, absoluteSize.X, 0, absoluteSize.Y)
end),
BackgroundColor3 = theme.BackgroundUIDefault.Color,
BackgroundTransparency = backgroundTransparency,
BorderSizePixel = 0,
Image = Images[BACKGROUND_IMAGE],
ImageColor3 = imageColor,
ImageTransparency = imageTransparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
AutoButtonColor = false,
ClipsDescendants = true,
Selectable = false,
[Roact.Ref] = self.props.defaultChildRef,
[Roact.Change.AbsoluteSize] = self.props.onAbsoluteSizeChanged,
defaultChild = UIBloxConfig.enableExperimentalGamepadSupport and self.buttonStackRef or nil,
}, {
AlertContents = Roact.createElement(FitFrameOnAxis, {
contentPadding = UDim.new(0, self.props.paddingBetween),
margin = self.props.margin,
minimumSize = UDim2.new(0, totalWidth, 0, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
[Roact.Change.AbsoluteSize] = function(rbx)
self.changeContentSize(rbx.AbsoluteSize)
end,
}, {
TitleContainer = Roact.createElement(AlertTitle, {
layoutOrder = 1,
margin = self.props.margin,
maxWidth = self.props.maxWidth,
minWidth = self.props.minWidth,
screenSize = self.props.screenSize,
title = self.props.title,
titleIcon = self.props.titleIcon,
titleIconSize = self.props.titleIconSize,
titlePadding = self.props.titlePadding,
titlePaddingWithIcon = self.props.titlePaddingWithIcon,
}),
Content = Roact.createElement(FitFrameOnAxis, {
BackgroundTransparency = 1,
contentPadding = UDim.new(0, self.props.middleContentPaddingBetweenBodyText),
LayoutOrder = 2,
minimumSize = UDim2.new(1, 0, 0, 0),
}, {
BodyText = self.props.bodyText and Roact.createElement(GenericTextLabel, {
BackgroundTransparency = 1,
colorStyle = theme.TextDefault,
fontStyle = font.Body,
LayoutOrder = 1,
Text = self.props.bodyText,
TextSize = fontSize,
TextXAlignment = Enum.TextXAlignment.Center,
Size = UDim2.new(1, 0, 0, fullTextHeight),
}),
MiddleContent = self.props.middleContent and Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and
RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, {
BackgroundTransparency = 1,
LayoutOrder = 2,
minimumSize = UDim2.new(1, 0, 0, 0),
[Roact.Ref] = self.middleContentRef,
NextSelectionDown = isMiddleContentFocusable and self.buttonStackRef or nil,
},
{
Content = self.props.middleContent()
}
),
}),
Buttons = buttonStackInfo and Roact.createElement(ButtonStack, buttonStackInfo),
})
})
end)
end
return Alert
@@ -0,0 +1,61 @@
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Cryo = require(Packages.Cryo)
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local Alert = require(script.Parent.Alert)
local AlertType = require(AlertRoot.Enum.AlertType)
local DEFAULT_REQUIRED_PROPS = {
alertType = AlertType.Informative,
title = "Hello World",
screenSize = Vector2.new(100, 100),
}
local function mountAlert(props)
local combinedProps = DEFAULT_REQUIRED_PROPS
if props then
combinedProps = Cryo.Dictionary.join(DEFAULT_REQUIRED_PROPS, props)
end
local tree = mockStyleComponent(
Roact.createElement(Alert, combinedProps)
)
local handle = Roact.mount(tree)
return tree, function()
Roact.unmount(handle)
end
end
return function()
describe("lifecycle", function()
it("should mount and unmount informative alerts without issue", function()
local _, cleanup = mountAlert({
alertType = AlertType.Informative,
})
cleanup()
end)
it("should mount and unmount interactive alerts without issue", function()
local _, cleanup = mountAlert({
alertType = AlertType.Interactive,
buttonStackInfo = {
buttons = {
{
props = {
text = "test",
onActivated = function() end,
}
},
},
},
})
cleanup()
end)
end)
end
@@ -0,0 +1,117 @@
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local FitFrame = require(Packages.FitFrame)
local FitFrameOnAxis = FitFrame.FitFrameOnAxis
local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel)
local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent)
local withStyle = require(UIBlox.Core.Style.withStyle)
local MARGIN = 24
local AlertTitle = Roact.PureComponent:extend("AlertTitle")
local validateProps = t.strictInterface({
layoutOrder = t.optional(t.number),
margin = t.optional(t.table),
maxWidth = t.optional(t.number),
minWidth = t.optional(t.number),
screenSize = t.Vector2,
title = t.string,
titleIcon = t.optional(t.union(t.table, t.string)),
titleIconSize = t.optional(t.number),
titlePadding = t.optional(t.number),
titlePaddingWithIcon = t.optional(t.number),
})
AlertTitle.defaultProps = {
margin = {
top = 0,
bottom = MARGIN,
left = MARGIN,
right = MARGIN,
},
maxWidth = 400,
minWidth = 272,
titleIconSize = 48,
titlePadding = 12,
titlePaddingWithIcon = 24,
}
function AlertTitle:render()
assert(validateProps(self.props))
local totalWidth = math.clamp(self.props.screenSize.X - self.props.margin.left - self.props.margin.right,
self.props.minWidth, self.props.maxWidth)
local innerWidth = totalWidth - self.props.margin.left - self.props.margin.right
local titleTopMargin
if self.props.titleIcon then
titleTopMargin = self.props.titlePaddingWithIcon
else
titleTopMargin = self.props.titlePadding
end
return withStyle(function(stylePalette)
local font = stylePalette.Font
local theme = stylePalette.Theme
local headerSize = font.BaseSize * font.Header1.RelativeSize
return Roact.createElement(FitFrameOnAxis, {
BackgroundTransparency = 1,
contentPadding = UDim.new(0, 8),
HorizontalAlignment = Enum.HorizontalAlignment.Center,
LayoutOrder = self.props.layoutOrder,
margin = {
top = titleTopMargin,
bottom = 0,
left = 0,
right = 0,
},
minimumSize = UDim2.new(1, 0, 0, 0),
}, {
TitleIcon = self.props.titleIcon and Roact.createElement(ImageSetComponent.Label, {
BackgroundTransparency = 1,
Image = self.props.titleIcon,
ImageColor3 = theme.IconEmphasis.Color,
ImageTransparency = theme.IconEmphasis.Transparency,
LayoutOrder = 0,
Size = UDim2.new(0, self.props.titleIconSize, 0, self.props.titleIconSize),
}),
TitleArea = Roact.createElement(FitFrameOnAxis, {
BackgroundTransparency = 1,
contentPadding = UDim.new(0, self.props.titlePadding),
HorizontalAlignment = Enum.HorizontalAlignment.Center,
LayoutOrder = 1,
minimumSize = UDim2.new(1, 0, 0, 0),
}, {
Title = Roact.createElement(GenericTextLabel, {
colorStyle = theme.TextEmphasis,
fontStyle = font.Header1,
maxSize = Vector2.new(innerWidth, headerSize * 2),
LayoutOrder = 1,
Text = self.props.title,
TextSize = headerSize,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
Underline = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = theme.Divider.Color,
BackgroundTransparency = theme.Divider.Transparency,
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 1),
}),
}),
})
end)
end
return AlertTitle
@@ -0,0 +1,28 @@
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local AlertTitle = require(script.Parent.AlertTitle)
return function()
describe("lifecycle", function()
it("should mount and unmount informative alerts without issue", function()
local element = mockStyleComponent({
Item = Roact.createElement(AlertTitle, {
title = "Hello World",
screenSize = Vector2.new(100, 100),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,13 @@
local AlertRoot = script.Parent.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local enumerate = require(Packages.enumerate)
return enumerate("AlertType", {
"Informative",
"Interactive",
"Loading",
})
@@ -0,0 +1,62 @@
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Alert = require(AlertRoot.Alert)
local AlertType = require(AlertRoot.Enum.AlertType)
local MIN_WIDTH = 272
local MAX_WIDTH = 400
local MARGIN = 24
local PADDING_BETWEEN = 24
local TITLE_PADDING = 12
local TITLE_MARGIN_WITH_ICON = 24
local TITLE_ICON_SIZE = 48
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig
local InformativeAlert = Roact.PureComponent:extend("InformativeAlert")
local validateProps = t.strictInterface({
anchorPoint = t.optional(t.Vector2),
bodyText = t.optional(t.string),
onMounted = t.optional(t.callback),
position = t.optional(t.UDim2),
screenSize = t.Vector2,
title = t.string,
titleIcon = t.optional(t.union(t.table, t.string)),
})
function InformativeAlert:render()
assert(validateProps(self.props))
return Roact.createElement(Alert, {
anchorPoint = self.props.anchorPoint,
alertType = AlertType.Informative,
bodyText = self.props.bodyText,
margin = {
top = 0,
bottom = MARGIN,
left = MARGIN,
right = MARGIN,
},
maxWidth = MAX_WIDTH,
minWidth = MIN_WIDTH,
onMounted = self.props.onMounted,
paddingBetween = PADDING_BETWEEN,
position = self.props.position,
screenSize = self.props.screenSize,
title = self.props.title,
titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil,
titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil,
titlePadding = TITLE_PADDING,
titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON,
})
end
return InformativeAlert
@@ -0,0 +1,27 @@
return function()
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local InformativeAlert = require(AlertRoot.InformativeAlert)
describe("lifecycle", function()
it("should mount and unmount without issue", function()
local element = mockStyleComponent({
Item = Roact.createElement(InformativeAlert, {
screenSize = Vector2.new(100, 100),
title = "Hello World",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,76 @@
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Alert = require(AlertRoot.Alert)
local AlertType = require(AlertRoot.Enum.AlertType)
local MIN_WIDTH = 272
local MAX_WIDTH = 400
local MARGIN = 24
local PADDING_BETWEEN = 24
local TITLE_PADDING = 12
local TITLE_MARGIN_WITH_ICON = 24
local TITLE_ICON_SIZE = 48
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig
local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack)
local InteractiveAlert = Roact.PureComponent:extend("InteractiveAlert")
local validateProps = t.strictInterface({
anchorPoint = t.optional(t.Vector2),
bodyText = t.optional(t.string),
buttonStackInfo = validateButtonStack,
middleContent = t.optional(t.callback),
onMounted = t.optional(t.callback),
onAbsoluteSizeChanged = t.optional(t.callback),
position = t.optional(t.UDim2),
screenSize = t.Vector2,
title = t.string,
titleIcon = t.optional(t.union(t.table, t.string)),
--Gamepad props
defaultChildRef = t.optional(t.table),
isMiddleContentFocusable = t.optional(t.boolean),
})
function InteractiveAlert:render()
assert(validateProps(self.props))
return Roact.createElement(Alert, {
anchorPoint = self.props.anchorPoint,
alertType = AlertType.Interactive,
bodyText = self.props.bodyText,
margin = {
top = 0,
bottom = MARGIN,
left = MARGIN,
right = MARGIN,
},
maxWidth = MAX_WIDTH,
minWidth = MIN_WIDTH,
buttonStackInfo = self.props.buttonStackInfo,
middleContent = self.props.middleContent,
isMiddleContentFocusable = self.props.isMiddleContentFocusable,
onMounted = self.props.onMounted,
onAbsoluteSizeChanged = self.props.onAbsoluteSizeChanged,
paddingBetween = PADDING_BETWEEN,
position = self.props.position,
screenSize = self.props.screenSize,
title = self.props.title,
titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil,
titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil,
titlePadding = TITLE_PADDING,
titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON,
defaultChildRef = self.props.defaultChildRef,
})
end
return InteractiveAlert
@@ -0,0 +1,36 @@
return function()
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local InteractiveAlert = require(AlertRoot.InteractiveAlert)
describe("lifecycle", function()
it("should mount and unmount without issue", function()
local element = mockStyleComponent({
Item = Roact.createElement(InteractiveAlert, {
buttonStackInfo = {
buttons = {
{
props = {
onActivated = function() end,
},
},
},
},
screenSize = Vector2.new(100, 100),
title = "Hello World",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,79 @@
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Alert = require(AlertRoot.Alert)
local AlertType = require(AlertRoot.Enum.AlertType)
local MIN_WIDTH = 272
local MAX_WIDTH = 400
local MARGIN = 24
local PADDING_BETWEEN = 24
local TITLE_PADDING = 12
local TITLE_MARGIN_WITH_ICON = 24
local TITLE_ICON_SIZE = 48
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig
local LoadingSpinner = require(UIBlox.App.Loading.LoadingSpinner)
local LoadingAlert = Roact.PureComponent:extend("LoadingAlert")
local validateProps = t.strictInterface({
anchorPoint = t.optional(t.Vector2),
bodyText = t.optional(t.string),
onMounted = t.optional(t.callback),
position = t.optional(t.UDim2),
screenSize = t.Vector2,
title = t.string,
titleIcon = t.optional(t.union(t.table, t.string)),
})
function LoadingAlert:init()
self.renderMiddleContent = function()
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 48),
BackgroundTransparency = 1,
}, {
Spinner = Roact.createElement(LoadingSpinner, {
size = UDim2.fromOffset(48, 48),
position = UDim2.fromScale(0.5, 0.5),
anchorPoint = Vector2.new(0.5, 0.5),
})
})
end
end
function LoadingAlert:render()
assert(validateProps(self.props))
return Roact.createElement(Alert, {
anchorPoint = self.props.anchorPoint,
alertType = AlertType.Loading,
margin = {
top = 0,
bottom = MARGIN,
left = MARGIN,
right = MARGIN,
},
maxWidth = MAX_WIDTH,
minWidth = MIN_WIDTH,
middleContent = self.renderMiddleContent,
onMounted = self.props.onMounted,
paddingBetween = PADDING_BETWEEN,
position = self.props.position,
screenSize = self.props.screenSize,
title = self.props.title,
titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil,
titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil,
titlePadding = TITLE_PADDING,
titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON,
})
end
return LoadingAlert
@@ -0,0 +1,27 @@
return function()
local AlertRoot = script.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local LoadingAlert = require(AlertRoot.LoadingAlert)
describe("lifecycle", function()
it("should mount and unmount without issue", function()
local element = mockStyleComponent({
Item = Roact.createElement(LoadingAlert, {
screenSize = Vector2.new(100, 100),
title = "Hello World",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,38 @@
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 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 = 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)),
})
@@ -0,0 +1,126 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView)
local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem)
local AlertRoot = script.Parent.Parent
local DialogRoot = AlertRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(UIBlox.App.ImageSet.Images)
local BACKGROUND_IMAGE = "icons/status/premium_small"
local Alert = require(AlertRoot.Alert)
local AlertType = require(AlertRoot.Enum.AlertType)
local ButtonType = require(AppRoot.Button.Enum.ButtonType)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig
local function close()
print("close")
end
local function confirm()
print("confirm")
end
local AlertContainer = Roact.PureComponent:extend("AlertContainer")
function AlertContainer:init()
self.screenSize = nil
self.screenRef = Roact.createRef()
self.state = {
screenSize = Vector2.new(0, 0),
}
self.changeScreenSize = function(rbx)
if self.state.screenSize ~= rbx.AbsoluteSize then
self:setState({
screenSize = rbx.AbsoluteSize
})
end
end
self.renderMiddle = function()
return Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
LayoutOrder = 3,
Size = UDim2.new(1, 0, 0, 60),
},{
CustomInner = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
LayoutOrder = 3,
Text = "Put any component you want here.",
TextSize = 13,
TextWrapped = true,
Size = UDim2.new(1, 0, 1, 0),
}),
})
end
end
function AlertContainer:didMount()
self.screenSize = self.screenRef and self.screenRef.current.AbsoluteSize
end
function AlertContainer:render()
return Roact.createElement(StoryItem, {
size = UDim2.new(1, 0, 1, 0),
title = "AlertContainer",
subTitle = "<<internal>>",
}, {
Screen = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
[Roact.Ref] = self.screenRef,
[Roact.Change.AbsoluteSize] = self.changeScreenSize,
}, {
Alert = Roact.createElement(Alert, {
anchorPoint = Vector2.new(0.5, 0),
alertType = AlertType.Interactive,
bodyText = "Body text goes here. Both InformativeAlert and "..
"InteractiveAlert use this component.",
buttonStackInfo = {
buttons = {
{
props = {
isDisabled = false,
onActivated = close,
text = "Cancel",
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
isDisabled = true,
onActivated = confirm,
text = "Confirm",
},
},
},
},
middleContent = self.renderMiddle,
position = UDim2.new(0.5, 0, 0, 10),
screenSize = self.state.screenSize,
title = "Alert Component. Title goes up to 2 lines max.",
titleIcon = enableAlertTitleIconConfig and Images[BACKGROUND_IMAGE] or nil,
})
})
})
end
return function(target)
local story = Roact.createElement(StoryView, {}, {
Roact.createElement(AlertContainer)
})
local handle = Roact.mount(story, target, "Alert")
return function()
Roact.unmount(handle)
end
end
@@ -0,0 +1,170 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local FitFrame = require(Packages.FitFrame)
local FitFrameVertical = FitFrame.FitFrameVertical
local ImageSetLabel = require(UIBlox.Core.ImageSet.ImageSetComponent).Label
local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel)
local withStyle = require(UIBlox.Core.Style.withStyle)
local ButtonType = require(UIBlox.App.Button.Enum.ButtonType)
local getIconSize = require(AppRoot.ImageSet.getIconSize)
local IconSize = require(AppRoot.ImageSet.Enum.IconSize)
local PartialPageModal = require(ModalRoot.PartialPageModal)
local BODY_CONTENTS_WIDTH = 253
local BODY_CONTENTS_MARGIN = 20
local EducationalModal = Roact.PureComponent:extend("EducationalModal")
EducationalModal.validateProps = t.strictInterface({
bodyContents = t.array(t.strictInterface({
icon = t.union(t.string, t.table),
text = t.string,
layoutOrder = t.integer,
isSystemMenuIcon = t.optional(t.boolean),
})),
cancelText = t.string,
confirmText = t.string,
titleText = t.string,
titleBackgroundImageProps = t.strictInterface({
image = t.string,
imageHeight = t.number,
}),
screenSize = t.Vector2,
onDismiss = t.callback,
onCancel = t.callback,
onConfirm = t.callback,
})
local function ContentItem(props)
local totalTextSize = Vector2.new(16, 16)
local paddingBetween = 8
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
local font = stylePalette.Font
local textSize = font.Body.RelativeSize * font.BaseSize
return Roact.createElement("Frame", {
Size = UDim2.new(0, BODY_CONTENTS_WIDTH, 0, totalTextSize.Y),
BackgroundTransparency = 1,
LayoutOrder = props.layoutOrder,
}, {
HorizontalLayout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Horizontal,
Padding = props.isSystemMenuIcon and UDim.new(0, paddingBetween + 2)
or UDim.new(0, paddingBetween),
VerticalAlignment = Enum.VerticalAlignment.Center
}),
Padding = Roact.createElement("UIPadding", {
PaddingLeft = props.isSystemMenuIcon and UDim.new(0, 2)
or UDim.new(0, 0),
}),
Icon = props.isSystemMenuIcon and Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(32, 32),
Image = "rbxasset://textures/ui/TopBar/iconBase.png",
LayoutOrder = 1,
}, {
Icon = Roact.createElement(ImageSetLabel, {
ZIndex = 1,
BackgroundTransparency = 1,
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.fromOffset(24, 24),
Image = "rbxasset://textures/ui/TopBar/coloredlogo.png",
}),
}) or Roact.createElement(ImageSetLabel, {
Image = props.icon,
Size = UDim2.fromOffset(getIconSize(IconSize.Medium), getIconSize(IconSize.Medium)),
ImageColor3 = theme.IconDefault.Color,
ImageTransparency = theme.IconDefault.Transparency,
BackgroundTransparency = 1,
LayoutOrder = 1,
}),
Roact.createElement(GenericTextLabel, {
Size = UDim2.new(1, 0, 0, totalTextSize.Y),
BackgroundTransparency = 1,
Text = props.text,
TextSize = textSize,
colorStyle = theme.TextDefault,
TextTransparency = theme.TextDefault.Transparency,
fontStyle = font.Body,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
LayoutOrder = 2,
}),
})
end)
end
function EducationalModal:init()
self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0))
end
function EducationalModal:render()
local props = self.props
local elements = {}
for _, content in ipairs(props.bodyContents) do
local current = Roact.createElement(ContentItem, {
icon = content.icon,
text = content.text,
layoutOrder = content.layoutOrder,
isSystemMenuIcon = content.isSystemMenuIcon,
})
table.insert(elements, current)
end
return Roact.createElement(PartialPageModal, {
title = props.titleText,
titleBackgroundImageProps = props.titleBackgroundImageProps,
screenSize = props.screenSize,
bottomPadding = 50,
buttonStackProps = {
buttons = {
{
props = {
isDisabled = false,
onActivated = props.onCancel,
text = props.cancelText,
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
isDisabled = false,
onActivated = props.onConfirm,
text = props.confirmText,
},
},
},
},
onCloseClicked = props.onDismiss,
}, {
BodyContents = Roact.createElement(FitFrameVertical, {
BackgroundTransparency = 1,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
width = UDim.new(1, 0),
contentPadding = UDim.new(0, 28),
margin = {
top = BODY_CONTENTS_MARGIN,
bottom = BODY_CONTENTS_MARGIN,
},
[Roact.Change.AbsoluteSize] = function(rbx)
self.changeContentSize(rbx.AbsoluteSize)
end,
}, elements),
})
end
return EducationalModal
@@ -0,0 +1,61 @@
return function()
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(Packages.UIBlox.App.ImageSet.Images)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local EducationalModal = require(script.Parent.EducationalModal)
local requiredProps = {
bodyContents = {
{
icon = Images["icons/logo/block"],
text = "Body 1",
layoutOrder = 1
},
{
icon = Images["icons/menu/home_on"],
text = "Body 2",
layoutOrder = 2
},
{
icon = Images["icons/menu/games_on"],
text = "Body 3",
layoutOrder = 3
},
},
cancelText = "Cancel",
confirmText = "Confirm",
titleText = "Title",
titleBackgroundImageProps = {
image = "rbxassetid://2610133241",
imageHeight = 200,
},
screenSize = Vector2.new(1920, 1080),
onDismiss = function()
print("Dismiss")
end,
onCancel = function()
print("Cancel")
end,
onConfirm = function()
print("Confirm")
end,
}
it("should create and destroy without errors", function()
local element = mockStyleComponent({
EducationalModalDialog = Roact.createElement(EducationalModal, requiredProps),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,92 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local FitFrame = require(Packages.FitFrame)
local FitFrameVertical = FitFrame.FitFrameVertical
local ButtonStack = require(AppRoot.Button.ButtonStack)
local ModalTitle = require(ModalRoot.ModalTitle)
local ModalWindow = require(ModalRoot.ModalWindow)
local FullPageModal = Roact.PureComponent:extend("FullPageModal")
local MARGIN = 24
local validateProps = t.strictInterface({
screenSize = t.Vector2,
[Roact.Children] = t.table,
position = t.optional(t.UDim2),
title = t.optional(t.string),
buttonStackProps = t.optional(t.table), -- Button stack validates the contents
onCloseClicked = t.optional(t.callback),
})
function FullPageModal:init()
self.state = {
buttonFrameSize = Vector2.new(0, 0),
}
self.changeButtonFrameSize = function(rbx)
if self.state.buttonFrameSize ~= rbx.AbsoluteSize then
self:setState({
buttonFrameSize = rbx.AbsoluteSize
})
end
end
end
function FullPageModal:render()
assert(validateProps(self.props))
return Roact.createElement(ModalWindow, {
isFullHeight = true,
screenSize = self.props.screenSize,
position = self.props.position,
}, {
TitleContainer = Roact.createElement(ModalTitle, {
title = self.props.title,
onCloseClicked = self.props.onCloseClicked,
}),
Content = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, -ModalTitle:GetHeight()),
Position = UDim2.new(0, 0, 0, ModalTitle:GetHeight()),
BackgroundTransparency = 1,
}, {
Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, MARGIN),
PaddingRight = UDim.new(0, MARGIN),
PaddingBottom = UDim.new(0, MARGIN),
}),
MiddleContent = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, -self.state.buttonFrameSize.Y),
BackgroundTransparency = 1,
LayoutOrder = 1
}, self.props[Roact.Children]),
Buttons = self.props.buttonStackProps and Roact.createElement(FitFrameVertical, {
BackgroundTransparency = 1,
width = UDim.new(1, 0),
LayoutOrder = 2,
[Roact.Change.AbsoluteSize] = self.changeButtonFrameSize,
}, {
Roact.createElement(ButtonStack, self.props.buttonStackProps),
}),
})
})
end
return FullPageModal
@@ -0,0 +1,56 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ButtonType = require(AppRoot.Button.Enum.ButtonType)
local FullPageModal = require(script.Parent.FullPageModal)
return function()
describe("lifecycle", function()
it("should mount and unmount FullPageModal without issue", function()
local element = mockStyleComponent({
FullPageModalContainer = Roact.createElement(FullPageModal, {
position = UDim2.new(0, 0, 0, 0),
title = "Title",
screenSize = Vector2.new(1920, 1080),
buttonStackProps = {
buttons = {
{
props = {
isDisabled = false,
onActivated = function() print("Cancel button was clicked") end,
text = "Cancel",
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
isDisabled = false,
onActivated = function() print("Confirm button was clicked") end,
text = "Confirm",
},
},
},
},
onCloseClicked = function() print("Close button was clicked") end,
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
Size = UDim2.new(1, 0, 0, 60),
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,126 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local CoreRoot = UIBlox.Core
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local Images = require(AppRoot.ImageSet.Images)
local ImageSetComponent = require(CoreRoot.ImageSet.ImageSetComponent)
local Controllable = require(CoreRoot.Control.Controllable)
local GenericTextLabel = require(CoreRoot.Text.GenericTextLabel.GenericTextLabel)
local withStyle = require(UIBlox.Core.Style.withStyle)
local X_BUTTON_SIZE = 36
local X_LEFT_PADDING = 6
local X_IMAGE = "icons/navigation/close"
local TITLE_HEIGHT = 48
local TITLE_MAX_HEIGHT_WITH_IMAGE = 261
local ModalTitle = Roact.PureComponent:extend("ModalTitle")
local validateProps = t.strictInterface({
title = t.string,
position = t.optional(t.UDim2),
anchor = t.optional(t.Vector2),
onCloseClicked = t.optional(t.callback),
titleBackgroundImageProps = t.optional(t.strictInterface({
image = t.string,
imageHeight = t.number,
})),
})
ModalTitle.defaultProps = {
title = "",
position = UDim2.new(0.5, 0, 0, 0),
anchor = Vector2.new(0.5, 0),
}
function ModalTitle:GetHeight()
return TITLE_HEIGHT
end
function ModalTitle:render()
assert(validateProps(self.props))
local titleBackground = self.props.titleBackgroundImageProps
return withStyle(function(stylePalette)
local font = stylePalette.Font
local theme = stylePalette.Theme
local headerSize = font.BaseSize * font.Header1.RelativeSize
local titleText = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, TITLE_HEIGHT),
BackgroundTransparency = 1,
}, {
CloseButton = Roact.createElement(Controllable, {
controlComponent = {
component = ImageSetComponent.Button,
props = {
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, TITLE_HEIGHT * 0.5 + X_LEFT_PADDING, 0.5, 0),
Size = UDim2.new(0, TITLE_HEIGHT, 0, TITLE_HEIGHT),
[Roact.Event.Activated] = self.props.onCloseClicked,
},
children = {
InputFillImage = Roact.createElement(ImageSetComponent.Label, {
BackgroundTransparency = 1,
Size = UDim2.new(0, X_BUTTON_SIZE, 0, X_BUTTON_SIZE),
Image = Images[X_IMAGE],
ImageColor3 = theme.IconEmphasis.Color,
ImageTransparency = theme.IconEmphasis.Transparency,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
})
}
},
onStateChanged = function(...) end,
}),
Title = Roact.createElement(GenericTextLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
colorStyle = theme.TextEmphasis,
fontStyle = font.Header1,
LayoutOrder = 1,
Text = self.props.title,
TextSize = headerSize,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
Underline = not titleBackground and Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 1, 0),
BorderSizePixel = 0,
BackgroundColor3 = theme.Divider.Color,
BackgroundTransparency = theme.Divider.Transparency,
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 1),
}),
})
if titleBackground then
local bgHeight = titleBackground.imageHeight
local height = math.min(math.max(TITLE_HEIGHT, bgHeight), TITLE_MAX_HEIGHT_WITH_IMAGE)
return Roact.createElement(ImageSetComponent.Label, {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(1, 0, 0, height),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundTransparency = 1,
ScaleType = Enum.ScaleType.Crop,
BorderSizePixel = 0,
Image = titleBackground.image,
ImageColor3 = Color3.new(255, 255, 255),
}, {
TitleText = titleText,
})
else
return titleText
end
end)
end
return ModalTitle
@@ -0,0 +1,51 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ModalTitle = require(script.Parent.ModalTitle)
return function()
describe("lifecycle", function()
it("should mount and unmount ModalTitle without issue", function()
local element = mockStyleComponent({
ModalTitleContainer = Roact.createElement(ModalTitle, {
title = "Title",
position = UDim2.new(0, 0, 0, 0),
anchor = Vector2.new(0, 0),
onCloseClicked = function() end,
titleBackgroundImageProps = {
image = "rbxassetid://2610133241",
imageHeight = 200,
},
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should throw on invalid props", function()
local element = mockStyleComponent({
ModalTitleContainer = Roact.createElement(ModalTitle, {
title = "Title",
position = UDim2.new(0, 0, 0, 0),
anchor = Vector2.new(0, 0),
onCloseClicked = function() end,
titleBackgroundImageProps = {
image = "rbxassetid://2610133241",
},
})
})
expect(function()
Roact.mount(element)
end).to.throw()
end)
end)
end
@@ -0,0 +1,125 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local FitFrame = require(Packages.FitFrame)
local FitFrameVertical = FitFrame.FitFrameVertical
local Images = require(AppRoot.ImageSet.Images)
local withStyle = require(UIBlox.Core.Style.withStyle)
local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent)
local SLICE_CENTER = Rect.new(8, 8, 9, 9)
local ANCHORED_BACKGROUND_IMAGE = "component_assets/bullet_17"
local FLOATING_BACKGROUND_IMAGE = "component_assets/circle_17"
local ModalWindow = Roact.PureComponent:extend("ModalWindow")
local MAX_WIDTH = 540
local validateProps = t.strictInterface({
isFullHeight = t.boolean,
screenSize = t.Vector2,
[Roact.Children] = t.table,
position = t.optional(t.UDim2),
anchorPoint = t.optional(t.Vector2),
})
function ModalWindow:init()
self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0))
end
-- Used to determine width of middle content for dynamically sizing children, see PartialPageModal
function ModalWindow:getWidth(screenWidth)
return self:isFullWidth(screenWidth) and screenWidth or MAX_WIDTH
end
-- Used to determine if the modal is anchored in the middle or bottom of the screen
function ModalWindow:isFullWidth(screenWidth)
return screenWidth < MAX_WIDTH
end
function ModalWindow:render()
assert(validateProps(self.props))
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
local screenSize = self.props.screenSize
local anchorPoint, backgroundImage, position, width
width = UDim.new(0, self:getWidth(screenSize.X))
if self:isFullWidth(screenSize.X) then
anchorPoint = Vector2.new(0.5, 1)
backgroundImage = ANCHORED_BACKGROUND_IMAGE
position = self.props.position or UDim2.new(0.5, 0, 1, 0)
else
anchorPoint = Vector2.new(0.5, 0.5)
backgroundImage = FLOATING_BACKGROUND_IMAGE
position = self.props.position or UDim2.new(0.5, 0, 0.5, 0)
end
anchorPoint = self.props.anchorPoint or anchorPoint
if self.props.isFullHeight then
return Roact.createElement(ImageSetComponent.Button, {
Position = position,
Size = UDim2.new(width, UDim.new(1, 0)),
AnchorPoint = anchorPoint,
BackgroundTransparency = 0,
BorderSizePixel = 0,
Image = Images[backgroundImage],
ImageColor3 = theme.BackgroundUIDefault.Color,
ImageTransparency = theme.BackgroundUIDefault.Transparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = SLICE_CENTER,
AutoButtonColor = false,
ClipsDescendants = true,
Selectable = false,
}, {
BackgroundImage = Roact.createElement(ImageSetComponent.Label, {
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
}, self.props[Roact.Children])
})
else
return Roact.createElement(ImageSetComponent.Button, {
Position = position,
Size = self.contentSize:map(function(absoluteSize)
return UDim2.new(0, absoluteSize.X, 0, absoluteSize.Y)
end),
AnchorPoint = anchorPoint,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Image = Images[backgroundImage],
ImageColor3 = theme.BackgroundUIDefault.Color,
ImageTransparency = theme.BackgroundUIDefault.Transparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = SLICE_CENTER,
AutoButtonColor = false,
ClipsDescendants = true,
Selectable = false,
}, {
BackgroundImage = Roact.createElement(FitFrameVertical, {
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
BorderSizePixel = 0,
width = width,
[Roact.Change.AbsoluteSize] = function(rbx)
self.changeContentSize(rbx.AbsoluteSize)
end,
}, self.props[Roact.Children])
})
end
end)
end
return ModalWindow
@@ -0,0 +1,122 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ModalWindow = require(script.Parent.ModalWindow)
return function()
describe("lifecycle", function()
it("should mount and unmount full height and large width ModalWindow without issue", function()
local element = mockStyleComponent({
PartialPageModalContainer = Roact.createElement(ModalWindow, {
isFullHeight = true,
screenSize = Vector2.new(1920, 1080),
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
Size = UDim2.new(1, 0, 1, 0),
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should mount and unmount full height and small width ModalWindow without issue", function()
local element = mockStyleComponent({
PartialPageModalContainer = Roact.createElement(ModalWindow, {
isFullHeight = true,
screenSize = Vector2.new(400, 1080),
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
Size = UDim2.new(1, 0, 1, 0),
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should mount and unmount partial height and large width ModalWindow without issue", function()
local element = mockStyleComponent({
PartialPageModalContainer = Roact.createElement(ModalWindow, {
isFullHeight = false,
screenSize = Vector2.new(1920, 1080),
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
Size = UDim2.new(1, 0, 1, 0),
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should mount and unmount partial height and small width ModalWindow without issue", function()
local element = mockStyleComponent({
PartialPageModalContainer = Roact.createElement(ModalWindow, {
isFullHeight = false,
screenSize = Vector2.new(400, 1080),
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
Size = UDim2.new(1, 0, 1, 0),
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should mount and unmount arbitrary anchorPoints without issue", function()
local element = mockStyleComponent({
AnchoredPageModalContainer = Roact.createElement(ModalWindow, {
anchorPoint = Vector2.new(0.25, 0.75),
isFullHeight = true,
screenSize = Vector2.new(1920, 1080),
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
Size = UDim2.new(1, 0, 1, 0),
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should return correct isFullWidth when screen size is larger than modal max width", function()
assert(ModalWindow:isFullWidth(800) == false)
end)
it("should return correct isFullWidth when screen size is smaller than modal max width", function()
assert(ModalWindow:isFullWidth(400) == true)
end)
it("should return correct getWidth when screen size is larger than modal max width", function()
assert(ModalWindow:getWidth(800) == 540)
end)
it("should return correct getWidth when screen size is smaller than modal max width", function()
assert(ModalWindow:getWidth(400) == 400)
end)
end)
end
@@ -0,0 +1,93 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local ButtonStack = require(AppRoot.Button.ButtonStack)
local FitFrame = require(Packages.FitFrame)
local FitFrameVertical = FitFrame.FitFrameVertical
local ModalTitle = require(ModalRoot.ModalTitle)
local ModalWindow = require(ModalRoot.ModalWindow)
local UIBloxConfig = require(UIBlox.UIBloxConfig)
local PartialPageModal = Roact.PureComponent:extend("PartialPageModal")
local MARGIN = 24
local validateProps = t.strictInterface({
screenSize = t.Vector2,
[Roact.Children] = t.table,
position = t.optional(t.UDim2),
anchorPoint = t.optional(t.Vector2),
title = t.optional(t.string),
titleBackgroundImageProps = t.optional(t.strictInterface({
image = t.string,
imageHeight = t.number,
})),
bottomPadding = t.optional(t.number),
buttonStackProps = t.optional(t.table), -- Button stack validates the contents
onCloseClicked = t.optional(t.callback),
contentPadding = t.optional(t.UDim),
})
-- Used to determine width of middle content for dynamically sizing children in the content
-- Example: Multi-lined text that requires to know the width of its space that can also dynamically change its height.
function PartialPageModal:getMiddleContentWidth(screenWidth)
return ModalWindow:getWidth(screenWidth) - 2 * MARGIN
end
function PartialPageModal:render()
assert(validateProps(self.props))
local screenSize = self.props.screenSize
local bottomPadding = self.props.bottomPadding or MARGIN
-- Only add bottom padding when window is anchored to the bottom
-- Used to align buttons with previous UI
if not ModalWindow:isFullWidth(screenSize.X) then
bottomPadding = MARGIN
end
return Roact.createElement(ModalWindow, {
isFullHeight = false,
screenSize = screenSize,
position = self.props.position,
anchorPoint = self.props.anchorPoint,
}, {
TitleContainer = Roact.createElement(ModalTitle, {
title = self.props.title,
titleBackgroundImageProps = self.props.titleBackgroundImageProps,
onCloseClicked = self.props.onCloseClicked,
}),
Content = Roact.createElement(FitFrameVertical, {
Position = UDim2.new(0, 0, 0, ModalTitle.TITLE_HEIGHT),
width = UDim.new(1, 0),
margin = {
top = 0,
bottom = bottomPadding,
left = MARGIN,
right = MARGIN,
},
BackgroundTransparency = 1,
contentPadding = UIBloxConfig.enableExperimentalGamepadSupport and self.props.contentPadding or nil,
}, {
MiddlContent = Roact.createElement(FitFrameVertical, {
width = UDim.new(1, 0),
BackgroundTransparency = 1,
}, self.props[Roact.Children]),
Buttons = self.props.buttonStackProps and Roact.createElement(ButtonStack, self.props.buttonStackProps),
})
})
end
return PartialPageModal
@@ -0,0 +1,88 @@
local ModalRoot = script.Parent
local DialogRoot = ModalRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local ButtonType = require(AppRoot.Button.Enum.ButtonType)
local PartialPageModal = require(script.Parent.PartialPageModal)
return function()
describe("lifecycle", function()
it("should mount and unmount PartialPageModal without issue", function()
local element = mockStyleComponent({
PartialPageModalContainer = Roact.createElement(PartialPageModal, {
position = UDim2.new(0, 0, 0, 0),
anchorPoint = Vector2.new(0, 0.5),
title = "Title",
titleBackgroundImageProps = {
image = "rbxassetid://2610133241",
imageHeight = 200,
},
screenSize = Vector2.new(1920, 1080),
bottomPadding = 100,
buttonStackProps = {
buttons = {
{
props = {
isDisabled = false,
onActivated = function() print("Cancel button was clicked") end,
text = "Cancel",
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
isDisabled = false,
onActivated = function() print("Confirm button was clicked") end,
text = "Confirm",
},
},
},
},
onCloseClicked = function() print("Close button was clicked") end,
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromRGB(164, 86, 78),
Size = UDim2.new(1, 0, 1, 0),
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should throw on invalid props", function()
local element = mockStyleComponent({
ModalTitleContainer = Roact.createElement(PartialPageModal, {
title = "Title",
titleBackgroundImageProps = {
image = "rbxassetid://2610133241",
},
screenSize = Vector2.new(1920, 1080),
})
})
expect(function()
Roact.mount(element)
end).to.throw()
end)
it("should return correct getMiddleContentWidth when screen size is larger than modal max width", function()
-- modal width - left margin - right margin
assert(PartialPageModal:getMiddleContentWidth(800) == 540 - 2 * 24)
end)
it("should return correct getMiddleContentWidth when screen size is smaller than modal max width", function()
-- modal width - left margin - right margin
assert(PartialPageModal:getMiddleContentWidth(400) == 400 - 2 * 24)
end)
end)
end
@@ -0,0 +1,111 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView)
local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem)
local ModalsRoot = script.Parent.Parent
local DialogRoot = ModalsRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBlox = AppRoot.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local ModalWindow = require(ModalsRoot.ModalWindow)
local PortraitModal = Roact.PureComponent:extend("PortraitModal")
function PortraitModal:init()
self.screenSize = nil
self.screenRef = Roact.createRef()
self.state = {
screenSize = Vector2.new(0, 0),
isFullHeight = false
}
self.changeScreenSize = function(rbx)
if self.state.screenSize ~= rbx.AbsoluteSize then
self:setState({
screenSize = rbx.AbsoluteSize
})
end
end
self.toggleisFullHeight = function()
self:setState({
isFullHeight = not self.state.isFullHeight
})
end
end
function PortraitModal: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,
}),
ButtonControlsFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 50),
LayoutOrder = 1,
}, {
Grid = Roact.createElement("UIGridLayout", {
CellSize = UDim2.new(0, 200, 0, 45),
SortOrder = Enum.SortOrder.LayoutOrder,
}),
DisableControl = Roact.createElement("TextButton", {
Text = self.state.isFullHeight and "Fit" or "Full Height",
[Roact.Event.Activated] = self.toggleisFullHeight,
LayoutOrder = 1,
}),
}),
Overview = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, -50),
BackgroundTransparency = 1,
LayoutOrder = 2,
}, {
Roact.createElement(StoryItem, {
size = UDim2.new(1, 0, 1, 0),
title = "ModalWindowContainer",
subTitle = "Expand and shrink the width of the window to see how the modal behaves on different widths",
}, {
ViewFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
[Roact.Ref] = self.screenRef,
[Roact.Change.AbsoluteSize] = self.changeScreenSize,
} , {
ModalWindowContainer = Roact.createElement(ModalWindow, {
isFullHeight = self.state.isFullHeight,
screenSize = self.state.screenSize,
}, {
Custom = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 60),
},{
CustomInner = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
LayoutOrder = 3,
Text = "Put any component you want here.",
TextSize = 13,
TextWrapped = true,
Size = UDim2.new(1, 0, 1, 0),
}),
}),
})
})
})
})
})
end
return function(target)
local handle = Roact.mount(Roact.createElement(StoryView, {}, {
Story = Roact.createElement(PortraitModal),
}), target, "PortraitModal")
return function()
Roact.unmount(handle)
end
end
@@ -0,0 +1,6 @@
return {
Appearing = "Appearing",
Appeared = "Appeared",
Disappearing = "Disappearing",
Disappeared = "Disappeared",
}
@@ -0,0 +1,66 @@
local ToastRoot = script.Parent
local DialogRoot = ToastRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBloxRoot = AppRoot.Parent
local Packages = UIBloxRoot.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local withStyle = require(UIBloxRoot.Core.Style.withStyle)
local ToastFrame = require(ToastRoot.ToastFrame)
local validateToastIcon = require(ToastRoot.Validator.validateToastIcon)
local validateToastText = require(ToastRoot.Validator.validateToastText)
local InformativeToast = Roact.PureComponent:extend("InformativeToast")
local validateProps = t.strictInterface({
anchorPoint = t.optional(t.Vector2),
iconProps = t.optional(validateToastIcon),
iconChildren = t.optional(t.table),
layoutOrder = t.optional(t.integer),
padding = t.optional(t.numberMin(0)),
position = t.optional(t.UDim2),
size = t.UDim2,
subtitleTextProps = t.optional(validateToastText),
textFrameSize = t.optional(t.UDim2),
titleTextProps = validateToastText,
})
InformativeToast.defaultProps = {
anchorPoint = Vector2.new(0.5, 0.5),
layoutOrder = 1,
position = UDim2.new(0.5, 0, 0.5, 0),
size = UDim2.new(1, 0, 1, 0),
}
function InformativeToast:render()
assert(validateProps(self.props))
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
return Roact.createElement("Frame", {
AnchorPoint = self.props.anchorPoint,
BackgroundColor3 = theme.BackgroundUIContrast.Color,
BackgroundTransparency = theme.BackgroundUIContrast.Transparency,
BorderSizePixel = 0,
ClipsDescendants = true,
LayoutOrder = self.props.layoutOrder,
Position = self.props.position,
Size = self.props.size,
}, {
ToastFrame = Roact.createElement(ToastFrame, {
iconProps = self.props.iconProps,
iconChildren = self.props.iconChildren,
padding = self.props.padding,
subtitleTextProps = self.props.subtitleTextProps,
textFrameSize = self.props.textFrameSize,
titleTextProps = self.props.titleTextProps,
}),
})
end)
end
return InformativeToast
@@ -0,0 +1,125 @@
return function()
local Toast = script.Parent
local Dialog = Toast.Parent
local App = Dialog.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(UIBlox.App.ImageSet.Images)
local TestStyle = require(UIBlox.App.Style.Validator.TestStyle)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local InformativeToast = require(Toast.InformativeToast)
local ICON_SIZE = 36
local testText = "Item On Sale"
local testSubText = "test test test"
local createInformativeToast = function(props)
return mockStyleComponent({
InformativeToast = Roact.createElement(InformativeToast, props)
})
end
it("should throw on invalid titleTextProps", function()
local element = createInformativeToast({
toastText = {},
})
expect(function()
Roact.mount(element)
end).to.throw()
end)
it("should create and destroy without errors with valid titleTextProps", function()
local element = createInformativeToast({
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, 0, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function()
local element = createInformativeToast({
subtitleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.CaptionBody,
Size = UDim2.new(1, 0, 0.5, 0),
Text = testSubText,
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, 0, 0.5, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with icon", function()
local element = createInformativeToast({
iconProps = {
Image = "rbxassetid://4126499279",
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, -ICON_SIZE, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with ImageSet compatible icon", function()
local element = createInformativeToast({
iconProps = {
Image = Images["icons/status/warning"],
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, -ICON_SIZE, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with composed image", function()
local element = createInformativeToast({
iconProps = {
Image = Images["icons/status/warning"],
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
},
iconChildren = {
Child = Roact.createElement("TextLabel"),
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, -ICON_SIZE, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,106 @@
local ToastRoot = script.Parent
local DialogRoot = ToastRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBloxRoot = AppRoot.Parent
local Packages = UIBloxRoot.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent)
local Images = require(UIBloxRoot.App.ImageSet.Images)
local withStyle = require(UIBloxRoot.Core.Style.withStyle)
local SpringAnimatedItem = require(UIBloxRoot.Utility.SpringAnimatedItem)
local ToastFrame = require(ToastRoot.ToastFrame)
local validateToastIcon = require(ToastRoot.Validator.validateToastIcon)
local validateToastText = require(ToastRoot.Validator.validateToastText)
local ANIMATION_SPRING_SETTINGS = {
dampingRatio = 1,
frequency = 4,
}
local PRESSED_SCALE = 0.95
local TOAST_BACKGROUND_IMAGE = Images["component_assets/circle_21"]
local TOAST_BORDER_IMAGE = Images["component_assets/circle_21_stroke_1"]
local TOAST_SLICE_CENTER = Rect.new(10, 10, 11, 11)
local InteractiveToast = Roact.PureComponent:extend("InteractiveToast")
local validateProps = t.strictInterface({
anchorPoint = t.optional(t.Vector2),
iconProps = t.optional(validateToastIcon),
iconChildren = t.optional(t.table),
layoutOrder = t.optional(t.integer),
padding = t.optional(t.numberMin(0)),
position = t.optional(t.UDim2),
pressed = t.optional(t.boolean),
pressedScale = t.number,
size = t.UDim2,
subtitleTextProps = t.optional(validateToastText),
textFrameSize = t.optional(t.UDim2),
titleTextProps = validateToastText,
})
InteractiveToast.defaultProps = {
anchorPoint = Vector2.new(0.5, 0.5),
layoutOrder = 1,
position = UDim2.new(0.5, 0, 0.5, 0),
pressedScale = PRESSED_SCALE,
size = UDim2.new(1, 0, 1, 0),
}
function InteractiveToast:render()
assert(validateProps(self.props))
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
return Roact.createElement(ImageSetComponent.Label, {
AnchorPoint = self.props.anchorPoint,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Image = TOAST_BACKGROUND_IMAGE,
ImageColor3 = theme.SystemPrimaryContent.Color,
ImageTransparency = theme.SystemPrimaryContent.Transparency,
LayoutOrder = self.props.layoutOrder,
Position = self.props.position,
ScaleType = Enum.ScaleType.Slice,
Size = self.props.size,
SliceCenter = TOAST_SLICE_CENTER,
}, {
Scaler = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, {
springOptions = ANIMATION_SPRING_SETTINGS,
animatedValues = {
scale = self.props.pressed and self.props.pressedScale or 1,
},
mapValuesToProps = function(values)
return {
Scale = values.scale,
}
end,
}),
ToastBorder = Roact.createElement(ImageSetComponent.Label, {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = TOAST_BORDER_IMAGE,
ImageColor3 = theme.TextDefault.Color,
ImageTransparency = theme.TextDefault.Transparency,
Position = UDim2.new(0.5, 0, 0.5, 0),
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.new(1, 0, 1, 0),
SliceCenter = TOAST_SLICE_CENTER,
}),
ToastFrame = Roact.createElement(ToastFrame, {
iconProps = self.props.iconProps,
iconChildren = self.props.iconChildren,
padding = self.props.padding,
subtitleTextProps = self.props.subtitleTextProps,
textFrameSize = self.props.textFrameSize,
titleTextProps = self.props.titleTextProps,
}),
})
end)
end
return InteractiveToast
@@ -0,0 +1,147 @@
return function()
local Toast = script.Parent
local Dialog = Toast.Parent
local App = Dialog.Parent
local UIBlox = App.Parent
local Packages = UIBlox.Parent
local Roact = require(Packages.Roact)
local Images = require(UIBlox.App.ImageSet.Images)
local TestStyle = require(UIBlox.App.Style.Validator.TestStyle)
local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent)
local InteractiveToast = require(Toast.InteractiveToast)
local ICON_SIZE = 36
local testText = "System Outage"
local testSubText = "Tap to see more information"
local createInteractiveToast = function(props)
return mockStyleComponent({
InteractiveToast = Roact.createElement(InteractiveToast, props)
})
end
it("should throw on invalid titleTextProps", function()
local element = createInteractiveToast({
textFrameSize = UDim2.new(1, 0, 1, 0),
titleTextProps = {},
})
expect(function()
Roact.mount(element)
end).to.throw()
end)
it("should create and destroy without errors with valid titleTextProps", function()
local element = createInteractiveToast({
textFrameSize = UDim2.new(1, 0, 1, 0),
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, 0, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function()
local element = createInteractiveToast({
textFrameSize = UDim2.new(1, 0, 1, 0),
subtitleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.CaptionBody,
Size = UDim2.new(1, 0, 0.5, 0),
Text = testSubText,
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, 0, 0.5, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with icon", function()
local element = createInteractiveToast({
textFrameSize = UDim2.new(1, 0, 1, 0),
iconProps = {
Image = "rbxassetid://4126499279",
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, -ICON_SIZE, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with ImageSet compatible icon", function()
local element = createInteractiveToast({
textFrameSize = UDim2.new(1, 0, 1, 0),
iconProps = {
Image = Images["icons/status/warning"],
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, -ICON_SIZE, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with composed image", function()
local element = createInteractiveToast({
textFrameSize = UDim2.new(1, 0, 1, 0),
iconProps = {
Image = Images["icons/status/warning"],
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
},
iconChildren = {
Child = Roact.createElement("TextLabel"),
},
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, -ICON_SIZE, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors when pressed", function()
local element = createInteractiveToast({
pressed = true,
textFrameSize = UDim2.new(1, 0, 1, 0),
titleTextProps = {
colorStyle = TestStyle.Theme.TextEmphasis,
fontStyle = TestStyle.Font.Header2,
Size = UDim2.new(1, -ICON_SIZE, 1, 0),
Text = testText,
},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,245 @@
local ToastRoot = script.Parent
local DialogRoot = ToastRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBloxRoot = AppRoot.Parent
local Packages = UIBloxRoot.Parent
local Roact = require(Packages.Roact)
local t = require(Packages.t)
local SlidingDirection = require(UIBloxRoot.Core.Animation.Enum.SlidingDirection)
local SlidingContainer = require(UIBloxRoot.Core.Animation.SlidingContainer)
local StateTable = require(UIBloxRoot.StateTable.StateTable)
local AnimationState = require(ToastRoot.Enum.AnimationState)
local InformativeToast = require(ToastRoot.InformativeToast)
local InteractiveToast = require(ToastRoot.InteractiveToast)
local ToastContainer = require(ToastRoot.ToastContainer)
local validateToastContent = require(ToastRoot.Validator.validateToastContent)
local SlideFromTopToast = Roact.PureComponent:extend("SlideFromTopToast")
local validateProps = t.strictInterface({
anchorPoint = t.optional(t.Vector2),
duration = t.optional(t.number),
layoutOrder = t.optional(t.integer),
position = t.optional(t.UDim2),
show = t.optional(t.boolean),
size = t.optional(t.UDim2),
springOptions = t.optional(t.table),
toastContent = validateToastContent,
})
SlideFromTopToast.defaultProps = {
anchorPoint = Vector2.new(0.5, 0),
position = UDim2.new(0.5, 0, 0, 20),
show = true,
}
local function toastContentEqual(toastContent1, toastContent2)
if toastContent1.iconColorStyle ~= toastContent2.iconColorStyle
or toastContent1.iconImage ~= toastContent2.iconImage
or toastContent1.iconSize ~= toastContent2.iconSize
or toastContent1.iconChildren ~= toastContent2.iconChildren
or toastContent1.onActivated ~= toastContent2.onActivated
or toastContent1.onDismissed ~= toastContent2.onDismissed
or toastContent1.swipeUpDismiss ~= toastContent2.swipeUpDismiss
or toastContent1.toastSubtitle ~= toastContent2.toastSubtitle
or toastContent1.toastTitle ~= toastContent2.toastTitle then
return false
end
return true
end
function SlideFromTopToast:init()
self.isMounted = false
self.currentToastContent = self.props.toastContent
self.onActivated = function()
self.stateTable.events.Activated({
activated = true,
})
end
self.onAppeared = function()
if self.currentToastContent.onAppeared then
self.currentToastContent.onAppeared()
end
local duration = self.props.duration
if duration and duration > 0 then
local currentToastContent = self.currentToastContent
delay(duration, function()
if currentToastContent == self.currentToastContent then
self.stateTable.events.AutoDismiss()
end
end)
end
end
self.onComplete = function()
local duration = self.props.duration
if self.state.currentState == AnimationState.Appearing and duration and duration <= 0 then
self.stateTable.events.AutoDismiss()
else
self.stateTable.events.AnimationComplete()
end
end
self.onDisappeared = function()
if self.state.context.activated then
if self.currentToastContent.onActivated then
self.currentToastContent.onActivated()
end
else
if self.currentToastContent.onDismissed then
self.currentToastContent.onDismissed()
end
end
end
self.onTouchSwipe = function(_, swipeDir)
if swipeDir == Enum.SwipeDirection.Up then
self.stateTable.events.ForceDismiss()
end
end
self.renderInteractiveToast = function(props)
return Roact.createElement(InteractiveToast, props)
end
self.renderInformativeToast = function(props)
return Roact.createElement(InformativeToast, props)
end
self.setContext = function(_, _, data)
return data
end
self.updateToastContent = function()
if self.currentToastContent ~= self.props.toastContent then
self.currentToastContent = self.props.toastContent
if self.props.show then
-- Show next toast content
self.stateTable.events.ForceAppear({
activated = false,
})
end
end
end
local initialState = AnimationState.Disappeared
self.state = {
currentState = initialState,
context = {
activated = false,
},
}
local stateTableName = string.format("Animated(%s)", tostring(self))
self.stateTable = StateTable.new(stateTableName, initialState, {}, {
[AnimationState.Appearing] = {
AnimationComplete = { nextState = AnimationState.Appeared, action = self.onAppeared },
AutoDismiss = { nextState = AnimationState.Disappearing, action = self.onAppeared },
ContentChanged = { nextState = AnimationState.Disappearing },
ForceDismiss = { nextState = AnimationState.Disappearing },
},
[AnimationState.Appeared] = {
Activated = { nextState = AnimationState.Disappearing, action = self.setContext },
AutoDismiss = { nextState = AnimationState.Disappearing },
ContentChanged = { nextState = AnimationState.Disappearing },
ForceDismiss = { nextState = AnimationState.Disappearing },
},
[AnimationState.Disappearing] = {
AnimationComplete = { nextState = AnimationState.Disappeared, action = self.onDisappeared },
},
[AnimationState.Disappeared] = {
ContentChanged = { nextState = AnimationState.Appearing, action = self.updateToastContent },
ForceAppear = { nextState = AnimationState.Appearing, action = self.setContext },
},
})
self.stateTable:onStateChange(function(oldState, newState, updatedContext)
if self.isMounted and oldState ~= newState then
self:setState({
currentState = newState,
context = updatedContext,
})
end
end)
end
function SlideFromTopToast:isShowing()
return self.state.currentState == AnimationState.Appearing or self.state.currentState == AnimationState.Appeared
end
function SlideFromTopToast:render()
assert(validateProps(self.props))
local onActivated = self.currentToastContent.onActivated
local swipeUpDismiss = self.currentToastContent.swipeUpDismiss
if swipeUpDismiss == nil then
swipeUpDismiss = true
end
return Roact.createElement(SlidingContainer, {
show = self:isShowing(),
layoutOrder = self.props.layoutOrder,
onComplete = self.onComplete,
slidingDirection = SlidingDirection.Down,
springOptions = self.props.springOptions,
}, {
ToastContainer = Roact.createElement(ToastContainer, {
anchorPoint = self.props.anchorPoint,
position = self.props.position,
size = self.props.size,
-- Toast content props
iconColorStyle = self.currentToastContent.iconColorStyle,
iconImage = self.currentToastContent.iconImage,
iconSize = self.currentToastContent.iconSize,
iconChildren = self.currentToastContent.iconChildren,
onActivated = onActivated and self.onActivated,
onTouchSwipe = swipeUpDismiss and self.onTouchSwipe,
renderToast = onActivated and self.renderInteractiveToast or self.renderInformativeToast,
toastSubtitle = self.currentToastContent.toastSubtitle,
toastTitle = self.currentToastContent.toastTitle,
}),
})
end
function SlideFromTopToast:didMount()
self.isMounted = true
if self.props.show then
self.stateTable.events.ForceAppear({
activated = false,
})
end
end
function SlideFromTopToast:willUnmount()
self.isMounted = false
end
function SlideFromTopToast:didUpdate(oldProps, oldState)
if oldProps.show ~= self.props.show then
if self.props.show then
self.stateTable.events.ForceAppear({
activated = false,
})
else
self.stateTable.events.ForceDismiss()
end
end
if not toastContentEqual(oldProps.toastContent, self.props.toastContent) then
-- Toast content updated, need to force dismiss current toast and show the new one
self.stateTable.events.ContentChanged({
activated = false,
})
end
if oldState.currentState ~= self.state.currentState and
self.state.currentState == AnimationState.Disappeared then
self.updateToastContent()
end
end
return SlideFromTopToast

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