From fbf74b346a57c1dbc3f90d208440bc143b4d1170 Mon Sep 17 00:00:00 2001 From: Avafe <65048459+ImAvafe@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:13:56 -0400 Subject: [PATCH] Make a functional dropdown component (help me) --- samples/SettingsMenu/init.luau | 32 +++ src/Components/Dropdown.luau | 56 ----- .../{ => Dropdown}/Dropdown.story.luau | 15 +- src/Components/Dropdown/DropdownHandler.luau | 194 ++++++++++++++++++ src/Components/Dropdown/init.luau | 74 +++++++ wally.toml | 1 + 6 files changed, 312 insertions(+), 60 deletions(-) delete mode 100644 src/Components/Dropdown.luau rename src/Components/{ => Dropdown}/Dropdown.story.luau (59%) create mode 100644 src/Components/Dropdown/DropdownHandler.luau create mode 100644 src/Components/Dropdown/init.luau diff --git a/samples/SettingsMenu/init.luau b/samples/SettingsMenu/init.luau index db6113e..6d205bb 100644 --- a/samples/SettingsMenu/init.luau +++ b/samples/SettingsMenu/init.luau @@ -70,6 +70,38 @@ return function(Scope: Fusion.Scope, Props: Props) PlaceholderText = "Nickname", CharacterLimit = 20, }, + Scope:Dropdown { + Items = { + "R$1,000,000", + "End world hunger", + "Infinite Skibidi Toilet", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + "Another option", + }, + }, }, }, Scope:Button { diff --git a/src/Components/Dropdown.luau b/src/Components/Dropdown.luau deleted file mode 100644 index a97ed59..0000000 --- a/src/Components/Dropdown.luau +++ /dev/null @@ -1,56 +0,0 @@ -local OnyxUI = script.Parent.Parent -local Fusion = require(OnyxUI.Parent.Fusion) -local Util = require(OnyxUI.Util) -local Themer = require(OnyxUI.Themer) - -local OnEvent = Fusion.OnEvent -local Children = Fusion.Children - -local Base = require(OnyxUI.Components.Base) -local Components = { - TextInput = require(OnyxUI.Components.TextInput), - Frame = require(OnyxUI.Components.Frame), -} - -export type Props = Base.Props & { - Open: Fusion.UsedAs?, -} - -return function(Scope: Fusion.Scope, Props: Props) - local Scope = Fusion.innerScope(Scope, Fusion, Components, Util) - local Theme = Themer.Theme:now() - - local Open = Scope:EnsureValue(Util.Fallback(Props.Open, false)) - - return Scope:Hydrate(Scope:TextInput(Util.CombineProps(Props, { - Name = script.Name, - Parent = Props.Parent, - StrokeColor = Theme.Colors.NeutralContent.Dark, - PlaceholderText = "Dropdown", - -- TextEditable = false, - - [Children] = { - Scope:Computed(function(Use) - if Use(Open) then - return Scope:New "Folder" { - [Children] = { - Scope:Frame { - AnchorPoint = Vector2.new(0, 1), - Position = UDim2.fromScale(0, 1), - Size = UDim2.fromScale(1, 1), - BackgroundTransparency = 0, - }, - }, - } - end - end), - }, - }))) { - [OnEvent "Focused"] = function() - Open:set(true) - end, - [OnEvent "FocusLost"] = function() - Open:set(false) - end, - } -end diff --git a/src/Components/Dropdown.story.luau b/src/Components/Dropdown/Dropdown.story.luau similarity index 59% rename from src/Components/Dropdown.story.luau rename to src/Components/Dropdown/Dropdown.story.luau index 0eebea4..492ef54 100644 --- a/src/Components/Dropdown.story.luau +++ b/src/Components/Dropdown/Dropdown.story.luau @@ -1,4 +1,4 @@ -local OnyxUI = script.Parent.Parent +local OnyxUI = script.Parent.Parent.Parent local Fusion = require(OnyxUI.Parent.Fusion) local Util = require(OnyxUI.Util) local Themer = require(OnyxUI.Themer) @@ -6,8 +6,9 @@ local Themer = require(OnyxUI.Themer) local Children = Fusion.Children local Components = { - Dropdown = require(script.Parent.Dropdown), - Frame = require(script.Parent.Frame), + Dropdown = require(script.Parent), + Frame = require(script.Parent.Parent.Frame), + Text = require(script.Parent.Parent.Text), } return { @@ -20,9 +21,15 @@ return { Padding = Scope:Computed(function(Use) return UDim.new(0, Use(Theme.StrokeThickness["1"])) end), + ListEnabled = true, [Children] = { - Scope:Dropdown {}, + Scope:Dropdown { + Items = { "R$1,000,000", "End world hunger", "Infinite Skibidi Toilet" }, + }, + Scope:Text { + Text = "This text should not move.", + }, }, } diff --git a/src/Components/Dropdown/DropdownHandler.luau b/src/Components/Dropdown/DropdownHandler.luau new file mode 100644 index 0000000..d078e15 --- /dev/null +++ b/src/Components/Dropdown/DropdownHandler.luau @@ -0,0 +1,194 @@ +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +local CoreGui = game:GetService("CoreGui") + +local OnyxUI = script.Parent.Parent.Parent +local Fusion = require(OnyxUI.Parent.Fusion) +local Util = require(OnyxUI.Util) +local Themer = require(OnyxUI.Themer) +local Signal = require(OnyxUI.Parent.Signal) + +local Children = Fusion.Children + +local Components = { + TextInput = require(OnyxUI.Components.TextInput), + Scroller = require(OnyxUI.Components.Scroller), + Button = require(OnyxUI.Components.Button), + Group = require(OnyxUI.Components.Group), +} + +local Scope = Fusion.scoped(Fusion, Components, Util) + +local States = { + Object = Scope:Value(nil), + Items = Scope:Value(Scope:Value({})), + Selection = Scope:Value(Scope:Value(nil)), + Theme = Scope:Value(Themer.Theme:now()), + OnSelected = Scope:Value(Scope:Value(function() end)), + FocusLost = Signal.new(), +} + +local Open = Scope:Computed(function(Use) + return Use(States.Object) ~= nil +end) +local CanvasPosition = Scope:Value(Vector2.new()) +local OnFocusLost = function(SelectedItem) + if SelectedItem then + Fusion.peek(States.Selection):set(SelectedItem) + end + + States.Items:set({}) + States.Object:set(nil) + + Fusion.peek(Fusion.peek(States.OnSelected))() + States.OnSelected:set(function() end) + + CanvasPosition:set(Vector2.new(), true) +end + +States.FocusLost:Connect(function() + OnFocusLost() +end) + +local function WatchPositionAndSize(GuiObject: Fusion.UsedAs) + local AbsolutePosition = Scope:Value(Vector2.new()) + local AbsoluteSize = Scope:Value(Vector2.new()) + + local PositionConnection: RBXScriptConnection? + local SizeConnection: RBXScriptConnection? + + Scope:Observer(GuiObject):onChange(function() + local ObjectValue = Fusion.peek(GuiObject) + + if PositionConnection then + PositionConnection:Disconnect() + end + if SizeConnection then + SizeConnection:Disconnect() + end + + if ObjectValue ~= nil then + AbsolutePosition:set(ObjectValue.AbsolutePosition) + AbsoluteSize:set(ObjectValue.AbsoluteSize) + + SizeConnection = ObjectValue:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + AbsoluteSize:set(ObjectValue.AbsoluteSize) + end) + + task.delay(1, function() + PositionConnection = ObjectValue:GetPropertyChangedSignal("AbsolutePosition"):Connect(function() + States.Object:set(nil) + end) + end) + end + end) + + return AbsolutePosition, AbsoluteSize +end + +local AbsolutePosition, AbsoluteSize = WatchPositionAndSize(States.Object) + +Scope:New "ScreenGui" { + Name = "DropdownHandler", + DisplayOrder = 1000, + Parent = Scope:Computed(function(Use) + if RunService:IsRunning() and (Players.LocalPlayer ~= nil) then + return Players.LocalPlayer:FindFirstChild("PlayerGui") + else + return CoreGui + end + end), + Enabled = Open, + + [Children] = { + Scope:Group { + Name = "List", + AnchorPoint = Vector2.new(0, -1), + Position = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + local AbsolutePositionValue = Use(AbsolutePosition) + local AbsoluteSizeValue = Use(AbsoluteSize) + return UDim2.fromOffset( + AbsolutePositionValue.X, + (AbsolutePositionValue.Y + AbsoluteSizeValue.Y) + (Use(Theme.StrokeThickness["1"]) * 2) + ) + end), + Size = Scope:Computed(function(Use) + local AbsoluteSizeValue = Use(AbsoluteSize) + return UDim2.new(UDim.new(0, AbsoluteSizeValue.X), UDim.new(0, 0)) + end), + AutomaticSize = Enum.AutomaticSize.Y, + CornerRadius = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + return Use(UDim.new(0, Use(Theme.CornerRadius["1"]))) + end), + + [Children] = { + Scope:Scroller { + Size = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + return UDim2.new(UDim.new(1, 0), UDim.new(0, Use(Theme.Spacing["16"]))) + end), + AutomaticSize = Enum.AutomaticSize.None, + StrokeEnabled = true, + StrokeColor = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + return Use(Theme.Colors.Neutral.Main) + end), + ListEnabled = true, + ListPadding = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + return Use(UDim.new(0, Use(Theme.Spacing["0"]))) + end), + ListHorizontalFlex = Enum.UIFlexAlignment.Fill, + BackgroundTransparency = 0, + BackgroundColor3 = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + return Use(Theme.Colors.Neutral.Main) + end), + Active = true, + CanvasPosition = CanvasPosition, + + [Children] = { + Scope:Computed(function(Use) + return Scope:ForValues(Use(States.Items), function(Use, Scope, Item) + local IsHovering = Scope:Value(false) + + return Themer.Theme:is(Use(States.Theme)):during(function() + return Scope:Button { + Name = "ItemButton", + Content = { Item }, + Style = "Ghost", + Color = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + return Use(Theme.Colors.BaseContent.Main) + end), + BackgroundTransparency = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + if Use(IsHovering) then + return 1 - Use(Theme.Emphasis.Light) + else + return 1 + end + end), + IsHovering = IsHovering, + CornerRadius = Scope:Computed(function(Use) + local Theme = Use(States.Theme) + return UDim.new(0, Use(Theme.CornerRadius["0"])) + end), + + OnActivated = function() + OnFocusLost(Item) + end, + } + end) + end) + end), + }, + }, + }, + }, + }, +} + +return States diff --git a/src/Components/Dropdown/init.luau b/src/Components/Dropdown/init.luau new file mode 100644 index 0000000..b1d2c49 --- /dev/null +++ b/src/Components/Dropdown/init.luau @@ -0,0 +1,74 @@ +local OnyxUI = script.Parent.Parent +local Fusion = require(OnyxUI.Parent.Fusion) +local Util = require(OnyxUI.Util) +local Themer = require(OnyxUI.Themer) +local DropdownHandler = require(script.DropdownHandler) + +local Base = require(OnyxUI.Components.Base) +local Components = { + TextInput = require(OnyxUI.Components.TextInput), + Scroller = require(OnyxUI.Components.Scroller), + Button = require(OnyxUI.Components.Button), + Group = require(OnyxUI.Components.Group), +} + +export type Props = Base.Props & { + Open: Fusion.UsedAs?, + Items: Fusion.UsedAs<{ string }>?, + Selection: Fusion.UsedAs?, + PlaceholderText: Fusion.UsedAs?, +} + +local Scope = Fusion.scoped(Fusion, Components, Util) + +return function(Scope: Fusion.Scope, Props: Props) + local Scope = Fusion.innerScope(Scope, Fusion, Components, Util) + local Theme = Themer.Theme:now() + + local Open = Scope:EnsureValue(Util.Fallback(Props.Open, false)) + local Items = Util.Fallback(Props.Items, {}) + local Selection = Scope:EnsureValue(Util.Fallback(Props.Selection, nil)) + local PlaceholderText = Util.Fallback(Props.PlaceholderText, "Dropdown") + local OnSelected = Util.Fallback(Props.OnSelected, function() end) + + local Text = Scope:Value("") + Scope:Observer(Selection):onChange(function() + local SelectionValue = tostring(Fusion.peek(Selection)) + Text:set(SelectionValue or "") + end) + + local Object + local OnFocused = function() + DropdownHandler.Object:set(Object) + DropdownHandler.Items:set(Items) + DropdownHandler.Selection:set(Selection) + DropdownHandler.Theme:set(Theme) + DropdownHandler.OnSelected:set(function() + OnSelected() + + Object:ReleaseFocus() + end) + end + + Object = Scope:TextInput(Util.CombineProps(Props, { + Name = script.Name, + Parent = Props.Parent, + Text = Text, + PlaceholderText = Scope:Computed(function(Use) + if Use(Selection) then + return Use(Selection) + else + return Use(PlaceholderText) + end + end), + + IsFocused = Open, + OnFocused = OnFocused, + -- OnFocusLost = function() + -- task.wait(0.1) + -- DropdownHandler.FocusLost:Fire() + -- end, + })) + + return Object +end diff --git a/wally.toml b/wally.toml index 016a0c6..9162d03 100644 --- a/wally.toml +++ b/wally.toml @@ -11,3 +11,4 @@ realm = "shared" [dependencies] ColorUtils = "csqrl/colour-utils@1.4.1" Fusion = "elttob/fusion@0.3.0" +Signal = "sleitnick/signal@2.0.1"