Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modals #251

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Common/ContextProviders.luau
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local React = require("@pkg/React")

local ContextStack = require("@root/Common/ContextStack")
local ModalContext = require("@root/Modal/ModalContext")
local NavigationContext = require("@root/Navigation/NavigationContext")
local PluginContext = require("@root/Plugin/PluginContext")
local SettingsContext = require("@root/UserSettings/SettingsContext")
Expand All @@ -20,6 +21,7 @@ local function ContextProviders(props: Props)
defaultScreen = "Home",
}),
React.createElement(SettingsContext.Provider),
React.createElement(ModalContext.Provider),
},
}, props.children)
end
Expand Down
31 changes: 31 additions & 0 deletions src/Common/useTag.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
local CollectionService = game:GetService("CollectionService")

local React = require("@pkg/React")

local useCallback = React.useCallback
local useEffect = React.useEffect
local useState = React.useState

local function useTag(tagName: string): { Instance }
local instances, setInstances = useState({})

local sync = useCallback(function()
setInstances(CollectionService:GetTagged(tagName))
end, { tagName })

useEffect(function()
local added = CollectionService:GetInstanceAddedSignal(tagName):Connect(sync)
local removed = CollectionService:GetInstanceRemovedSignal(tagName):Connect(sync)

sync()

return function()
added:Disconnect()
removed:Disconnect()
end
end, { tagName, sync })

return instances
end

return useTag
44 changes: 44 additions & 0 deletions src/Common/useTag.spec.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
local JestGlobals = require("@pkg/JestGlobals")

local renderHook = require("@root/Testing/renderHook")
local useTag = require("./useTag")

local beforeEach = JestGlobals.beforeEach
local afterEach = JestGlobals.afterEach
local test = JestGlobals.test
local expect = JestGlobals.expect

local container

beforeEach(function()
container = Instance.new("Folder")
container.Parent = game
end)

afterEach(function()
container:Destroy()
end)

test("all tagged instances are used for the initial state", function()
local instance = Instance.new("Part")
instance:SetTag("foo")
instance.Parent = container

local other = Instance.new("Part")
other:SetTag("bar")
other.Parent = container

local get = renderHook(function()
return useTag("foo")
end)

local instances = get()

expect(instances).toEqual({
instance,
})
end)

test("adding the tag to an instance updates state", function() end)

test("removing the tag from an instance updates state", function() end)
103 changes: 103 additions & 0 deletions src/Modal/Modal.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
local React = require("@pkg/React")
local Sift = require("@pkg/Sift")

local useTheme = require("@root/Common/useTheme")

export type Props = {
title: string,
onClose: () -> (),
maxSize: Vector2?,
children: React.Node?,
}

local defaultProps = {
maxSize = Vector2.new(500, math.huge),
}

type InternalProps = typeof(defaultProps) & Props

local function Modal(providedProps: Props)
local props: InternalProps = Sift.Dictionary.join(defaultProps, providedProps)
local theme = useTheme()

return React.createElement("ImageButton", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 0.6,
BackgroundColor3 = Color3.fromRGB(0, 0, 0),
BorderSizePixel = 0,
AutoButtonColor = false,
[React.Event.Activated] = props.onClose,
}, {
Layout = React.createElement("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
}),

Window = React.createElement("ImageButton", {
Active = true,
AutoButtonColor = false,
AutomaticSize = Enum.AutomaticSize.XY,
BackgroundColor3 = theme.modal,
BorderSizePixel = 0,
}, {
SizeConstraint = React.createElement("UISizeConstraint", {
MaxSize = props.maxSize,
}),

Padding = React.createElement("UIPadding", {
PaddingTop = theme.paddingLarge,
PaddingRight = theme.paddingLarge,
PaddingBottom = theme.paddingLarge,
PaddingLeft = theme.paddingLarge,
}),

UICorner = React.createElement("UICorner", {
CornerRadius = theme.corner,
}),

Layout = React.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = theme.paddingLarge,
}),

Header = React.createElement("Frame", {
LayoutOrder = 1,
AutomaticSize = Enum.AutomaticSize.Y,
Size = UDim2.fromScale(1, 0),
BackgroundTransparency = 1,
}, {
Title = React.createElement("TextLabel", {
Text = props.title,
Font = theme.headerFont,
TextColor3 = theme.text,
TextSize = theme.headerTextSize,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
AutomaticSize = Enum.AutomaticSize.XY,
BackgroundTransparency = 1,
}),

Close = React.createElement("TextButton", {
Font = Enum.Font.BuilderSansExtraBold,
TextColor3 = theme.text,
TextSize = theme.headerTextSize,
Text = "X",
AutomaticSize = Enum.AutomaticSize.XY,
Position = UDim2.fromScale(1, 0),
AnchorPoint = Vector2.new(1, 0),
BackgroundTransparency = 1,
[React.Event.Activated] = props.onClose,
}),
}),

Content = React.createElement("Frame", {
LayoutOrder = 2,
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
}, props.children),
}),
})
end

return Modal
55 changes: 55 additions & 0 deletions src/Modal/Modal.story.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
local React = require("@pkg/React")

local ContextProviders = require("@root/Common/ContextProviders")
local MockPlugin = require("@root/Testing/MockPlugin")
local Modal = require("./Modal")
local useTheme = require("@root/Common/useTheme")

local function ExampleContent()
local theme = useTheme()

local paragraphs = {}
for i = 1, 2 do
paragraphs[`Paragraph{i}`] = React.createElement("TextLabel", {
LayoutOrder = i,
AutomaticSize = Enum.AutomaticSize.XY,
BackgroundTransparency = 1,
Text = "Occaecat commodo fugiat fugiat minim adipisicing esse duis anim ea elit enim officia ut. Deserunt exercitation sit nisi ullamco labore in mollit nostrud Lorem. Commodo laboris est qui enim aute ea dolore voluptate dolor nulla. Quis duis ut voluptate magna dolore pariatur mollit in occaecat aute. Adipisicing magna mollit non sit eiusmod reprehenderit id officia officia non dolore aute voluptate ex.",
TextWrapped = true,
Font = theme.font,
TextColor3 = theme.text,
TextSize = theme.textSize,
LineHeight = 1.5,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
})
end

return React.createElement("Frame", {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
}, {
Layout = React.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = theme.padding,
}),
}, paragraphs)
end

return {
story = function()
return React.createElement(ContextProviders, {
plugin = MockPlugin.new(),
}, {
Model = React.createElement(Modal, {
title = "Example Modal",
onClose = function()
print("close")
end,
}, {
Content = React.createElement(ExampleContent),
}),
})
end,
}
40 changes: 40 additions & 0 deletions src/Modal/ModalContext.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
local React = require("@pkg/React")

local useTag = require("@root/Common/useTag")

local useContext = React.useContext

local MODAL_TAG = "modal-root"

local Context = React.createContext({})

export type ModalContext = {
tag: string,
close: () -> (),
open: (element: React.Node) -> (),
}

export type Props = {
children: any,
}

local function Provider(props: Props)
local target = useTag(MODAL_TAG)[1]

return React.createElement(Context.Provider, {
value = {
tag = MODAL_TAG,
target = target,
},
}, props.children)
end

local function use(): ModalContext
return useContext(Context)
end

return {
Context = Context,
Provider = Provider,
use = use,
}
20 changes: 20 additions & 0 deletions src/Modal/ModalPortal.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
local React = require("@pkg/React")
local ReactRoblox = require("@pkg/ReactRoblox")

local ModalContext = require("@root/Modal/ModalContext")

export type Props = {
children: React.Node?,
}

local function ModalPortal(props: Props)
local modalContext = ModalContext.use()

if modalContext.target then
return ReactRoblox.createPortal(props.children, modalContext.target)
else
return nil
end
end

return ModalPortal
13 changes: 13 additions & 0 deletions src/Modal/ModalRoot.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
local React = require("@pkg/React")

local ModalContext = require("@root/Modal/ModalContext")

local function ModalRoot()
local modalContext = ModalContext.use()

return React.createElement("Folder", {
[React.Tag] = modalContext.tag,
})
end

return ModalRoot
Loading
Loading