add gs
This commit is contained in:
@@ -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
|
||||
+314
@@ -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
|
||||
+72
@@ -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
|
||||
+221
@@ -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
|
||||
+33
@@ -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
|
||||
+160
@@ -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
|
||||
+134
@@ -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
|
||||
+91
@@ -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
|
||||
+142
@@ -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
|
||||
+110
@@ -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
|
||||
+264
@@ -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
|
||||
+124
@@ -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
|
||||
+181
@@ -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
|
||||
+111
@@ -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
|
||||
+65
@@ -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
|
||||
+62
@@ -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
|
||||
+142
@@ -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
|
||||
+33
@@ -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
|
||||
+13
@@ -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",
|
||||
})
|
||||
+157
@@ -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
|
||||
+293
@@ -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
|
||||
+65
@@ -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
|
||||
+62
@@ -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
|
||||
+66
@@ -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
|
||||
+62
@@ -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
|
||||
+66
@@ -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
|
||||
+62
@@ -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
|
||||
+145
@@ -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
|
||||
+288
@@ -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
|
||||
+46
@@ -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),
|
||||
})
|
||||
+49
@@ -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),
|
||||
})
|
||||
+94
@@ -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
|
||||
+25
@@ -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
|
||||
+112
@@ -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
|
||||
+47
@@ -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
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
return {
|
||||
Small = 16,
|
||||
Regular = 36,
|
||||
Large = 48,
|
||||
XLarge = 96,
|
||||
XXLarge = 192,
|
||||
}
|
||||
+106
@@ -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
|
||||
+40
@@ -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
|
||||
+96
@@ -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
|
||||
+84
@@ -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
|
||||
+269
@@ -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
|
||||
+93
@@ -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
|
||||
+53
@@ -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
|
||||
+29
@@ -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
|
||||
+64
@@ -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
|
||||
+61
@@ -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
|
||||
+36
@@ -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
|
||||
+110
@@ -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
|
||||
+99
@@ -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
|
||||
+20
@@ -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
|
||||
+21
@@ -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
|
||||
+62
@@ -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
|
||||
+176
@@ -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
|
||||
+57
@@ -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
|
||||
+17
@@ -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
|
||||
+22
@@ -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
|
||||
+6
@@ -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)
|
||||
+191
@@ -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
|
||||
+82
@@ -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"),
|
||||
}
|
||||
+30
@@ -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
|
||||
+269
@@ -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
|
||||
+39
@@ -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
|
||||
+425
@@ -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
|
||||
+42
@@ -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
|
||||
+195
@@ -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
|
||||
+213
@@ -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
|
||||
+61
@@ -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
|
||||
+117
@@ -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
|
||||
+28
@@ -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
|
||||
+13
@@ -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",
|
||||
})
|
||||
+62
@@ -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
|
||||
+27
@@ -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
|
||||
+76
@@ -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
|
||||
+36
@@ -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
|
||||
+79
@@ -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
|
||||
+27
@@ -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
|
||||
+38
@@ -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)),
|
||||
})
|
||||
+126
@@ -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
|
||||
+170
@@ -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
|
||||
+61
@@ -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
|
||||
+92
@@ -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
|
||||
+56
@@ -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
|
||||
+126
@@ -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
|
||||
+51
@@ -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
|
||||
+125
@@ -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
|
||||
+122
@@ -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
|
||||
+93
@@ -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
|
||||
+88
@@ -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
|
||||
+111
@@ -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
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
return {
|
||||
Appearing = "Appearing",
|
||||
Appeared = "Appeared",
|
||||
Disappearing = "Disappearing",
|
||||
Disappeared = "Disappeared",
|
||||
}
|
||||
+66
@@ -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
|
||||
+125
@@ -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
|
||||
+106
@@ -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
|
||||
+147
@@ -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
|
||||
+245
@@ -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
Reference in New Issue
Block a user