@seed-design/lynx-react 0.5.1 → 0.6.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/Checkbox/Checkbox.d.ts +26 -0
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/lib/components/Checkbox/Checkbox.jsx +1 -1
- package/lib/components/Icon/Icon.d.ts +5 -0
- package/lib/components/Icon/Icon.d.ts.map +1 -1
- package/lib/components/Icon/Icon.jsx +14 -3
- package/lib/components/MannerTemp/MannerTemp.d.ts +19 -0
- package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -0
- package/lib/components/MannerTemp/MannerTemp.jsx +47 -0
- package/lib/components/MannerTemp/index.d.ts +2 -0
- package/lib/components/MannerTemp/index.d.ts.map +1 -0
- package/lib/components/MannerTempBadge/MannerTempBadge.d.ts +13 -0
- package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -0
- package/lib/components/MannerTempBadge/MannerTempBadge.jsx +15 -0
- package/lib/components/MannerTempBadge/index.d.ts +2 -0
- package/lib/components/MannerTempBadge/index.d.ts.map +1 -0
- package/lib/components/NotificationBadge/NotificationBadge.d.ts +18 -0
- package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -0
- package/lib/components/NotificationBadge/NotificationBadge.jsx +34 -0
- package/lib/components/NotificationBadge/index.d.ts +2 -0
- package/lib/components/NotificationBadge/index.d.ts.map +1 -0
- package/lib/components/PageBanner/PageBanner.d.ts +35 -0
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -0
- package/lib/components/PageBanner/PageBanner.jsx +150 -0
- package/lib/components/PageBanner/PageBanner.namespace.d.ts +2 -0
- package/lib/components/PageBanner/PageBanner.namespace.d.ts.map +1 -0
- package/lib/components/PageBanner/PageBanner.namespace.js +14 -0
- package/lib/components/PageBanner/index.d.ts +3 -0
- package/lib/components/PageBanner/index.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts +9 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/lib/components/RadioGroup/RadioGroup.jsx +3 -2
- package/lib/components/SegmentedControl/SegmentedControl.d.ts +27 -0
- package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.jsx +120 -0
- package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts +2 -0
- package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.namespace.js +10 -0
- package/lib/components/SegmentedControl/index.d.ts +3 -0
- package/lib/components/SegmentedControl/index.d.ts.map +1 -0
- package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts +2 -0
- package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts.map +1 -0
- package/lib/components/SelectBox/CheckSelectBox.namespace.js +17 -0
- package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts +2 -0
- package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts.map +1 -0
- package/lib/components/SelectBox/RadioSelectBox.namespace.js +15 -0
- package/lib/components/SelectBox/SelectBox.d.ts +85 -0
- package/lib/components/SelectBox/SelectBox.d.ts.map +1 -0
- package/lib/components/SelectBox/SelectBox.jsx +237 -0
- package/lib/components/SelectBox/index.d.ts +4 -0
- package/lib/components/SelectBox/index.d.ts.map +1 -0
- package/lib/components/Tabs/Tabs.d.ts +1 -1
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.jsx +10 -3
- package/lib/components/Tabs/Tabs.utils.d.ts +1 -0
- package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.utils.js +4 -1
- package/lib/components/TextField/TextField.d.ts.map +1 -1
- package/lib/components/TextField/TextField.jsx +4 -0
- package/lib/components/index.d.ts +6 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/hooks/useIconColor.d.ts +1 -0
- package/lib/hooks/useIconColor.d.ts.map +1 -1
- package/lib/hooks/useIconColor.js +4 -1
- package/lib/index.js +11 -1
- package/package.json +3 -3
- package/src/components/Checkbox/Checkbox.tsx +1 -1
- package/src/components/Icon/Icon.test.tsx +25 -1
- package/src/components/Icon/Icon.tsx +22 -1
- package/src/components/MannerTemp/MannerTemp.test.tsx +41 -0
- package/src/components/MannerTemp/MannerTemp.tsx +85 -0
- package/src/components/MannerTemp/index.ts +1 -0
- package/src/components/MannerTempBadge/MannerTempBadge.test.tsx +35 -0
- package/src/components/MannerTempBadge/MannerTempBadge.tsx +34 -0
- package/src/components/MannerTempBadge/index.ts +1 -0
- package/src/components/NotificationBadge/NotificationBadge.test.tsx +28 -0
- package/src/components/NotificationBadge/NotificationBadge.tsx +81 -0
- package/src/components/NotificationBadge/index.ts +1 -0
- package/src/components/PageBanner/PageBanner.namespace.ts +16 -0
- package/src/components/PageBanner/PageBanner.test.tsx +124 -0
- package/src/components/PageBanner/PageBanner.tsx +340 -0
- package/src/components/PageBanner/index.ts +18 -0
- package/src/components/RadioGroup/RadioGroup.tsx +3 -2
- package/src/components/SegmentedControl/SegmentedControl.namespace.ts +8 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +81 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +236 -0
- package/src/components/SegmentedControl/index.ts +10 -0
- package/src/components/SelectBox/CheckSelectBox.namespace.ts +22 -0
- package/src/components/SelectBox/RadioSelectBox.namespace.ts +18 -0
- package/src/components/SelectBox/SelectBox.test.tsx +169 -0
- package/src/components/SelectBox/SelectBox.tsx +533 -0
- package/src/components/SelectBox/index.ts +3 -0
- package/src/components/Tabs/Tabs.test.tsx +26 -1
- package/src/components/Tabs/Tabs.tsx +14 -2
- package/src/components/Tabs/Tabs.utils.ts +7 -0
- package/src/components/TextField/TextField.test.tsx +48 -0
- package/src/components/TextField/TextField.tsx +8 -1
- package/src/components/index.ts +6 -0
- package/src/hooks/useIconColor.ts +6 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { fireEvent, render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import * as PageBanner from "./PageBanner.namespace";
|
|
6
|
+
|
|
7
|
+
function getRenderedRoot() {
|
|
8
|
+
const root = elementTree.root;
|
|
9
|
+
|
|
10
|
+
if (!root) {
|
|
11
|
+
throw new Error("Expected Lynx render root to exist.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getPageBannerRoot() {
|
|
18
|
+
const root = getRenderedRoot();
|
|
19
|
+
|
|
20
|
+
if (root.classList.contains("seed-page-banner__root")) return root;
|
|
21
|
+
|
|
22
|
+
const pageBannerRoot = root.querySelector<HTMLElement>(".seed-page-banner__root");
|
|
23
|
+
if (!pageBannerRoot) {
|
|
24
|
+
throw new Error("Expected PageBanner root to exist.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return pageBannerRoot;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("PageBanner", () => {
|
|
31
|
+
it("renders compound slots with the default neutral weak style", () => {
|
|
32
|
+
render(
|
|
33
|
+
<PageBanner.Root>
|
|
34
|
+
<PageBanner.Content>
|
|
35
|
+
<PageBanner.Body>
|
|
36
|
+
<PageBanner.Title>Title</PageBanner.Title>
|
|
37
|
+
<PageBanner.Description>Description</PageBanner.Description>
|
|
38
|
+
</PageBanner.Body>
|
|
39
|
+
<PageBanner.Button>Action</PageBanner.Button>
|
|
40
|
+
</PageBanner.Content>
|
|
41
|
+
<PageBanner.CloseButton accessibility-label="Close" />
|
|
42
|
+
</PageBanner.Root>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const root = getPageBannerRoot();
|
|
46
|
+
|
|
47
|
+
expect(root).toHaveClass("seed-page-banner__root--tone_neutral-variant_weak");
|
|
48
|
+
expect(root.querySelector(".seed-page-banner__content")?.tagName.toLowerCase()).toBe("view");
|
|
49
|
+
expect(root.querySelector(".seed-page-banner__body")?.tagName.toLowerCase()).toBe("text");
|
|
50
|
+
expect(root.querySelector(".seed-page-banner__title")).toHaveClass(
|
|
51
|
+
"seed-page-banner__title--tone_neutral-variant_weak",
|
|
52
|
+
);
|
|
53
|
+
expect(root.querySelector(".seed-page-banner__description")).toHaveClass(
|
|
54
|
+
"seed-page-banner__description--tone_neutral-variant_weak",
|
|
55
|
+
);
|
|
56
|
+
expect(root.querySelector(".seed-page-banner__button")).toHaveAttribute(
|
|
57
|
+
"accessibility-traits",
|
|
58
|
+
"button",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("tracks pressed state and maps actionable accessibility", () => {
|
|
63
|
+
const onTap = vi.fn();
|
|
64
|
+
|
|
65
|
+
render(
|
|
66
|
+
<PageBanner.Root tone="positive" variant="solid" bindtap={onTap}>
|
|
67
|
+
<PageBanner.Content>
|
|
68
|
+
<PageBanner.Body>
|
|
69
|
+
<PageBanner.Description>Actionable</PageBanner.Description>
|
|
70
|
+
</PageBanner.Body>
|
|
71
|
+
</PageBanner.Content>
|
|
72
|
+
</PageBanner.Root>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const root = getPageBannerRoot();
|
|
76
|
+
|
|
77
|
+
expect(root).toHaveAttribute("accessibility-element", "true");
|
|
78
|
+
expect(root).toHaveAttribute("accessibility-traits", "button");
|
|
79
|
+
|
|
80
|
+
fireEvent.touchstart(root, {});
|
|
81
|
+
expect(root).toHaveClass("seed-page-banner__root--tone_positive-variant_solid-pressed_true");
|
|
82
|
+
|
|
83
|
+
fireEvent.tap(root);
|
|
84
|
+
expect(onTap).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("dismisses an uncontrolled banner and composes the close tap handler", () => {
|
|
88
|
+
const onTap = vi.fn();
|
|
89
|
+
const onDismiss = vi.fn();
|
|
90
|
+
|
|
91
|
+
render(
|
|
92
|
+
<PageBanner.Root onDismiss={onDismiss}>
|
|
93
|
+
<PageBanner.CloseButton bindtap={onTap} accessibility-label="Close" />
|
|
94
|
+
</PageBanner.Root>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const closeButton = getPageBannerRoot().querySelector(".seed-page-banner__closeButton");
|
|
98
|
+
if (!closeButton) throw new Error("Expected close button to exist.");
|
|
99
|
+
|
|
100
|
+
fireEvent.tap(closeButton);
|
|
101
|
+
|
|
102
|
+
expect(onTap).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(getRenderedRoot().querySelector(".seed-page-banner__root")).not.toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("notifies without hiding a controlled banner", () => {
|
|
108
|
+
const onDismiss = vi.fn();
|
|
109
|
+
|
|
110
|
+
render(
|
|
111
|
+
<PageBanner.Root open onDismiss={onDismiss}>
|
|
112
|
+
<PageBanner.CloseButton accessibility-label="Close" />
|
|
113
|
+
</PageBanner.Root>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const closeButton = getPageBannerRoot().querySelector(".seed-page-banner__closeButton");
|
|
117
|
+
if (!closeButton) throw new Error("Expected close button to exist.");
|
|
118
|
+
|
|
119
|
+
fireEvent.tap(closeButton);
|
|
120
|
+
|
|
121
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(getPageBannerRoot()).toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { pageBanner, type PageBannerVariantProps } from "@seed-design/lynx-css/recipes/page-banner";
|
|
2
|
+
import * as React from "@lynx-js/react";
|
|
3
|
+
import { useMemoizedFn } from "@lynx-js/lynx-ui-common";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
|
|
6
|
+
import { useControllableState } from "../../hooks/useControllableState";
|
|
7
|
+
import { usePressTap } from "../../hooks/usePressTap";
|
|
8
|
+
import type {
|
|
9
|
+
LynxAccessibilityProps,
|
|
10
|
+
LynxPressableProps,
|
|
11
|
+
LynxStyledElementProps,
|
|
12
|
+
LynxTextRef,
|
|
13
|
+
LynxViewProps,
|
|
14
|
+
LynxViewRef,
|
|
15
|
+
} from "../../types";
|
|
16
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
17
|
+
import { IconSlotProvider } from "../Icon/Icon";
|
|
18
|
+
|
|
19
|
+
const { ClassNamesProvider, PropsProvider, useClassNames, useProps } =
|
|
20
|
+
createSlotRecipeContext(pageBanner);
|
|
21
|
+
|
|
22
|
+
type TapHandler = NonNullable<LynxViewProps["bindtap"]>;
|
|
23
|
+
|
|
24
|
+
interface PageBannerContextValue {
|
|
25
|
+
dismiss: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PageBannerContext = React.createContext<PageBannerContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
function usePageBannerContext(consumer: string) {
|
|
31
|
+
const context = React.useContext(PageBannerContext);
|
|
32
|
+
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error(`<${consumer}/> must be rendered inside <PageBannerRoot/>.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return context;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @platform Lynx
|
|
44
|
+
*
|
|
45
|
+
* 웹 대비 차이:
|
|
46
|
+
* - `asChild`와 DOM 이벤트 대신 Lynx native `<view>`와 `bindtap`을 사용합니다.
|
|
47
|
+
* - 웹 focus ring과 동적 scale feedback은 지원하지 않습니다.
|
|
48
|
+
*/
|
|
49
|
+
export interface PageBannerRootProps
|
|
50
|
+
extends Omit<PageBannerVariantProps, "pressed" | "closeButtonPressed">,
|
|
51
|
+
LynxStyledElementProps,
|
|
52
|
+
LynxPressableProps,
|
|
53
|
+
LynxAccessibilityProps {
|
|
54
|
+
defaultOpen?: boolean;
|
|
55
|
+
open?: boolean;
|
|
56
|
+
onDismiss?: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const PageBannerRoot = React.forwardRef<unknown, PageBannerRootProps>((props, ref) => {
|
|
60
|
+
if (
|
|
61
|
+
process.env.NODE_ENV !== "production" &&
|
|
62
|
+
props.variant === "solid" &&
|
|
63
|
+
props.tone === "magic"
|
|
64
|
+
) {
|
|
65
|
+
console.error(
|
|
66
|
+
'`magic` tone is not available for `solid` variant in PageBanner components. Please use variant="weak" or a different tone instead.',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [variantProps, otherProps] = pageBanner.splitVariantProps(props);
|
|
71
|
+
const {
|
|
72
|
+
children,
|
|
73
|
+
className,
|
|
74
|
+
style,
|
|
75
|
+
defaultOpen = true,
|
|
76
|
+
open: openProp,
|
|
77
|
+
onDismiss,
|
|
78
|
+
bindtap,
|
|
79
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
80
|
+
"accessibility-element": accessibilityElement,
|
|
81
|
+
"accessibility-traits": accessibilityTraits,
|
|
82
|
+
...nativeProps
|
|
83
|
+
} = otherProps;
|
|
84
|
+
const [open, setOpen] = useControllableState({
|
|
85
|
+
value: openProp,
|
|
86
|
+
defaultValue: defaultOpen,
|
|
87
|
+
});
|
|
88
|
+
const isInteractive = bindtap != null || mainThreadBindtap != null;
|
|
89
|
+
const pressTap = usePressTap({
|
|
90
|
+
disabled: !isInteractive,
|
|
91
|
+
onTap: bindtap,
|
|
92
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
93
|
+
});
|
|
94
|
+
const classNames = pageBanner({ ...variantProps, pressed: pressTap.pressed });
|
|
95
|
+
const dismiss = useMemoizedFn(() => {
|
|
96
|
+
if (!open) return;
|
|
97
|
+
|
|
98
|
+
setOpen(false);
|
|
99
|
+
onDismiss?.();
|
|
100
|
+
});
|
|
101
|
+
const contextValue = React.useMemo(() => ({ dismiss }), [dismiss]);
|
|
102
|
+
const iconSlotContextValue = React.useMemo(
|
|
103
|
+
() => ({
|
|
104
|
+
classNames: {
|
|
105
|
+
prefixIcon: classNames.prefixIcon,
|
|
106
|
+
suffixIcon: classNames.suffixIcon,
|
|
107
|
+
},
|
|
108
|
+
deps: [variantProps.tone ?? "neutral", variantProps.variant ?? "weak", pressTap.pressed],
|
|
109
|
+
}),
|
|
110
|
+
[
|
|
111
|
+
classNames.prefixIcon,
|
|
112
|
+
classNames.suffixIcon,
|
|
113
|
+
pressTap.pressed,
|
|
114
|
+
variantProps.tone,
|
|
115
|
+
variantProps.variant,
|
|
116
|
+
],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!open) return null;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<PageBannerContext.Provider value={contextValue}>
|
|
123
|
+
<ClassNamesProvider value={classNames}>
|
|
124
|
+
<PropsProvider value={variantProps}>
|
|
125
|
+
<IconSlotProvider value={iconSlotContextValue}>
|
|
126
|
+
<view
|
|
127
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
128
|
+
{...nativeProps}
|
|
129
|
+
{...(isInteractive ? pressTap : {})}
|
|
130
|
+
accessibility-element={accessibilityElement ?? (isInteractive ? true : undefined)}
|
|
131
|
+
accessibility-traits={accessibilityTraits ?? (isInteractive ? "button" : undefined)}
|
|
132
|
+
className={clsx(classNames.root, className)}
|
|
133
|
+
style={style}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</view>
|
|
137
|
+
</IconSlotProvider>
|
|
138
|
+
</PropsProvider>
|
|
139
|
+
</ClassNamesProvider>
|
|
140
|
+
</PageBannerContext.Provider>
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
PageBannerRoot.displayName = "PageBannerRoot";
|
|
144
|
+
|
|
145
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
146
|
+
|
|
147
|
+
export interface PageBannerContentProps extends LynxStyledElementProps, LynxAccessibilityProps {}
|
|
148
|
+
|
|
149
|
+
export const PageBannerContent = React.forwardRef<unknown, PageBannerContentProps>((props, ref) => {
|
|
150
|
+
const { children, className, style, ...nativeProps } = props;
|
|
151
|
+
const classNames = useClassNames();
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<view
|
|
155
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
156
|
+
{...nativeProps}
|
|
157
|
+
className={clsx(classNames.content, className)}
|
|
158
|
+
style={style}
|
|
159
|
+
>
|
|
160
|
+
{children}
|
|
161
|
+
</view>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
PageBannerContent.displayName = "PageBannerContent";
|
|
165
|
+
|
|
166
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
167
|
+
|
|
168
|
+
export interface PageBannerBodyProps extends LynxStyledElementProps, LynxAccessibilityProps {}
|
|
169
|
+
|
|
170
|
+
export const PageBannerBody = React.forwardRef<unknown, PageBannerBodyProps>((props, ref) => {
|
|
171
|
+
const { children, className, style, ...nativeProps } = props;
|
|
172
|
+
const classNames = useClassNames();
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<text
|
|
176
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
177
|
+
{...nativeProps}
|
|
178
|
+
className={clsx(classNames.body, className)}
|
|
179
|
+
style={style}
|
|
180
|
+
>
|
|
181
|
+
{children}
|
|
182
|
+
</text>
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
PageBannerBody.displayName = "PageBannerBody";
|
|
186
|
+
|
|
187
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
188
|
+
|
|
189
|
+
export interface PageBannerTitleProps extends LynxStyledElementProps, LynxAccessibilityProps {}
|
|
190
|
+
|
|
191
|
+
export const PageBannerTitle = React.forwardRef<unknown, PageBannerTitleProps>((props, ref) => {
|
|
192
|
+
const { children, className, style, ...nativeProps } = props;
|
|
193
|
+
const classNames = useClassNames();
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<text
|
|
197
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
198
|
+
{...nativeProps}
|
|
199
|
+
className={clsx(classNames.title, className)}
|
|
200
|
+
style={style}
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
{" "}
|
|
204
|
+
</text>
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
PageBannerTitle.displayName = "PageBannerTitle";
|
|
208
|
+
|
|
209
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
210
|
+
|
|
211
|
+
export interface PageBannerDescriptionProps
|
|
212
|
+
extends LynxStyledElementProps,
|
|
213
|
+
LynxAccessibilityProps {}
|
|
214
|
+
|
|
215
|
+
export const PageBannerDescription = React.forwardRef<unknown, PageBannerDescriptionProps>(
|
|
216
|
+
(props, ref) => {
|
|
217
|
+
const { children, className, style, ...nativeProps } = props;
|
|
218
|
+
const classNames = useClassNames();
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<text
|
|
222
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
223
|
+
{...nativeProps}
|
|
224
|
+
className={clsx(classNames.description, className)}
|
|
225
|
+
style={style}
|
|
226
|
+
>
|
|
227
|
+
{children}
|
|
228
|
+
</text>
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
PageBannerDescription.displayName = "PageBannerDescription";
|
|
233
|
+
|
|
234
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
235
|
+
|
|
236
|
+
export interface PageBannerButtonProps
|
|
237
|
+
extends LynxStyledElementProps,
|
|
238
|
+
LynxPressableProps,
|
|
239
|
+
LynxAccessibilityProps {}
|
|
240
|
+
|
|
241
|
+
export const PageBannerButton = React.forwardRef<unknown, PageBannerButtonProps>((props, ref) => {
|
|
242
|
+
const {
|
|
243
|
+
children,
|
|
244
|
+
className,
|
|
245
|
+
style,
|
|
246
|
+
"accessibility-element": accessibilityElement = true,
|
|
247
|
+
"accessibility-traits": accessibilityTraits = "button",
|
|
248
|
+
...nativeProps
|
|
249
|
+
} = props;
|
|
250
|
+
const classNames = useClassNames();
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<text
|
|
254
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
255
|
+
{...nativeProps}
|
|
256
|
+
accessibility-element={accessibilityElement}
|
|
257
|
+
accessibility-traits={accessibilityTraits}
|
|
258
|
+
className={clsx(classNames.button, className)}
|
|
259
|
+
style={style}
|
|
260
|
+
>
|
|
261
|
+
{children}
|
|
262
|
+
</text>
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
PageBannerButton.displayName = "PageBannerButton";
|
|
266
|
+
|
|
267
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
268
|
+
|
|
269
|
+
export interface PageBannerCloseButtonProps
|
|
270
|
+
extends LynxStyledElementProps,
|
|
271
|
+
LynxPressableProps,
|
|
272
|
+
LynxAccessibilityProps {}
|
|
273
|
+
|
|
274
|
+
export const PageBannerCloseButton = React.forwardRef<unknown, PageBannerCloseButtonProps>(
|
|
275
|
+
(props, ref) => {
|
|
276
|
+
const {
|
|
277
|
+
children,
|
|
278
|
+
className,
|
|
279
|
+
style,
|
|
280
|
+
bindtap,
|
|
281
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
282
|
+
"accessibility-element": accessibilityElement = true,
|
|
283
|
+
"accessibility-label": accessibilityLabel,
|
|
284
|
+
"accessibility-traits": accessibilityTraits = "button",
|
|
285
|
+
...nativeProps
|
|
286
|
+
} = props;
|
|
287
|
+
const parentVariantProps = useProps() ?? {};
|
|
288
|
+
const { dismiss } = usePageBannerContext("PageBannerCloseButton");
|
|
289
|
+
const handleTap = useMemoizedFn<TapHandler>((event) => {
|
|
290
|
+
bindtap?.(event);
|
|
291
|
+
dismiss();
|
|
292
|
+
});
|
|
293
|
+
const pressTap = usePressTap({
|
|
294
|
+
onTap: handleTap,
|
|
295
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
296
|
+
});
|
|
297
|
+
const classNames = pageBanner({
|
|
298
|
+
...parentVariantProps,
|
|
299
|
+
closeButtonPressed: pressTap.pressed,
|
|
300
|
+
});
|
|
301
|
+
const iconSlotContextValue = React.useMemo(
|
|
302
|
+
() => ({
|
|
303
|
+
classNames: { suffixIcon: classNames.closeButtonIcon },
|
|
304
|
+
deps: [
|
|
305
|
+
parentVariantProps.tone ?? "neutral",
|
|
306
|
+
parentVariantProps.variant ?? "weak",
|
|
307
|
+
pressTap.pressed,
|
|
308
|
+
],
|
|
309
|
+
}),
|
|
310
|
+
[
|
|
311
|
+
classNames.closeButtonIcon,
|
|
312
|
+
parentVariantProps.tone,
|
|
313
|
+
parentVariantProps.variant,
|
|
314
|
+
pressTap.pressed,
|
|
315
|
+
],
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (process.env.NODE_ENV !== "production" && accessibilityElement && !accessibilityLabel) {
|
|
319
|
+
console.warn("PageBannerCloseButton requires `accessibility-label` for accessibility.");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<IconSlotProvider value={iconSlotContextValue}>
|
|
324
|
+
<view
|
|
325
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
326
|
+
{...nativeProps}
|
|
327
|
+
{...pressTap}
|
|
328
|
+
accessibility-element={accessibilityElement}
|
|
329
|
+
accessibility-label={accessibilityLabel}
|
|
330
|
+
accessibility-traits={accessibilityTraits}
|
|
331
|
+
className={clsx(classNames.closeButton, className)}
|
|
332
|
+
style={style}
|
|
333
|
+
>
|
|
334
|
+
{children}
|
|
335
|
+
</view>
|
|
336
|
+
</IconSlotProvider>
|
|
337
|
+
);
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
PageBannerCloseButton.displayName = "PageBannerCloseButton";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PageBannerBody,
|
|
3
|
+
PageBannerButton,
|
|
4
|
+
PageBannerCloseButton,
|
|
5
|
+
PageBannerContent,
|
|
6
|
+
PageBannerDescription,
|
|
7
|
+
PageBannerRoot,
|
|
8
|
+
PageBannerTitle,
|
|
9
|
+
type PageBannerBodyProps,
|
|
10
|
+
type PageBannerButtonProps,
|
|
11
|
+
type PageBannerCloseButtonProps,
|
|
12
|
+
type PageBannerContentProps,
|
|
13
|
+
type PageBannerDescriptionProps,
|
|
14
|
+
type PageBannerRootProps,
|
|
15
|
+
type PageBannerTitleProps,
|
|
16
|
+
} from "./PageBanner";
|
|
17
|
+
|
|
18
|
+
export * as PageBanner from "./PageBanner.namespace";
|
|
@@ -60,7 +60,7 @@ interface RadioGroupItemContextValue {
|
|
|
60
60
|
|
|
61
61
|
const RadioGroupItemContext = React.createContext<RadioGroupItemContextValue | null>(null);
|
|
62
62
|
|
|
63
|
-
function useRadioGroupItemContext(consumer: string): RadioGroupItemContextValue {
|
|
63
|
+
export function useRadioGroupItemContext(consumer: string): RadioGroupItemContextValue {
|
|
64
64
|
const ctx = React.useContext(RadioGroupItemContext);
|
|
65
65
|
if (!ctx) {
|
|
66
66
|
throw new Error(`<${consumer}/> must be rendered inside <RadioGroupItem/>.`);
|
|
@@ -232,6 +232,7 @@ export const RadioGroupItemControl = React.forwardRef<unknown, RadioGroupItemCon
|
|
|
232
232
|
pressed: itemContext.pressed,
|
|
233
233
|
};
|
|
234
234
|
const classes = radiomark(radiomarkVariantProps);
|
|
235
|
+
const controlClassName = radio(groupContext.radioVariantProps).control;
|
|
235
236
|
|
|
236
237
|
return (
|
|
237
238
|
<RadiomarkControlContext.Provider
|
|
@@ -239,7 +240,7 @@ export const RadioGroupItemControl = React.forwardRef<unknown, RadioGroupItemCon
|
|
|
239
240
|
>
|
|
240
241
|
<view
|
|
241
242
|
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
242
|
-
className={clsx(classes.root, className)}
|
|
243
|
+
className={clsx(classes.root, controlClassName, className)}
|
|
243
244
|
{...nativeProps}
|
|
244
245
|
>
|
|
245
246
|
{children}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SegmentedControlIndicator as Indicator,
|
|
3
|
+
SegmentedControlItem as Item,
|
|
4
|
+
SegmentedControlRoot as Root,
|
|
5
|
+
type SegmentedControlIndicatorProps as IndicatorProps,
|
|
6
|
+
type SegmentedControlItemProps as ItemProps,
|
|
7
|
+
type SegmentedControlRootProps as RootProps,
|
|
8
|
+
} from "./SegmentedControl";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { fireEvent, render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import * as SegmentedControl from "./SegmentedControl.namespace";
|
|
6
|
+
|
|
7
|
+
function BasicSegmentedControl(props: {
|
|
8
|
+
value?: string;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
onValueChange?: (value: string) => void;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<SegmentedControl.Root {...props} accessibility-label="정렬 기준">
|
|
14
|
+
<SegmentedControl.Item value="hot">인기순</SegmentedControl.Item>
|
|
15
|
+
<SegmentedControl.Item value="new">최신순</SegmentedControl.Item>
|
|
16
|
+
<SegmentedControl.Indicator />
|
|
17
|
+
</SegmentedControl.Root>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getItem(container: HTMLElement, label: string) {
|
|
22
|
+
const item = Array.from(
|
|
23
|
+
container.querySelectorAll<HTMLElement>(".seed-segmented-control__item"),
|
|
24
|
+
).find((element) => element.textContent === label);
|
|
25
|
+
if (!item) throw new Error(`Expected item for ${label} to exist.`);
|
|
26
|
+
return item;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("SegmentedControl", () => {
|
|
30
|
+
it("changes an uncontrolled value when an item is tapped", () => {
|
|
31
|
+
const onValueChange = vi.fn();
|
|
32
|
+
const { container } = render(
|
|
33
|
+
<BasicSegmentedControl defaultValue="hot" onValueChange={onValueChange} />,
|
|
34
|
+
);
|
|
35
|
+
const hot = getItem(container, "인기순");
|
|
36
|
+
const latest = getItem(container, "최신순");
|
|
37
|
+
|
|
38
|
+
expect(hot).toHaveAttribute("accessibility-value", "selected");
|
|
39
|
+
expect(latest).toHaveAttribute("accessibility-value", "not selected");
|
|
40
|
+
|
|
41
|
+
fireEvent.tap(latest);
|
|
42
|
+
|
|
43
|
+
expect(onValueChange).toHaveBeenCalledWith("new");
|
|
44
|
+
expect(latest).toHaveAttribute("accessibility-value", "selected");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not select a disabled item", () => {
|
|
48
|
+
const onValueChange = vi.fn();
|
|
49
|
+
const { container } = render(
|
|
50
|
+
<SegmentedControl.Root defaultValue="hot" onValueChange={onValueChange}>
|
|
51
|
+
<SegmentedControl.Item value="hot">인기순</SegmentedControl.Item>
|
|
52
|
+
<SegmentedControl.Item value="new" disabled>
|
|
53
|
+
최신순
|
|
54
|
+
</SegmentedControl.Item>
|
|
55
|
+
</SegmentedControl.Root>,
|
|
56
|
+
);
|
|
57
|
+
const disabledItem = getItem(container, "최신순");
|
|
58
|
+
|
|
59
|
+
fireEvent.tap(disabledItem);
|
|
60
|
+
|
|
61
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
62
|
+
expect(disabledItem).toHaveAttribute("accessibility-traits", "disabled");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("keeps the press-start selection on the fading item background", () => {
|
|
66
|
+
const { container } = render(<BasicSegmentedControl defaultValue="hot" />);
|
|
67
|
+
const latest = getItem(container, "최신순");
|
|
68
|
+
const getBackground = () =>
|
|
69
|
+
latest.querySelector<HTMLElement>(".seed-segmented-control__itemBackground");
|
|
70
|
+
|
|
71
|
+
fireEvent.touchstart(latest, {});
|
|
72
|
+
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_true");
|
|
73
|
+
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
|
|
74
|
+
|
|
75
|
+
fireEvent.touchend(latest, {});
|
|
76
|
+
fireEvent.tap(latest);
|
|
77
|
+
|
|
78
|
+
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_false");
|
|
79
|
+
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
|
|
80
|
+
});
|
|
81
|
+
});
|