@lattice-ui/accordion 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @lattice-ui/accordion
2
+
3
+ Headless accordion primitives for Roblox UI with single and multiple disclosure modes.
4
+
5
+ ## Exports
6
+
7
+ - `Accordion`
8
+ - `Accordion.Root`
9
+ - `Accordion.Item`
10
+ - `Accordion.Header`
11
+ - `Accordion.Trigger`
12
+ - `Accordion.Content`
13
+
14
+ ## Notes
15
+
16
+ - Supports `type="single" | "multiple"`.
17
+ - Supports controlled/uncontrolled `value`.
18
+ - Keyboard navigation is integrated with roving focus.
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AccordionContentProps } from "./types";
3
+ export declare function AccordionContent(props: AccordionContentProps): React.JSX.Element | undefined;
@@ -0,0 +1,31 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local useAccordionItemContext = TS.import(script, script.Parent, "context").useAccordionItemContext
7
+ local function AccordionContent(props)
8
+ local itemContext = useAccordionItemContext()
9
+ local forceMount = props.forceMount == true
10
+ if not itemContext.open and not forceMount then
11
+ return nil
12
+ end
13
+ if props.asChild then
14
+ local child = props.children
15
+ if not React.isValidElement(child) then
16
+ error("[AccordionContent] `asChild` requires a child element.")
17
+ end
18
+ return React.createElement(Slot, {
19
+ Visible = itemContext.open,
20
+ }, child)
21
+ end
22
+ return React.createElement("frame", {
23
+ BackgroundColor3 = Color3.fromRGB(35, 41, 54),
24
+ BorderSizePixel = 0,
25
+ Size = UDim2.fromOffset(260, 44),
26
+ Visible = itemContext.open,
27
+ }, props.children)
28
+ end
29
+ return {
30
+ AccordionContent = AccordionContent,
31
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AccordionHeaderProps } from "./types";
3
+ export declare function AccordionHeader(props: AccordionHeaderProps): React.JSX.Element;
@@ -0,0 +1,22 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local function AccordionHeader(props)
7
+ if props.asChild then
8
+ local child = props.children
9
+ if not child then
10
+ error("[AccordionHeader] `asChild` requires a child element.")
11
+ end
12
+ return React.createElement(Slot, nil, child)
13
+ end
14
+ return React.createElement("frame", {
15
+ BackgroundTransparency = 1,
16
+ BorderSizePixel = 0,
17
+ Size = UDim2.fromOffset(260, 34),
18
+ }, props.children)
19
+ end
20
+ return {
21
+ AccordionHeader = AccordionHeader,
22
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AccordionItemProps } from "./types";
3
+ export declare function AccordionItem(props: AccordionItemProps): React.JSX.Element;
@@ -0,0 +1,41 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local _context = TS.import(script, script.Parent, "context")
7
+ local AccordionItemContextProvider = _context.AccordionItemContextProvider
8
+ local useAccordionContext = _context.useAccordionContext
9
+ local function AccordionItem(props)
10
+ local accordionContext = useAccordionContext()
11
+ local _openValues = accordionContext.openValues
12
+ local _value = props.value
13
+ local open = table.find(_openValues, _value) ~= nil
14
+ local disabled = props.disabled == true
15
+ local contextValue = React.useMemo(function()
16
+ return {
17
+ value = props.value,
18
+ open = open,
19
+ disabled = disabled,
20
+ }
21
+ end, { disabled, open, props.value })
22
+ if props.asChild then
23
+ local child = props.children
24
+ if not React.isValidElement(child) then
25
+ error("[AccordionItem] `asChild` requires a child element.")
26
+ end
27
+ return React.createElement(AccordionItemContextProvider, {
28
+ value = contextValue,
29
+ }, React.createElement(Slot, nil, child))
30
+ end
31
+ return React.createElement(AccordionItemContextProvider, {
32
+ value = contextValue,
33
+ }, React.createElement("frame", {
34
+ BackgroundTransparency = 1,
35
+ BorderSizePixel = 0,
36
+ Size = UDim2.fromOffset(260, 80),
37
+ }, props.children))
38
+ end
39
+ return {
40
+ AccordionItem = AccordionItem,
41
+ }
@@ -0,0 +1,4 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AccordionProps } from "./types";
3
+ export declare function AccordionRoot(props: AccordionProps): React.JSX.Element;
4
+ export { AccordionRoot as Accordion };
@@ -0,0 +1,68 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local useControllableState = _core.useControllableState
6
+ local RovingFocusGroup = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusGroup
7
+ local AccordionContextProvider = TS.import(script, script.Parent, "context").AccordionContextProvider
8
+ local _state = TS.import(script, script.Parent, "state")
9
+ local nextAccordionValues = _state.nextAccordionValues
10
+ local normalizeAccordionValue = _state.normalizeAccordionValue
11
+ local function AccordionRoot(props)
12
+ local accordionType = props.type or "single"
13
+ local _condition = props.loop
14
+ if _condition == nil then
15
+ _condition = true
16
+ end
17
+ local loop = _condition
18
+ local _condition_1 = props.collapsible
19
+ if _condition_1 == nil then
20
+ _condition_1 = false
21
+ end
22
+ local collapsible = _condition_1
23
+ local _condition_2 = props.defaultValue
24
+ if _condition_2 == nil then
25
+ _condition_2 = (if accordionType == "single" then "" else {})
26
+ end
27
+ local defaultValue = _condition_2
28
+ local _binding = useControllableState({
29
+ value = props.value,
30
+ defaultValue = defaultValue,
31
+ onChange = props.onValueChange,
32
+ })
33
+ local rawValue = _binding[1]
34
+ local setRawValue = _binding[2]
35
+ local openValues = normalizeAccordionValue(accordionType, rawValue)
36
+ local toggleItem = React.useCallback(function(candidateValue)
37
+ local nextValues = nextAccordionValues(accordionType, openValues, candidateValue, collapsible)
38
+ if accordionType == "single" then
39
+ local _condition_3 = nextValues[1]
40
+ if _condition_3 == nil then
41
+ _condition_3 = ""
42
+ end
43
+ setRawValue(_condition_3)
44
+ return nil
45
+ end
46
+ setRawValue(nextValues)
47
+ end, { accordionType, collapsible, openValues, setRawValue })
48
+ local contextValue = React.useMemo(function()
49
+ return {
50
+ type = accordionType,
51
+ openValues = openValues,
52
+ loop = loop,
53
+ toggleItem = toggleItem,
54
+ }
55
+ end, { accordionType, loop, openValues, toggleItem })
56
+ return React.createElement(AccordionContextProvider, {
57
+ value = contextValue,
58
+ }, React.createElement(RovingFocusGroup, {
59
+ active = true,
60
+ autoFocus = "none",
61
+ loop = loop,
62
+ orientation = "vertical",
63
+ }, props.children))
64
+ end
65
+ return {
66
+ AccordionRoot = AccordionRoot,
67
+ Accordion = AccordionRoot,
68
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AccordionTriggerProps } from "./types";
3
+ export declare function AccordionTrigger(props: AccordionTriggerProps): React.JSX.Element;
@@ -0,0 +1,70 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local RovingFocusItem = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).RovingFocusItem
7
+ local _context = TS.import(script, script.Parent, "context")
8
+ local useAccordionContext = _context.useAccordionContext
9
+ local useAccordionItemContext = _context.useAccordionItemContext
10
+ local function AccordionTrigger(props)
11
+ local accordionContext = useAccordionContext()
12
+ local itemContext = useAccordionItemContext()
13
+ local disabled = itemContext.disabled
14
+ local handleActivated = React.useCallback(function()
15
+ if disabled then
16
+ return nil
17
+ end
18
+ accordionContext.toggleItem(itemContext.value)
19
+ end, { accordionContext, disabled, itemContext.value })
20
+ local handleInputBegan = React.useCallback(function(_rbx, inputObject)
21
+ if disabled then
22
+ return nil
23
+ end
24
+ local keyCode = inputObject.KeyCode
25
+ if keyCode == Enum.KeyCode.Return or keyCode == Enum.KeyCode.Space then
26
+ accordionContext.toggleItem(itemContext.value)
27
+ end
28
+ end, { accordionContext, disabled, itemContext.value })
29
+ local eventHandlers = React.useMemo(function()
30
+ return {
31
+ Activated = handleActivated,
32
+ InputBegan = handleInputBegan,
33
+ }
34
+ end, { handleActivated, handleInputBegan })
35
+ if props.asChild then
36
+ local child = props.children
37
+ if not child then
38
+ error("[AccordionTrigger] `asChild` requires a child element.")
39
+ end
40
+ return React.createElement(RovingFocusItem, {
41
+ asChild = true,
42
+ disabled = disabled,
43
+ }, React.createElement(Slot, {
44
+ Active = not disabled,
45
+ Event = eventHandlers,
46
+ Selectable = not disabled,
47
+ }, child))
48
+ end
49
+ return React.createElement(RovingFocusItem, {
50
+ asChild = true,
51
+ disabled = disabled,
52
+ }, React.createElement("textbutton", {
53
+ Active = not disabled,
54
+ AutoButtonColor = false,
55
+ BackgroundColor3 = Color3.fromRGB(41, 48, 63),
56
+ BorderSizePixel = 0,
57
+ Event = eventHandlers,
58
+ Selectable = not disabled,
59
+ Size = UDim2.fromOffset(260, 34),
60
+ Text = if itemContext.open then "Collapse" else "Expand",
61
+ TextColor3 = if disabled then Color3.fromRGB(143, 150, 165) else Color3.fromRGB(236, 241, 249),
62
+ TextSize = 14,
63
+ TextXAlignment = Enum.TextXAlignment.Left,
64
+ }, React.createElement("uipadding", {
65
+ PaddingLeft = UDim.new(0, 10),
66
+ }), props.children))
67
+ end
68
+ return {
69
+ AccordionTrigger = AccordionTrigger,
70
+ }
@@ -0,0 +1,4 @@
1
+ import type { AccordionContextValue, AccordionItemContextValue } from "./types";
2
+ declare const AccordionContextProvider: import("@rbxts/react").Provider<AccordionContextValue | undefined>, useAccordionContext: () => AccordionContextValue;
3
+ declare const AccordionItemContextProvider: import("@rbxts/react").Provider<AccordionItemContextValue | undefined>, useAccordionItemContext: () => AccordionItemContextValue;
4
+ export { AccordionContextProvider, AccordionItemContextProvider, useAccordionContext, useAccordionItemContext };
@@ -0,0 +1,15 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local createStrictContext = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).createStrictContext
4
+ local _binding = createStrictContext("Accordion")
5
+ local AccordionContextProvider = _binding[1]
6
+ local useAccordionContext = _binding[2]
7
+ local _binding_1 = createStrictContext("AccordionItem")
8
+ local AccordionItemContextProvider = _binding_1[1]
9
+ local useAccordionItemContext = _binding_1[2]
10
+ return {
11
+ AccordionContextProvider = AccordionContextProvider,
12
+ AccordionItemContextProvider = AccordionItemContextProvider,
13
+ useAccordionContext = useAccordionContext,
14
+ useAccordionItemContext = useAccordionItemContext,
15
+ }
@@ -0,0 +1,3 @@
1
+ export type AccordionType = "single" | "multiple";
2
+ export declare function normalizeAccordionValue(accordionType: AccordionType, value: string | Array<string> | undefined): Array<string>;
3
+ export declare function nextAccordionValues(accordionType: AccordionType, currentValues: Array<string>, candidateValue: string, collapsible: boolean): Array<string>;
@@ -0,0 +1,65 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local function normalizeAccordionValue(accordionType, value)
3
+ if accordionType == "single" then
4
+ if value == nil then
5
+ return {}
6
+ end
7
+ local _value = value
8
+ if type(_value) == "string" then
9
+ return if #value > 0 then { value } else {}
10
+ end
11
+ return if value[1] ~= nil then { value[1] } else {}
12
+ end
13
+ if value == nil then
14
+ return {}
15
+ end
16
+ local _value = value
17
+ if type(_value) == "string" then
18
+ return if #value > 0 then { value } else {}
19
+ end
20
+ local deduped = {}
21
+ for _, item in value do
22
+ if not (table.find(deduped, item) ~= nil) then
23
+ table.insert(deduped, item)
24
+ end
25
+ end
26
+ return deduped
27
+ end
28
+ local function nextAccordionValues(accordionType, currentValues, candidateValue, collapsible)
29
+ local _currentValues = currentValues
30
+ local _candidateValue = candidateValue
31
+ local isOpen = table.find(_currentValues, _candidateValue) ~= nil
32
+ if accordionType == "single" then
33
+ if isOpen then
34
+ return if collapsible then {} else { candidateValue }
35
+ end
36
+ return { candidateValue }
37
+ end
38
+ if isOpen then
39
+ -- ▼ ReadonlyArray.filter ▼
40
+ local _newValue = {}
41
+ local _callback = function(value)
42
+ return value ~= candidateValue
43
+ end
44
+ local _length = 0
45
+ for _k, _v in currentValues do
46
+ if _callback(_v, _k - 1, currentValues) == true then
47
+ _length += 1
48
+ _newValue[_length] = _v
49
+ end
50
+ end
51
+ -- ▲ ReadonlyArray.filter ▲
52
+ return _newValue
53
+ end
54
+ local _array = {}
55
+ local _length = #_array
56
+ local _currentValuesLength = #currentValues
57
+ table.move(currentValues, 1, _currentValuesLength, _length + 1, _array)
58
+ _length += _currentValuesLength
59
+ _array[_length + 1] = candidateValue
60
+ return _array
61
+ end
62
+ return {
63
+ normalizeAccordionValue = normalizeAccordionValue,
64
+ nextAccordionValues = nextAccordionValues,
65
+ }
@@ -0,0 +1,41 @@
1
+ import type React from "@rbxts/react";
2
+ import type { AccordionType } from "./state";
3
+ export type AccordionContextValue = {
4
+ type: AccordionType;
5
+ openValues: Array<string>;
6
+ loop: boolean;
7
+ toggleItem: (value: string) => void;
8
+ };
9
+ export type AccordionItemContextValue = {
10
+ value: string;
11
+ open: boolean;
12
+ disabled: boolean;
13
+ };
14
+ export type AccordionProps = {
15
+ type?: AccordionType;
16
+ value?: string | Array<string>;
17
+ defaultValue?: string | Array<string>;
18
+ onValueChange?: (value: string | Array<string>) => void;
19
+ collapsible?: boolean;
20
+ loop?: boolean;
21
+ children?: React.ReactNode;
22
+ };
23
+ export type AccordionItemProps = {
24
+ value: string;
25
+ asChild?: boolean;
26
+ disabled?: boolean;
27
+ children?: React.ReactNode;
28
+ };
29
+ export type AccordionHeaderProps = {
30
+ asChild?: boolean;
31
+ children?: React.ReactElement;
32
+ };
33
+ export type AccordionTriggerProps = {
34
+ asChild?: boolean;
35
+ children?: React.ReactElement;
36
+ };
37
+ export type AccordionContentProps = {
38
+ asChild?: boolean;
39
+ forceMount?: boolean;
40
+ children?: React.ReactNode;
41
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/out/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { AccordionContent } from "./Accordion/AccordionContent";
2
+ import { AccordionHeader } from "./Accordion/AccordionHeader";
3
+ import { AccordionItem } from "./Accordion/AccordionItem";
4
+ import { AccordionRoot } from "./Accordion/AccordionRoot";
5
+ import { AccordionTrigger } from "./Accordion/AccordionTrigger";
6
+ export declare const Accordion: {
7
+ readonly Root: typeof AccordionRoot;
8
+ readonly Item: typeof AccordionItem;
9
+ readonly Header: typeof AccordionHeader;
10
+ readonly Trigger: typeof AccordionTrigger;
11
+ readonly Content: typeof AccordionContent;
12
+ };
13
+ export type { AccordionType } from "./Accordion/state";
14
+ export { nextAccordionValues, normalizeAccordionValue } from "./Accordion/state";
15
+ export type { AccordionContentProps, AccordionContextValue, AccordionHeaderProps, AccordionItemContextValue, AccordionItemProps, AccordionProps, AccordionTriggerProps, } from "./Accordion/types";
package/out/init.luau ADDED
@@ -0,0 +1,20 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local exports = {}
4
+ local AccordionContent = TS.import(script, script, "Accordion", "AccordionContent").AccordionContent
5
+ local AccordionHeader = TS.import(script, script, "Accordion", "AccordionHeader").AccordionHeader
6
+ local AccordionItem = TS.import(script, script, "Accordion", "AccordionItem").AccordionItem
7
+ local AccordionRoot = TS.import(script, script, "Accordion", "AccordionRoot").AccordionRoot
8
+ local AccordionTrigger = TS.import(script, script, "Accordion", "AccordionTrigger").AccordionTrigger
9
+ local Accordion = {
10
+ Root = AccordionRoot,
11
+ Item = AccordionItem,
12
+ Header = AccordionHeader,
13
+ Trigger = AccordionTrigger,
14
+ Content = AccordionContent,
15
+ }
16
+ local _state = TS.import(script, script, "Accordion", "state")
17
+ exports.nextAccordionValues = _state.nextAccordionValues
18
+ exports.normalizeAccordionValue = _state.normalizeAccordionValue
19
+ exports.Accordion = Accordion
20
+ return exports
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@lattice-ui/accordion",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "main": "out/init.luau",
6
+ "types": "out/index.d.ts",
7
+ "dependencies": {
8
+ "@lattice-ui/core": "0.3.0",
9
+ "@lattice-ui/focus": "0.3.0"
10
+ },
11
+ "devDependencies": {
12
+ "@rbxts/react": "17.3.7-ts.1",
13
+ "@rbxts/react-roblox": "17.3.7-ts.1"
14
+ },
15
+ "peerDependencies": {
16
+ "@rbxts/react": "^17",
17
+ "@rbxts/react-roblox": "^17"
18
+ },
19
+ "scripts": {
20
+ "build": "rbxtsc -p tsconfig.json",
21
+ "typecheck": "tsc -p tsconfig.typecheck.json",
22
+ "watch": "rbxtsc -p tsconfig.json -w"
23
+ }
24
+ }
@@ -0,0 +1,32 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useAccordionItemContext } from "./context";
3
+ import type { AccordionContentProps } from "./types";
4
+
5
+ export function AccordionContent(props: AccordionContentProps) {
6
+ const itemContext = useAccordionItemContext();
7
+ const forceMount = props.forceMount === true;
8
+
9
+ if (!itemContext.open && !forceMount) {
10
+ return undefined;
11
+ }
12
+
13
+ if (props.asChild) {
14
+ const child = props.children;
15
+ if (!React.isValidElement(child)) {
16
+ error("[AccordionContent] `asChild` requires a child element.");
17
+ }
18
+
19
+ return <Slot Visible={itemContext.open}>{child}</Slot>;
20
+ }
21
+
22
+ return (
23
+ <frame
24
+ BackgroundColor3={Color3.fromRGB(35, 41, 54)}
25
+ BorderSizePixel={0}
26
+ Size={UDim2.fromOffset(260, 44)}
27
+ Visible={itemContext.open}
28
+ >
29
+ {props.children}
30
+ </frame>
31
+ );
32
+ }
@@ -0,0 +1,19 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { AccordionHeaderProps } from "./types";
3
+
4
+ export function AccordionHeader(props: AccordionHeaderProps) {
5
+ if (props.asChild) {
6
+ const child = props.children;
7
+ if (!child) {
8
+ error("[AccordionHeader] `asChild` requires a child element.");
9
+ }
10
+
11
+ return <Slot>{child}</Slot>;
12
+ }
13
+
14
+ return (
15
+ <frame BackgroundTransparency={1} BorderSizePixel={0} Size={UDim2.fromOffset(260, 34)}>
16
+ {props.children}
17
+ </frame>
18
+ );
19
+ }
@@ -0,0 +1,39 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { AccordionItemContextProvider, useAccordionContext } from "./context";
3
+ import type { AccordionItemProps } from "./types";
4
+
5
+ export function AccordionItem(props: AccordionItemProps) {
6
+ const accordionContext = useAccordionContext();
7
+ const open = accordionContext.openValues.includes(props.value);
8
+ const disabled = props.disabled === true;
9
+
10
+ const contextValue = React.useMemo(
11
+ () => ({
12
+ value: props.value,
13
+ open,
14
+ disabled,
15
+ }),
16
+ [disabled, open, props.value],
17
+ );
18
+
19
+ if (props.asChild) {
20
+ const child = props.children;
21
+ if (!React.isValidElement(child)) {
22
+ error("[AccordionItem] `asChild` requires a child element.");
23
+ }
24
+
25
+ return (
26
+ <AccordionItemContextProvider value={contextValue}>
27
+ <Slot>{child}</Slot>
28
+ </AccordionItemContextProvider>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <AccordionItemContextProvider value={contextValue}>
34
+ <frame BackgroundTransparency={1} BorderSizePixel={0} Size={UDim2.fromOffset(260, 80)}>
35
+ {props.children}
36
+ </frame>
37
+ </AccordionItemContextProvider>
38
+ );
39
+ }
@@ -0,0 +1,54 @@
1
+ import { React, useControllableState } from "@lattice-ui/core";
2
+ import { RovingFocusGroup } from "@lattice-ui/focus";
3
+ import { AccordionContextProvider } from "./context";
4
+ import { nextAccordionValues, normalizeAccordionValue } from "./state";
5
+ import type { AccordionProps } from "./types";
6
+
7
+ export function AccordionRoot(props: AccordionProps) {
8
+ const accordionType = props.type ?? "single";
9
+ const loop = props.loop ?? true;
10
+ const collapsible = props.collapsible ?? false;
11
+
12
+ const defaultValue = props.defaultValue ?? (accordionType === "single" ? "" : []);
13
+
14
+ const [rawValue, setRawValue] = useControllableState<string | Array<string>>({
15
+ value: props.value,
16
+ defaultValue,
17
+ onChange: props.onValueChange,
18
+ });
19
+
20
+ const openValues = normalizeAccordionValue(accordionType, rawValue);
21
+
22
+ const toggleItem = React.useCallback(
23
+ (candidateValue: string) => {
24
+ const nextValues = nextAccordionValues(accordionType, openValues, candidateValue, collapsible);
25
+ if (accordionType === "single") {
26
+ setRawValue(nextValues[0] ?? "");
27
+ return;
28
+ }
29
+
30
+ setRawValue(nextValues);
31
+ },
32
+ [accordionType, collapsible, openValues, setRawValue],
33
+ );
34
+
35
+ const contextValue = React.useMemo(
36
+ () => ({
37
+ type: accordionType,
38
+ openValues,
39
+ loop,
40
+ toggleItem,
41
+ }),
42
+ [accordionType, loop, openValues, toggleItem],
43
+ );
44
+
45
+ return (
46
+ <AccordionContextProvider value={contextValue}>
47
+ <RovingFocusGroup active autoFocus="none" loop={loop} orientation="vertical">
48
+ {props.children}
49
+ </RovingFocusGroup>
50
+ </AccordionContextProvider>
51
+ );
52
+ }
53
+
54
+ export { AccordionRoot as Accordion };
@@ -0,0 +1,76 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { RovingFocusItem } from "@lattice-ui/focus";
3
+ import { useAccordionContext, useAccordionItemContext } from "./context";
4
+ import type { AccordionTriggerProps } from "./types";
5
+
6
+ export function AccordionTrigger(props: AccordionTriggerProps) {
7
+ const accordionContext = useAccordionContext();
8
+ const itemContext = useAccordionItemContext();
9
+ const disabled = itemContext.disabled;
10
+
11
+ const handleActivated = React.useCallback(() => {
12
+ if (disabled) {
13
+ return;
14
+ }
15
+
16
+ accordionContext.toggleItem(itemContext.value);
17
+ }, [accordionContext, disabled, itemContext.value]);
18
+
19
+ const handleInputBegan = React.useCallback(
20
+ (_rbx: GuiObject, inputObject: InputObject) => {
21
+ if (disabled) {
22
+ return;
23
+ }
24
+
25
+ const keyCode = inputObject.KeyCode;
26
+ if (keyCode === Enum.KeyCode.Return || keyCode === Enum.KeyCode.Space) {
27
+ accordionContext.toggleItem(itemContext.value);
28
+ }
29
+ },
30
+ [accordionContext, disabled, itemContext.value],
31
+ );
32
+
33
+ const eventHandlers = React.useMemo(
34
+ () => ({
35
+ Activated: handleActivated,
36
+ InputBegan: handleInputBegan,
37
+ }),
38
+ [handleActivated, handleInputBegan],
39
+ );
40
+
41
+ if (props.asChild) {
42
+ const child = props.children;
43
+ if (!child) {
44
+ error("[AccordionTrigger] `asChild` requires a child element.");
45
+ }
46
+
47
+ return (
48
+ <RovingFocusItem asChild disabled={disabled}>
49
+ <Slot Active={!disabled} Event={eventHandlers} Selectable={!disabled}>
50
+ {child}
51
+ </Slot>
52
+ </RovingFocusItem>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <RovingFocusItem asChild disabled={disabled}>
58
+ <textbutton
59
+ Active={!disabled}
60
+ AutoButtonColor={false}
61
+ BackgroundColor3={Color3.fromRGB(41, 48, 63)}
62
+ BorderSizePixel={0}
63
+ Event={eventHandlers}
64
+ Selectable={!disabled}
65
+ Size={UDim2.fromOffset(260, 34)}
66
+ Text={itemContext.open ? "Collapse" : "Expand"}
67
+ TextColor3={disabled ? Color3.fromRGB(143, 150, 165) : Color3.fromRGB(236, 241, 249)}
68
+ TextSize={14}
69
+ TextXAlignment={Enum.TextXAlignment.Left}
70
+ >
71
+ <uipadding PaddingLeft={new UDim(0, 10)} />
72
+ {props.children}
73
+ </textbutton>
74
+ </RovingFocusItem>
75
+ );
76
+ }
@@ -0,0 +1,8 @@
1
+ import { createStrictContext } from "@lattice-ui/core";
2
+ import type { AccordionContextValue, AccordionItemContextValue } from "./types";
3
+
4
+ const [AccordionContextProvider, useAccordionContext] = createStrictContext<AccordionContextValue>("Accordion");
5
+ const [AccordionItemContextProvider, useAccordionItemContext] =
6
+ createStrictContext<AccordionItemContextValue>("AccordionItem");
7
+
8
+ export { AccordionContextProvider, AccordionItemContextProvider, useAccordionContext, useAccordionItemContext };
@@ -0,0 +1,58 @@
1
+ export type AccordionType = "single" | "multiple";
2
+
3
+ export function normalizeAccordionValue(
4
+ accordionType: AccordionType,
5
+ value: string | Array<string> | undefined,
6
+ ): Array<string> {
7
+ if (accordionType === "single") {
8
+ if (value === undefined) {
9
+ return [];
10
+ }
11
+
12
+ if (typeIs(value, "string")) {
13
+ return value.size() > 0 ? [value] : [];
14
+ }
15
+
16
+ return value[0] !== undefined ? [value[0]] : [];
17
+ }
18
+
19
+ if (value === undefined) {
20
+ return [];
21
+ }
22
+
23
+ if (typeIs(value, "string")) {
24
+ return value.size() > 0 ? [value] : [];
25
+ }
26
+
27
+ const deduped: Array<string> = [];
28
+ for (const item of value) {
29
+ if (!deduped.includes(item)) {
30
+ deduped.push(item);
31
+ }
32
+ }
33
+
34
+ return deduped;
35
+ }
36
+
37
+ export function nextAccordionValues(
38
+ accordionType: AccordionType,
39
+ currentValues: Array<string>,
40
+ candidateValue: string,
41
+ collapsible: boolean,
42
+ ): Array<string> {
43
+ const isOpen = currentValues.includes(candidateValue);
44
+
45
+ if (accordionType === "single") {
46
+ if (isOpen) {
47
+ return collapsible ? [] : [candidateValue];
48
+ }
49
+
50
+ return [candidateValue];
51
+ }
52
+
53
+ if (isOpen) {
54
+ return currentValues.filter((value) => value !== candidateValue);
55
+ }
56
+
57
+ return [...currentValues, candidateValue];
58
+ }
@@ -0,0 +1,48 @@
1
+ import type React from "@rbxts/react";
2
+ import type { AccordionType } from "./state";
3
+
4
+ export type AccordionContextValue = {
5
+ type: AccordionType;
6
+ openValues: Array<string>;
7
+ loop: boolean;
8
+ toggleItem: (value: string) => void;
9
+ };
10
+
11
+ export type AccordionItemContextValue = {
12
+ value: string;
13
+ open: boolean;
14
+ disabled: boolean;
15
+ };
16
+
17
+ export type AccordionProps = {
18
+ type?: AccordionType;
19
+ value?: string | Array<string>;
20
+ defaultValue?: string | Array<string>;
21
+ onValueChange?: (value: string | Array<string>) => void;
22
+ collapsible?: boolean;
23
+ loop?: boolean;
24
+ children?: React.ReactNode;
25
+ };
26
+
27
+ export type AccordionItemProps = {
28
+ value: string;
29
+ asChild?: boolean;
30
+ disabled?: boolean;
31
+ children?: React.ReactNode;
32
+ };
33
+
34
+ export type AccordionHeaderProps = {
35
+ asChild?: boolean;
36
+ children?: React.ReactElement;
37
+ };
38
+
39
+ export type AccordionTriggerProps = {
40
+ asChild?: boolean;
41
+ children?: React.ReactElement;
42
+ };
43
+
44
+ export type AccordionContentProps = {
45
+ asChild?: boolean;
46
+ forceMount?: boolean;
47
+ children?: React.ReactNode;
48
+ };
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { AccordionContent } from "./Accordion/AccordionContent";
2
+ import { AccordionHeader } from "./Accordion/AccordionHeader";
3
+ import { AccordionItem } from "./Accordion/AccordionItem";
4
+ import { AccordionRoot } from "./Accordion/AccordionRoot";
5
+ import { AccordionTrigger } from "./Accordion/AccordionTrigger";
6
+
7
+ export const Accordion = {
8
+ Root: AccordionRoot,
9
+ Item: AccordionItem,
10
+ Header: AccordionHeader,
11
+ Trigger: AccordionTrigger,
12
+ Content: AccordionContent,
13
+ } as const;
14
+
15
+ export type { AccordionType } from "./Accordion/state";
16
+ export { nextAccordionValues, normalizeAccordionValue } from "./Accordion/state";
17
+ export type {
18
+ AccordionContentProps,
19
+ AccordionContextValue,
20
+ AccordionHeaderProps,
21
+ AccordionItemContextValue,
22
+ AccordionItemProps,
23
+ AccordionProps,
24
+ AccordionTriggerProps,
25
+ } from "./Accordion/types";
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "out",
6
+ "declaration": true,
7
+ "typeRoots": [
8
+ "./node_modules/@rbxts",
9
+ "../../node_modules/@rbxts",
10
+ "./node_modules/@lattice-ui",
11
+ "../../node_modules/@lattice-ui"
12
+ ],
13
+ "types": ["types", "compiler-types"]
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "baseUrl": "..",
6
+ "rootDir": "..",
7
+ "paths": {
8
+ "@lattice-ui/accordion": ["accordion/src/index.ts"],
9
+ "@lattice-ui/avatar": ["avatar/src/index.ts"],
10
+ "@lattice-ui/checkbox": ["checkbox/src/index.ts"],
11
+ "@lattice-ui/combobox": ["combobox/src/index.ts"],
12
+ "@lattice-ui/core": ["core/src/index.ts"],
13
+ "@lattice-ui/dialog": ["dialog/src/index.ts"],
14
+ "@lattice-ui/focus": ["focus/src/index.ts"],
15
+ "@lattice-ui/layer": ["layer/src/index.ts"],
16
+ "@lattice-ui/menu": ["menu/src/index.ts"],
17
+ "@lattice-ui/popover": ["popover/src/index.ts"],
18
+ "@lattice-ui/popper": ["popper/src/index.ts"],
19
+ "@lattice-ui/progress": ["progress/src/index.ts"],
20
+ "@lattice-ui/radio-group": ["radio-group/src/index.ts"],
21
+ "@lattice-ui/scroll-area": ["scroll-area/src/index.ts"],
22
+ "@lattice-ui/select": ["select/src/index.ts"],
23
+ "@lattice-ui/slider": ["slider/src/index.ts"],
24
+ "@lattice-ui/style": ["style/src/index.ts"],
25
+ "@lattice-ui/switch": ["switch/src/index.ts"],
26
+ "@lattice-ui/system": ["system/src/index.ts"],
27
+ "@lattice-ui/tabs": ["tabs/src/index.ts"],
28
+ "@lattice-ui/text-field": ["text-field/src/index.ts"],
29
+ "@lattice-ui/textarea": ["textarea/src/index.ts"],
30
+ "@lattice-ui/toast": ["toast/src/index.ts"],
31
+ "@lattice-ui/toggle-group": ["toggle-group/src/index.ts"],
32
+ "@lattice-ui/tooltip": ["tooltip/src/index.ts"]
33
+ }
34
+ }
35
+ }