@seed-design/lynx-react 0.4.1 → 0.5.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/lib/components/Accordion/Accordion.d.ts +56 -0
- package/lib/components/Accordion/Accordion.d.ts.map +1 -0
- package/lib/components/Accordion/Accordion.jsx +210 -0
- package/lib/components/Accordion/Accordion.namespace.d.ts +2 -0
- package/lib/components/Accordion/Accordion.namespace.d.ts.map +1 -0
- package/lib/components/Accordion/Accordion.namespace.js +17 -0
- package/lib/components/Accordion/index.d.ts +3 -0
- package/lib/components/Accordion/index.d.ts.map +1 -0
- package/lib/components/ActionButton/ActionButton.jsx +6 -6
- package/lib/components/AppBar/AppBar.jsx +1 -1
- package/lib/components/Badge/Badge.jsx +1 -1
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.jsx +6 -3
- package/lib/components/Box/Box.jsx +1 -1
- package/lib/components/Callout/Callout.d.ts +32 -0
- package/lib/components/Callout/Callout.d.ts.map +1 -0
- package/lib/components/Callout/Callout.jsx +113 -0
- package/lib/components/Callout/Callout.namespace.d.ts +2 -0
- package/lib/components/Callout/Callout.namespace.d.ts.map +1 -0
- package/lib/components/Callout/Callout.namespace.js +13 -0
- package/lib/components/Callout/index.d.ts +3 -0
- package/lib/components/Callout/index.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/lib/components/Checkbox/Checkbox.jsx +36 -5
- package/lib/components/Chip/Chip.d.ts +66 -0
- package/lib/components/Chip/Chip.d.ts.map +1 -0
- package/lib/components/Chip/Chip.jsx +159 -0
- package/lib/components/Chip/Chip.namespace.d.ts +2 -0
- package/lib/components/Chip/Chip.namespace.d.ts.map +1 -0
- package/lib/components/Chip/Chip.namespace.js +16 -0
- package/lib/components/Chip/index.d.ts +3 -0
- package/lib/components/Chip/index.d.ts.map +1 -0
- package/lib/components/Divider/Divider.d.ts +31 -0
- package/lib/components/Divider/Divider.d.ts.map +1 -0
- package/lib/components/Divider/Divider.jsx +26 -0
- package/lib/components/Divider/index.d.ts +2 -0
- package/lib/components/Divider/index.d.ts.map +1 -0
- package/lib/components/Field/Field.jsx +1 -1
- package/lib/components/Icon/Icon.jsx +2 -2
- package/lib/components/ProgressCircle/ProgressCircle.jsx +2 -2
- package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
- package/lib/components/Stack/Stack.jsx +1 -1
- package/lib/components/Switch/Switch.jsx +2 -2
- package/lib/components/Tabs/Tabs.d.ts +59 -0
- package/lib/components/Tabs/Tabs.d.ts.map +1 -0
- package/lib/components/Tabs/Tabs.jsx +406 -0
- package/lib/components/Tabs/Tabs.namespace.d.ts +2 -0
- package/lib/components/Tabs/Tabs.namespace.d.ts.map +1 -0
- package/lib/components/Tabs/Tabs.namespace.js +14 -0
- package/lib/components/Tabs/index.d.ts +3 -0
- package/lib/components/Tabs/index.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.jsx +2 -2
- package/lib/components/Text/Text.jsx +2 -2
- package/lib/components/TextField/TextField.jsx +2 -2
- package/lib/components/index.d.ts +5 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/hooks/useIconColor.d.ts +2 -1
- package/lib/hooks/useIconColor.d.ts.map +1 -1
- package/lib/hooks/useIconColor.js +29 -5
- package/lib/hooks/usePressTap.d.ts.map +1 -1
- package/lib/hooks/usePressTap.js +1 -0
- package/lib/index.js +14 -5
- package/lib/utils/create-slot-recipe-context.jsx +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.namespace.ts +22 -0
- package/src/components/Accordion/Accordion.test.tsx +129 -0
- package/src/components/Accordion/Accordion.tsx +453 -0
- package/src/components/Accordion/index.ts +24 -0
- package/src/components/BottomSheet/BottomSheet.test.tsx +24 -0
- package/src/components/BottomSheet/BottomSheet.tsx +14 -2
- package/src/components/Callout/Callout.namespace.ts +14 -0
- package/src/components/Callout/Callout.test.tsx +188 -0
- package/src/components/Callout/Callout.tsx +272 -0
- package/src/components/Callout/index.ts +16 -0
- package/src/components/Checkbox/Checkbox.test.tsx +176 -0
- package/src/components/Checkbox/Checkbox.tsx +36 -2
- package/src/components/Chip/Chip.namespace.ts +20 -0
- package/src/components/Chip/Chip.test.tsx +113 -0
- package/src/components/Chip/Chip.tsx +373 -0
- package/src/components/Chip/index.ts +22 -0
- package/src/components/Divider/Divider.test.tsx +73 -0
- package/src/components/Divider/Divider.tsx +76 -0
- package/src/components/Divider/index.ts +1 -0
- package/src/components/Icon/Icon.test.tsx +129 -0
- package/src/components/Tabs/Tabs.namespace.ts +16 -0
- package/src/components/Tabs/Tabs.test.tsx +181 -0
- package/src/components/Tabs/Tabs.tsx +706 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/index.ts +5 -0
- package/src/hooks/useIconColor.ts +43 -5
- package/src/hooks/usePressTap.test.ts +13 -0
- package/src/hooks/usePressTap.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import clsx from "clsx";
|
|
2
1
|
import { createContext, forwardRef, useContext } from "@lynx-js/react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
3
|
//#region src/utils/create-slot-recipe-context.tsx
|
|
4
4
|
function assertNotIntrinsicComponent(Component, caller) {
|
|
5
5
|
if (typeof Component !== "string") return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/lynx-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/daangn/seed-design.git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@lynx-js/react": "^0.117.0",
|
|
41
41
|
"@lynx-js/types": "^3.7.0",
|
|
42
|
-
"@seed-design/lynx-css": "0.
|
|
42
|
+
"@seed-design/lynx-css": "0.9.0",
|
|
43
43
|
"@testing-library/jest-dom": "^6.9.1",
|
|
44
44
|
"jsdom": "^29.0.2",
|
|
45
45
|
"preact": "npm:@lynx-js/internal-preact@10.28.4-ee7bb26",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AccordionBody as Body,
|
|
3
|
+
AccordionContent as Content,
|
|
4
|
+
AccordionDescription as Description,
|
|
5
|
+
AccordionHeader as Header,
|
|
6
|
+
AccordionItem as Item,
|
|
7
|
+
AccordionPrefix as Prefix,
|
|
8
|
+
AccordionRoot as Root,
|
|
9
|
+
AccordionSuffixIcon as SuffixIcon,
|
|
10
|
+
AccordionTitle as Title,
|
|
11
|
+
AccordionTrigger as Trigger,
|
|
12
|
+
type AccordionBodyProps as BodyProps,
|
|
13
|
+
type AccordionContentProps as ContentProps,
|
|
14
|
+
type AccordionDescriptionProps as DescriptionProps,
|
|
15
|
+
type AccordionHeaderProps as HeaderProps,
|
|
16
|
+
type AccordionItemProps as ItemProps,
|
|
17
|
+
type AccordionPrefixProps as PrefixProps,
|
|
18
|
+
type AccordionRootProps as RootProps,
|
|
19
|
+
type AccordionSuffixIconProps as SuffixIconProps,
|
|
20
|
+
type AccordionTitleProps as TitleProps,
|
|
21
|
+
type AccordionTriggerProps as TriggerProps,
|
|
22
|
+
} from "./Accordion";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { act, fireEvent, render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import type * as React from "@lynx-js/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { Accordion } from "./index";
|
|
7
|
+
|
|
8
|
+
function getRenderedRoot() {
|
|
9
|
+
const root = elementTree.root;
|
|
10
|
+
|
|
11
|
+
if (!root) {
|
|
12
|
+
throw new Error("Expected Lynx render root to exist.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return root;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function TestAccordion(props: React.ComponentProps<typeof Accordion.Root> = {}) {
|
|
19
|
+
return (
|
|
20
|
+
<Accordion.Root {...props}>
|
|
21
|
+
<Accordion.Item value="first">
|
|
22
|
+
<Accordion.Header>
|
|
23
|
+
<Accordion.Trigger className="first-trigger" accessibility-label="첫 번째">
|
|
24
|
+
<Accordion.Body>
|
|
25
|
+
<Accordion.Title>첫 번째</Accordion.Title>
|
|
26
|
+
</Accordion.Body>
|
|
27
|
+
</Accordion.Trigger>
|
|
28
|
+
</Accordion.Header>
|
|
29
|
+
<Accordion.Content className="first-content">
|
|
30
|
+
<text>첫 번째 내용</text>
|
|
31
|
+
</Accordion.Content>
|
|
32
|
+
</Accordion.Item>
|
|
33
|
+
<Accordion.Item value="second">
|
|
34
|
+
<Accordion.Header>
|
|
35
|
+
<Accordion.Trigger className="second-trigger" accessibility-label="두 번째">
|
|
36
|
+
<Accordion.Body>
|
|
37
|
+
<Accordion.Title>두 번째</Accordion.Title>
|
|
38
|
+
</Accordion.Body>
|
|
39
|
+
</Accordion.Trigger>
|
|
40
|
+
</Accordion.Header>
|
|
41
|
+
<Accordion.Content>
|
|
42
|
+
<text>두 번째 내용</text>
|
|
43
|
+
</Accordion.Content>
|
|
44
|
+
</Accordion.Item>
|
|
45
|
+
</Accordion.Root>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("Accordion", () => {
|
|
50
|
+
it("opens and closes an item in uncontrolled mode", () => {
|
|
51
|
+
render(<TestAccordion />);
|
|
52
|
+
|
|
53
|
+
const trigger = getRenderedRoot().querySelector<HTMLElement>(".first-trigger");
|
|
54
|
+
const pressedOverlay = trigger?.querySelector<HTMLElement>(".seed-accordion__pressedOverlay");
|
|
55
|
+
expect(trigger).not.toBeNull();
|
|
56
|
+
expect(pressedOverlay).not.toBeNull();
|
|
57
|
+
expect(pressedOverlay).toHaveAttribute("accessibility-elements-hidden", "true");
|
|
58
|
+
expect(trigger).toHaveAttribute("accessibility-value", "접힘");
|
|
59
|
+
|
|
60
|
+
fireEvent.tap(trigger as HTMLElement);
|
|
61
|
+
expect(trigger).toHaveAttribute("accessibility-value", "펼쳐짐");
|
|
62
|
+
|
|
63
|
+
fireEvent.tap(trigger as HTMLElement);
|
|
64
|
+
expect(trigger).toHaveAttribute("accessibility-value", "접힘");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("transitions content between zero and its measured height", () => {
|
|
68
|
+
render(<TestAccordion defaultValues={["first"]} />);
|
|
69
|
+
|
|
70
|
+
const root = getRenderedRoot();
|
|
71
|
+
const trigger = root.querySelector<HTMLElement>(".first-trigger");
|
|
72
|
+
const content = root.querySelector<HTMLElement>(".first-content");
|
|
73
|
+
const contentInner = content?.querySelector<HTMLElement>(".seed-accordion__contentInner");
|
|
74
|
+
|
|
75
|
+
expect(content).not.toBeNull();
|
|
76
|
+
expect(contentInner).not.toBeNull();
|
|
77
|
+
expect(content).toHaveStyle({ height: "0px" });
|
|
78
|
+
|
|
79
|
+
act(() => {
|
|
80
|
+
fireEvent.layoutchange(contentInner as HTMLElement, { height: 84 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(content).toHaveStyle({ height: "84px" });
|
|
84
|
+
|
|
85
|
+
fireEvent.tap(trigger as HTMLElement);
|
|
86
|
+
expect(content).toHaveStyle({ height: "0px" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps one item open by default and supports multiple mode", () => {
|
|
90
|
+
render(<TestAccordion multiple defaultValues={["first"]} />);
|
|
91
|
+
|
|
92
|
+
const root = getRenderedRoot();
|
|
93
|
+
const first = root.querySelector<HTMLElement>(".first-trigger");
|
|
94
|
+
const second = root.querySelector<HTMLElement>(".second-trigger");
|
|
95
|
+
|
|
96
|
+
expect(first).toHaveAttribute("accessibility-value", "펼쳐짐");
|
|
97
|
+
expect(second).toHaveAttribute("accessibility-value", "접힘");
|
|
98
|
+
|
|
99
|
+
fireEvent.tap(second as HTMLElement);
|
|
100
|
+
expect(first).toHaveAttribute("accessibility-value", "펼쳐짐");
|
|
101
|
+
expect(second).toHaveAttribute("accessibility-value", "펼쳐짐");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("reports controlled value changes without mutating the rendered state", () => {
|
|
105
|
+
const onValuesChange = vi.fn();
|
|
106
|
+
render(<TestAccordion values={[]} onValuesChange={onValuesChange} />);
|
|
107
|
+
|
|
108
|
+
const trigger = getRenderedRoot().querySelector<HTMLElement>(".first-trigger");
|
|
109
|
+
fireEvent.tap(trigger as HTMLElement);
|
|
110
|
+
|
|
111
|
+
expect(onValuesChange).toHaveBeenCalledWith(["first"]);
|
|
112
|
+
expect(trigger).toHaveAttribute("accessibility-value", "접힘");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("ignores tap when the root is disabled", () => {
|
|
116
|
+
const onValuesChange = vi.fn();
|
|
117
|
+
render(<TestAccordion disabled onValuesChange={onValuesChange} />);
|
|
118
|
+
|
|
119
|
+
const trigger = getRenderedRoot().querySelector<HTMLElement>(".first-trigger");
|
|
120
|
+
fireEvent.tap(trigger as HTMLElement);
|
|
121
|
+
|
|
122
|
+
expect(onValuesChange).not.toHaveBeenCalled();
|
|
123
|
+
expect(trigger).toHaveAttribute("accessibility-traits", "disabled");
|
|
124
|
+
expect(getRenderedRoot().querySelector(".first-content")).toHaveAttribute(
|
|
125
|
+
"accessibility-elements-hidden",
|
|
126
|
+
"true",
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import type { ReactElement } from "@lynx-js/react";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import { accordion, type AccordionVariantProps } from "@seed-design/lynx-css/recipes/accordion";
|
|
6
|
+
|
|
7
|
+
import { useControllableState } from "../../hooks/useControllableState";
|
|
8
|
+
import { usePressTap } from "../../hooks/usePressTap";
|
|
9
|
+
import type {
|
|
10
|
+
LynxAccessibilityProps,
|
|
11
|
+
LynxIconElementProps,
|
|
12
|
+
LynxPressableProps,
|
|
13
|
+
LynxStyledElementProps,
|
|
14
|
+
LynxTextRef,
|
|
15
|
+
LynxViewProps,
|
|
16
|
+
LynxViewRef,
|
|
17
|
+
} from "../../types";
|
|
18
|
+
import { toArray } from "../../utils/children";
|
|
19
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
20
|
+
import { InternalIcon } from "../Icon/Icon";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @platform Lynx
|
|
24
|
+
*
|
|
25
|
+
* 웹 대비 미지원 기능:
|
|
26
|
+
* - `asChild`: Lynx에 Slot 기반 polymorphic 렌더링이 없음
|
|
27
|
+
* - `headingLevel`: Lynx 접근성 heading은 level을 받지 않음
|
|
28
|
+
* - DOM ARIA와 키보드 탐색: Lynx native 접근성 속성과 tap 상호작용으로 대체
|
|
29
|
+
* - `size="responsive"`: Lynx preset은 viewport media query를 지원하지 않음
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
type PublicAccordionVariantProps = Omit<AccordionVariantProps, "open" | "pressed" | "disabled">;
|
|
33
|
+
|
|
34
|
+
interface AccordionContextValue {
|
|
35
|
+
values: string[];
|
|
36
|
+
disabled: boolean;
|
|
37
|
+
variantProps: PublicAccordionVariantProps;
|
|
38
|
+
toggle: (value: string) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const AccordionContext = React.createContext<AccordionContextValue | null>(null);
|
|
42
|
+
|
|
43
|
+
function useAccordionContext(consumer: string): AccordionContextValue {
|
|
44
|
+
const context = React.useContext(AccordionContext);
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error(`<${consumer}/> must be rendered inside <AccordionRoot/>.`);
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface AccordionItemContextValue {
|
|
52
|
+
value: string;
|
|
53
|
+
open: boolean;
|
|
54
|
+
disabled: boolean;
|
|
55
|
+
variantProps: PublicAccordionVariantProps;
|
|
56
|
+
toggle: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const AccordionItemContext = React.createContext<AccordionItemContextValue | null>(null);
|
|
60
|
+
const AccordionItemPositionContext = React.createContext({ isLast: false });
|
|
61
|
+
|
|
62
|
+
function useAccordionItemContext(consumer: string): AccordionItemContextValue {
|
|
63
|
+
const context = React.useContext(AccordionItemContext);
|
|
64
|
+
if (!context) {
|
|
65
|
+
throw new Error(`<${consumer}/> must be rendered inside <AccordionItem/>.`);
|
|
66
|
+
}
|
|
67
|
+
return context;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(accordion);
|
|
71
|
+
|
|
72
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
73
|
+
|
|
74
|
+
export interface AccordionRootProps extends PublicAccordionVariantProps, LynxStyledElementProps {
|
|
75
|
+
values?: string[];
|
|
76
|
+
defaultValues?: string[];
|
|
77
|
+
onValuesChange?: (values: string[]) => void;
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
multiple?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const AccordionRoot = React.forwardRef<unknown, AccordionRootProps>((props, ref) => {
|
|
83
|
+
const {
|
|
84
|
+
children,
|
|
85
|
+
className,
|
|
86
|
+
values: valuesProp,
|
|
87
|
+
defaultValues = [],
|
|
88
|
+
onValuesChange,
|
|
89
|
+
disabled = false,
|
|
90
|
+
multiple = false,
|
|
91
|
+
...restProps
|
|
92
|
+
} = props;
|
|
93
|
+
const [variantProps, nativeProps] = accordion.splitVariantProps(restProps);
|
|
94
|
+
const [rawValues, setValues] = useControllableState({
|
|
95
|
+
value: valuesProp,
|
|
96
|
+
defaultValue: defaultValues,
|
|
97
|
+
onChange: onValuesChange,
|
|
98
|
+
});
|
|
99
|
+
const values = multiple ? rawValues : rawValues.slice(0, 1);
|
|
100
|
+
|
|
101
|
+
const toggle = React.useCallback(
|
|
102
|
+
(itemValue: string) => {
|
|
103
|
+
if (disabled) return;
|
|
104
|
+
|
|
105
|
+
if (!multiple) {
|
|
106
|
+
setValues(values[0] === itemValue ? [] : [itemValue]);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setValues(
|
|
111
|
+
values.includes(itemValue)
|
|
112
|
+
? values.filter((value) => value !== itemValue)
|
|
113
|
+
: [...values, itemValue],
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
[disabled, multiple, setValues, values],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const contextValue = React.useMemo<AccordionContextValue>(
|
|
120
|
+
() => ({ values, disabled, variantProps, toggle }),
|
|
121
|
+
[disabled, toggle, values, variantProps],
|
|
122
|
+
);
|
|
123
|
+
const classes = accordion({ ...variantProps, disabled });
|
|
124
|
+
const items = toArray(children);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<AccordionContext.Provider value={contextValue}>
|
|
128
|
+
<view
|
|
129
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
130
|
+
className={clsx(classes.root, className)}
|
|
131
|
+
{...nativeProps}
|
|
132
|
+
>
|
|
133
|
+
{items.map((item, index) => (
|
|
134
|
+
<AccordionItemPositionContext.Provider
|
|
135
|
+
key={React.isValidElement(item) ? (item.key ?? index) : index}
|
|
136
|
+
value={{ isLast: index === items.length - 1 }}
|
|
137
|
+
>
|
|
138
|
+
{item}
|
|
139
|
+
</AccordionItemPositionContext.Provider>
|
|
140
|
+
))}
|
|
141
|
+
</view>
|
|
142
|
+
</AccordionContext.Provider>
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
AccordionRoot.displayName = "AccordionRoot";
|
|
146
|
+
|
|
147
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
148
|
+
|
|
149
|
+
export interface AccordionItemProps extends LynxStyledElementProps {
|
|
150
|
+
value: string;
|
|
151
|
+
disabled?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const AccordionItem = React.forwardRef<unknown, AccordionItemProps>((props, ref) => {
|
|
155
|
+
const { children, className, value, disabled: itemDisabled = false, ...nativeProps } = props;
|
|
156
|
+
const accordionContext = useAccordionContext("AccordionItem");
|
|
157
|
+
const { isLast } = React.useContext(AccordionItemPositionContext);
|
|
158
|
+
const disabled = accordionContext.disabled || itemDisabled;
|
|
159
|
+
const open = accordionContext.values.includes(value);
|
|
160
|
+
const toggle = React.useCallback(() => accordionContext.toggle(value), [accordionContext, value]);
|
|
161
|
+
const classes = accordion({
|
|
162
|
+
...accordionContext.variantProps,
|
|
163
|
+
open,
|
|
164
|
+
disabled,
|
|
165
|
+
pressed: false,
|
|
166
|
+
});
|
|
167
|
+
const contextValue = React.useMemo<AccordionItemContextValue>(
|
|
168
|
+
() => ({ value, open, disabled, variantProps: accordionContext.variantProps, toggle }),
|
|
169
|
+
[accordionContext.variantProps, disabled, open, toggle, value],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<AccordionItemContext.Provider value={contextValue}>
|
|
174
|
+
<ClassNamesProvider value={classes}>
|
|
175
|
+
<view
|
|
176
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
177
|
+
className={clsx(classes.item, className)}
|
|
178
|
+
{...nativeProps}
|
|
179
|
+
>
|
|
180
|
+
{children}
|
|
181
|
+
{!isLast ? (
|
|
182
|
+
<view className={classes.divider} accessibility-elements-hidden={true} />
|
|
183
|
+
) : null}
|
|
184
|
+
</view>
|
|
185
|
+
</ClassNamesProvider>
|
|
186
|
+
</AccordionItemContext.Provider>
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
AccordionItem.displayName = "AccordionItem";
|
|
190
|
+
|
|
191
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
192
|
+
|
|
193
|
+
export interface AccordionHeaderProps extends LynxStyledElementProps, LynxAccessibilityProps {}
|
|
194
|
+
|
|
195
|
+
export const AccordionHeader = React.forwardRef<unknown, AccordionHeaderProps>((props, ref) => {
|
|
196
|
+
const {
|
|
197
|
+
children,
|
|
198
|
+
className,
|
|
199
|
+
"accessibility-heading": accessibilityHeading = true,
|
|
200
|
+
...nativeProps
|
|
201
|
+
} = props;
|
|
202
|
+
const classes = useClassNames();
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<view
|
|
206
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
207
|
+
className={clsx(classes.header, className)}
|
|
208
|
+
accessibility-heading={accessibilityHeading}
|
|
209
|
+
{...nativeProps}
|
|
210
|
+
>
|
|
211
|
+
{children}
|
|
212
|
+
</view>
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
AccordionHeader.displayName = "AccordionHeader";
|
|
216
|
+
|
|
217
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
218
|
+
|
|
219
|
+
export interface AccordionTriggerProps
|
|
220
|
+
extends LynxStyledElementProps,
|
|
221
|
+
LynxAccessibilityProps,
|
|
222
|
+
Pick<LynxPressableProps, "bindtap"> {
|
|
223
|
+
expandedAccessibilityValue?: string;
|
|
224
|
+
collapsedAccessibilityValue?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const AccordionTrigger = React.forwardRef<unknown, AccordionTriggerProps>((props, ref) => {
|
|
228
|
+
const {
|
|
229
|
+
children,
|
|
230
|
+
className,
|
|
231
|
+
bindtap,
|
|
232
|
+
expandedAccessibilityValue = "펼쳐짐",
|
|
233
|
+
collapsedAccessibilityValue = "접힘",
|
|
234
|
+
"accessibility-element": accessibilityElement = true,
|
|
235
|
+
"accessibility-role-description": accessibilityRoleDescription = "button",
|
|
236
|
+
"accessibility-traits": accessibilityTraits,
|
|
237
|
+
"accessibility-value": accessibilityValue,
|
|
238
|
+
...nativeProps
|
|
239
|
+
} = props;
|
|
240
|
+
const context = useAccordionItemContext("AccordionTrigger");
|
|
241
|
+
const handleTap = React.useCallback(
|
|
242
|
+
(event: Parameters<NonNullable<LynxPressableProps["bindtap"]>>[0]) => {
|
|
243
|
+
context.toggle();
|
|
244
|
+
bindtap?.(event);
|
|
245
|
+
},
|
|
246
|
+
[bindtap, context],
|
|
247
|
+
);
|
|
248
|
+
const { pressed, ...pressHandlers } = usePressTap({
|
|
249
|
+
disabled: context.disabled,
|
|
250
|
+
onTap: handleTap,
|
|
251
|
+
});
|
|
252
|
+
const classes = accordion({
|
|
253
|
+
...context.variantProps,
|
|
254
|
+
open: context.open,
|
|
255
|
+
disabled: context.disabled,
|
|
256
|
+
pressed,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<ClassNamesProvider value={classes}>
|
|
261
|
+
<view
|
|
262
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
263
|
+
className={clsx(classes.trigger, className)}
|
|
264
|
+
accessibility-element={accessibilityElement}
|
|
265
|
+
accessibility-role-description={accessibilityRoleDescription}
|
|
266
|
+
accessibility-traits={accessibilityTraits ?? (context.disabled ? "disabled" : "button")}
|
|
267
|
+
accessibility-value={
|
|
268
|
+
accessibilityValue ??
|
|
269
|
+
(context.open ? expandedAccessibilityValue : collapsedAccessibilityValue)
|
|
270
|
+
}
|
|
271
|
+
{...pressHandlers}
|
|
272
|
+
{...nativeProps}
|
|
273
|
+
>
|
|
274
|
+
<view className={classes.pressedOverlay} accessibility-elements-hidden={true} />
|
|
275
|
+
{children}
|
|
276
|
+
</view>
|
|
277
|
+
</ClassNamesProvider>
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
281
|
+
|
|
282
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
283
|
+
|
|
284
|
+
export interface AccordionContentProps extends LynxStyledElementProps, LynxAccessibilityProps {}
|
|
285
|
+
|
|
286
|
+
type ContentLayoutChangeHandler = NonNullable<LynxViewProps["bindlayoutchange"]>;
|
|
287
|
+
|
|
288
|
+
function getContentLayoutHeight(event: Parameters<ContentLayoutChangeHandler>[0]): number | null {
|
|
289
|
+
const eventWithHeight = event as Parameters<ContentLayoutChangeHandler>[0] & { height?: number };
|
|
290
|
+
const height = event.detail?.height ?? event.params?.height ?? eventWithHeight.height;
|
|
291
|
+
|
|
292
|
+
if (typeof height !== "number" || !Number.isFinite(height)) return null;
|
|
293
|
+
return Math.max(0, height);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const AccordionContent = React.forwardRef<unknown, AccordionContentProps>((props, ref) => {
|
|
297
|
+
const {
|
|
298
|
+
children,
|
|
299
|
+
className,
|
|
300
|
+
style,
|
|
301
|
+
"accessibility-elements-hidden": accessibilityElementsHidden = false,
|
|
302
|
+
...nativeProps
|
|
303
|
+
} = props;
|
|
304
|
+
const context = useAccordionItemContext("AccordionContent");
|
|
305
|
+
const classes = useClassNames();
|
|
306
|
+
const [contentHeight, setContentHeight] = React.useState(0);
|
|
307
|
+
const handleContentLayoutChange = React.useCallback<ContentLayoutChangeHandler>((event) => {
|
|
308
|
+
const height = getContentLayoutHeight(event);
|
|
309
|
+
if (height !== null) setContentHeight((current) => (current === height ? current : height));
|
|
310
|
+
}, []);
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<view
|
|
314
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
315
|
+
className={clsx(classes.content, className)}
|
|
316
|
+
style={{ ...style, height: context.open ? `${contentHeight}px` : "0px" }}
|
|
317
|
+
accessibility-elements-hidden={!context.open || accessibilityElementsHidden}
|
|
318
|
+
{...nativeProps}
|
|
319
|
+
>
|
|
320
|
+
<view className={classes.contentInner} bindlayoutchange={handleContentLayoutChange}>
|
|
321
|
+
{children}
|
|
322
|
+
</view>
|
|
323
|
+
</view>
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
AccordionContent.displayName = "AccordionContent";
|
|
327
|
+
|
|
328
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
329
|
+
|
|
330
|
+
export interface AccordionBodyProps extends LynxStyledElementProps {}
|
|
331
|
+
|
|
332
|
+
export const AccordionBody = React.forwardRef<unknown, AccordionBodyProps>((props, ref) => {
|
|
333
|
+
const { children, className, ...nativeProps } = props;
|
|
334
|
+
const classes = useClassNames();
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<view
|
|
338
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
339
|
+
className={clsx(classes.body, className)}
|
|
340
|
+
{...nativeProps}
|
|
341
|
+
>
|
|
342
|
+
{children}
|
|
343
|
+
</view>
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
AccordionBody.displayName = "AccordionBody";
|
|
347
|
+
|
|
348
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
349
|
+
|
|
350
|
+
export interface AccordionTitleProps extends LynxStyledElementProps {}
|
|
351
|
+
|
|
352
|
+
export const AccordionTitle = React.forwardRef<unknown, AccordionTitleProps>((props, ref) => {
|
|
353
|
+
const { children, className, ...nativeProps } = props;
|
|
354
|
+
const classes = useClassNames();
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<text
|
|
358
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
359
|
+
className={clsx(classes.title, className)}
|
|
360
|
+
{...nativeProps}
|
|
361
|
+
>
|
|
362
|
+
{children}
|
|
363
|
+
</text>
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
AccordionTitle.displayName = "AccordionTitle";
|
|
367
|
+
|
|
368
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
369
|
+
|
|
370
|
+
export interface AccordionDescriptionProps extends LynxStyledElementProps {}
|
|
371
|
+
|
|
372
|
+
export const AccordionDescription = React.forwardRef<unknown, AccordionDescriptionProps>(
|
|
373
|
+
(props, ref) => {
|
|
374
|
+
const { children, className, ...nativeProps } = props;
|
|
375
|
+
const classes = useClassNames();
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<text
|
|
379
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
380
|
+
className={clsx(classes.description, className)}
|
|
381
|
+
{...nativeProps}
|
|
382
|
+
>
|
|
383
|
+
{children}
|
|
384
|
+
</text>
|
|
385
|
+
);
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
AccordionDescription.displayName = "AccordionDescription";
|
|
389
|
+
|
|
390
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
391
|
+
|
|
392
|
+
export interface AccordionPrefixProps extends LynxStyledElementProps {}
|
|
393
|
+
|
|
394
|
+
export const AccordionPrefix = React.forwardRef<unknown, AccordionPrefixProps>((props, ref) => {
|
|
395
|
+
const { children, className, ...nativeProps } = props;
|
|
396
|
+
const classes = useClassNames();
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<view
|
|
400
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
401
|
+
className={clsx(classes.prefix, className)}
|
|
402
|
+
{...nativeProps}
|
|
403
|
+
>
|
|
404
|
+
{children}
|
|
405
|
+
</view>
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
AccordionPrefix.displayName = "AccordionPrefix";
|
|
409
|
+
|
|
410
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
411
|
+
|
|
412
|
+
export interface AccordionSuffixIconProps extends LynxStyledElementProps {
|
|
413
|
+
icon?: ReactElement<LynxIconElementProps>;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const AccordionSuffixIcon = React.forwardRef<unknown, AccordionSuffixIconProps>(
|
|
417
|
+
(props, ref) => {
|
|
418
|
+
const { icon, children, className, style, ...nativeProps } = props;
|
|
419
|
+
const context = useAccordionItemContext("AccordionSuffixIcon");
|
|
420
|
+
const classes = useClassNames();
|
|
421
|
+
const mergedClassName = clsx(classes.suffixIcon, className);
|
|
422
|
+
|
|
423
|
+
if (icon) {
|
|
424
|
+
return (
|
|
425
|
+
<view
|
|
426
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
427
|
+
accessibility-elements-hidden={true}
|
|
428
|
+
{...nativeProps}
|
|
429
|
+
>
|
|
430
|
+
<InternalIcon
|
|
431
|
+
icon={icon}
|
|
432
|
+
className={mergedClassName}
|
|
433
|
+
style={style}
|
|
434
|
+
deps={[context.open, context.disabled]}
|
|
435
|
+
/>
|
|
436
|
+
</view>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<view
|
|
442
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
443
|
+
className={mergedClassName}
|
|
444
|
+
style={style}
|
|
445
|
+
accessibility-elements-hidden={true}
|
|
446
|
+
{...nativeProps}
|
|
447
|
+
>
|
|
448
|
+
{children}
|
|
449
|
+
</view>
|
|
450
|
+
);
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
AccordionSuffixIcon.displayName = "AccordionSuffixIcon";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AccordionRoot,
|
|
3
|
+
AccordionItem,
|
|
4
|
+
AccordionHeader,
|
|
5
|
+
AccordionTrigger,
|
|
6
|
+
AccordionContent,
|
|
7
|
+
AccordionBody,
|
|
8
|
+
AccordionTitle,
|
|
9
|
+
AccordionDescription,
|
|
10
|
+
AccordionPrefix,
|
|
11
|
+
AccordionSuffixIcon,
|
|
12
|
+
type AccordionRootProps,
|
|
13
|
+
type AccordionItemProps,
|
|
14
|
+
type AccordionHeaderProps,
|
|
15
|
+
type AccordionTriggerProps,
|
|
16
|
+
type AccordionContentProps,
|
|
17
|
+
type AccordionBodyProps,
|
|
18
|
+
type AccordionTitleProps,
|
|
19
|
+
type AccordionDescriptionProps,
|
|
20
|
+
type AccordionPrefixProps,
|
|
21
|
+
type AccordionSuffixIconProps,
|
|
22
|
+
} from "./Accordion";
|
|
23
|
+
|
|
24
|
+
export * as Accordion from "./Accordion.namespace";
|
|
@@ -143,6 +143,30 @@ describe("BottomSheet", () => {
|
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it("releases the sheet's inline left inset so the positioner can center it", () => {
|
|
147
|
+
render(
|
|
148
|
+
<BottomSheet.Root>
|
|
149
|
+
<BottomSheet.Content />
|
|
150
|
+
</BottomSheet.Root>,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(sheetMocks.contentProps.at(-1)).toMatchObject({
|
|
154
|
+
style: { left: "auto" },
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("lets a user-provided style win over the released left inset", () => {
|
|
159
|
+
render(
|
|
160
|
+
<BottomSheet.Root>
|
|
161
|
+
<BottomSheet.Content style={{ left: "12px" }} />
|
|
162
|
+
</BottomSheet.Root>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(sheetMocks.contentProps.at(-1)).toMatchObject({
|
|
166
|
+
style: { left: "12px" },
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
146
170
|
it("skips motion-engine animations when Root has skipAnimation", () => {
|
|
147
171
|
render(
|
|
148
172
|
<BottomSheet.Root skipAnimation>
|