@ubie/vitals-ui-consumer 0.0.1

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.
Files changed (170) hide show
  1. package/.storybook/main.ts +14 -0
  2. package/.storybook/preview.tsx +25 -0
  3. package/.storybook/vitest.setup.ts +7 -0
  4. package/dist/chunk-DKo7XVKm.mjs +33 -0
  5. package/dist/index.d.mts +1720 -0
  6. package/dist/index.d.mts.map +1 -0
  7. package/dist/index.mjs +10594 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/style.css +2299 -0
  10. package/package.json +47 -0
  11. package/src/components/Accordion/Accordion.module.css +75 -0
  12. package/src/components/Accordion/Accordion.spec.tsx +18 -0
  13. package/src/components/Accordion/Accordion.stories.tsx +61 -0
  14. package/src/components/Accordion/Accordion.tsx +89 -0
  15. package/src/components/ActionHalfModal/ActionHalfModal.module.css +180 -0
  16. package/src/components/ActionHalfModal/ActionHalfModal.spec.tsx +57 -0
  17. package/src/components/ActionHalfModal/ActionHalfModal.stories.tsx +469 -0
  18. package/src/components/ActionHalfModal/ActionHalfModal.tsx +269 -0
  19. package/src/components/ActionModal/ActionModal.module.css +145 -0
  20. package/src/components/ActionModal/ActionModal.spec.tsx +57 -0
  21. package/src/components/ActionModal/ActionModal.stories.tsx +302 -0
  22. package/src/components/ActionModal/ActionModal.tsx +232 -0
  23. package/src/components/Bold/Bold.module.css +4 -0
  24. package/src/components/Bold/Bold.spec.tsx +24 -0
  25. package/src/components/Bold/Bold.stories.tsx +54 -0
  26. package/src/components/Bold/Bold.tsx +31 -0
  27. package/src/components/Box/Box.module.css +46 -0
  28. package/src/components/Box/Box.spec.tsx +188 -0
  29. package/src/components/Box/Box.tsx +242 -0
  30. package/src/components/Button/Button.module.css +261 -0
  31. package/src/components/Button/Button.spec.tsx +82 -0
  32. package/src/components/Button/Button.tsx +99 -0
  33. package/src/components/Button/ButtonTypes.ts +107 -0
  34. package/src/components/Button/LinkButton.spec.tsx +86 -0
  35. package/src/components/Button/LinkButton.tsx +80 -0
  36. package/src/components/Button/VariantIcon.tsx +20 -0
  37. package/src/components/Button/useIcon.tsx +16 -0
  38. package/src/components/ButtonCard/ButtonCard.module.css +35 -0
  39. package/src/components/ButtonCard/ButtonCard.spec.tsx +18 -0
  40. package/src/components/ButtonCard/ButtonCard.stories.tsx +54 -0
  41. package/src/components/ButtonCard/ButtonCard.tsx +18 -0
  42. package/src/components/Center/Center.module.css +19 -0
  43. package/src/components/Center/Center.spec.tsx +143 -0
  44. package/src/components/Center/Center.tsx +108 -0
  45. package/src/components/Checkbox/Checkbox.module.css +124 -0
  46. package/src/components/Checkbox/Checkbox.spec.tsx +17 -0
  47. package/src/components/Checkbox/Checkbox.stories.tsx +213 -0
  48. package/src/components/Checkbox/Checkbox.tsx +50 -0
  49. package/src/components/CheckboxCard/CheckboxCard.module.css +102 -0
  50. package/src/components/CheckboxCard/CheckboxCard.spec.tsx +16 -0
  51. package/src/components/CheckboxCard/CheckboxCard.stories.tsx +205 -0
  52. package/src/components/CheckboxCard/CheckboxCard.tsx +53 -0
  53. package/src/components/CheckboxGroup/CheckboxGroup.module.css +16 -0
  54. package/src/components/CheckboxGroup/CheckboxGroup.spec.tsx +17 -0
  55. package/src/components/CheckboxGroup/CheckboxGroup.tsx +64 -0
  56. package/src/components/Color/Color.module.css +3 -0
  57. package/src/components/Color/Color.spec.tsx +24 -0
  58. package/src/components/Color/Color.stories.tsx +71 -0
  59. package/src/components/Color/Color.tsx +28 -0
  60. package/src/components/Divider/Divider.module.css +9 -0
  61. package/src/components/Divider/Divider.spec.tsx +42 -0
  62. package/src/components/Divider/Divider.stories.tsx +77 -0
  63. package/src/components/Divider/Divider.tsx +49 -0
  64. package/src/components/ErrorMessage/ErrorMessage.module.css +8 -0
  65. package/src/components/ErrorMessage/ErrorMessage.spec.tsx +12 -0
  66. package/src/components/ErrorMessage/ErrorMessage.tsx +20 -0
  67. package/src/components/Flex/Flex.module.css +24 -0
  68. package/src/components/Flex/Flex.spec.tsx +188 -0
  69. package/src/components/Flex/Flex.tsx +173 -0
  70. package/src/components/FlexItem/FlexItem.module.css +14 -0
  71. package/src/components/FlexItem/FlexItem.spec.tsx +84 -0
  72. package/src/components/FlexItem/FlexItem.tsx +106 -0
  73. package/src/components/Heading/Heading.module.css +131 -0
  74. package/src/components/Heading/Heading.tsx +86 -0
  75. package/src/components/HelperMessage/HelperMessage.module.css +8 -0
  76. package/src/components/HelperMessage/HelperMessage.tsx +15 -0
  77. package/src/components/Icon/Icon.module.css +6 -0
  78. package/src/components/Icon/Icon.spec.tsx +24 -0
  79. package/src/components/Icon/Icon.stories.tsx +100 -0
  80. package/src/components/Icon/Icon.tsx +101 -0
  81. package/src/components/Input/Input.module.css +51 -0
  82. package/src/components/Input/Input.spec.tsx +14 -0
  83. package/src/components/Input/Input.tsx +27 -0
  84. package/src/components/Label/Label.module.css +14 -0
  85. package/src/components/Label/Label.tsx +39 -0
  86. package/src/components/LinkCard/LinkCard.module.css +72 -0
  87. package/src/components/LinkCard/LinkCard.tsx +96 -0
  88. package/src/components/MessageHalfModal/MessageHalfModal.module.css +181 -0
  89. package/src/components/MessageHalfModal/MessageHalfModal.spec.tsx +73 -0
  90. package/src/components/MessageHalfModal/MessageHalfModal.stories.tsx +242 -0
  91. package/src/components/MessageHalfModal/MessageHalfModal.tsx +194 -0
  92. package/src/components/MessageModal/MessageModal.module.css +149 -0
  93. package/src/components/MessageModal/MessageModal.spec.tsx +57 -0
  94. package/src/components/MessageModal/MessageModal.stories.tsx +223 -0
  95. package/src/components/MessageModal/MessageModal.tsx +178 -0
  96. package/src/components/Pre/Pre.module.css +8 -0
  97. package/src/components/Pre/Pre.spec.tsx +11 -0
  98. package/src/components/Pre/Pre.stories.tsx +76 -0
  99. package/src/components/Pre/Pre.tsx +40 -0
  100. package/src/components/RadioButton/RadioButton.module.css +92 -0
  101. package/src/components/RadioButton/RadioButton.spec.tsx +25 -0
  102. package/src/components/RadioButton/RadioButton.tsx +55 -0
  103. package/src/components/RadioCard/RadioCard.module.css +109 -0
  104. package/src/components/RadioCard/RadioCard.tsx +61 -0
  105. package/src/components/RadioGroup/RadioGroup.module.css +16 -0
  106. package/src/components/RadioGroup/RadioGroup.spec.tsx +17 -0
  107. package/src/components/RadioGroup/RadioGroup.tsx +60 -0
  108. package/src/components/Select/Select.module.css +70 -0
  109. package/src/components/Select/Select.spec.tsx +12 -0
  110. package/src/components/Select/Select.tsx +56 -0
  111. package/src/components/Stack/Stack.module.css +10 -0
  112. package/src/components/Stack/Stack.spec.tsx +177 -0
  113. package/src/components/Stack/Stack.tsx +151 -0
  114. package/src/components/Stepper/Stepper.module.css +137 -0
  115. package/src/components/Stepper/Stepper.spec.tsx +198 -0
  116. package/src/components/Stepper/Stepper.stories.tsx +192 -0
  117. package/src/components/Stepper/Stepper.tsx +70 -0
  118. package/src/components/Stepper/StepperItem.tsx +113 -0
  119. package/src/components/Text/Text.module.css +168 -0
  120. package/src/components/Text/Text.tsx +192 -0
  121. package/src/components/TextArea/TextArea.module.css +46 -0
  122. package/src/components/TextArea/TextArea.spec.tsx +13 -0
  123. package/src/components/TextArea/TextArea.tsx +29 -0
  124. package/src/components/Toggle/Toggle.module.css +71 -0
  125. package/src/components/Toggle/Toggle.spec.tsx +21 -0
  126. package/src/components/Toggle/Toggle.tsx +56 -0
  127. package/src/font.ts +2 -0
  128. package/src/hooks/useScrollable.ts +58 -0
  129. package/src/icons/AppleIcon.tsx +14 -0
  130. package/src/icons/GoogleIcon.tsx +27 -0
  131. package/src/icons/LINEIcon.tsx +16 -0
  132. package/src/index.ts +35 -0
  133. package/src/sharedComponents/RequiredLabel/RequiredLabel.module.css +10 -0
  134. package/src/sharedComponents/RequiredLabel/RequiredLabel.tsx +8 -0
  135. package/src/sharedComponents/VisuallyHidden/VisuallyHidden.module.css +15 -0
  136. package/src/sharedComponents/VisuallyHidden/VisuallyHidden.tsx +22 -0
  137. package/src/stories/Accordion.stories.portable.ts +4 -0
  138. package/src/stories/Box.stories.tsx +474 -0
  139. package/src/stories/Button.stories.tsx +262 -0
  140. package/src/stories/Center.stories.tsx +126 -0
  141. package/src/stories/ErrorMessage.stories.tsx +19 -0
  142. package/src/stories/Flex.stories.tsx +345 -0
  143. package/src/stories/Form.stories.tsx +83 -0
  144. package/src/stories/Heading.stories.tsx +263 -0
  145. package/src/stories/HelperMessage.stories.tsx +22 -0
  146. package/src/stories/Input.stories.tsx +145 -0
  147. package/src/stories/Label.stories.tsx +32 -0
  148. package/src/stories/LinkButton.stories.tsx +207 -0
  149. package/src/stories/LinkCard.stories.tsx +90 -0
  150. package/src/stories/RadioButton.stories.tsx +168 -0
  151. package/src/stories/RadioCard.stories.tsx +236 -0
  152. package/src/stories/Select.stories.tsx +97 -0
  153. package/src/stories/Stack.stories.tsx +167 -0
  154. package/src/stories/Text.stories.tsx +396 -0
  155. package/src/stories/TextArea.stories.tsx +49 -0
  156. package/src/stories/Toggle.stories.tsx +30 -0
  157. package/src/test/vitest-jest-dom.d.ts +12 -0
  158. package/src/types/attributes.ts +6 -0
  159. package/src/types/global.d.ts +11 -0
  160. package/src/types/icon.ts +3 -0
  161. package/src/types/style.ts +254 -0
  162. package/src/utils/component.ts +8 -0
  163. package/src/utils/style.spec.ts +57 -0
  164. package/src/utils/style.ts +387 -0
  165. package/src/utils/types.ts +8 -0
  166. package/tsconfig.json +18 -0
  167. package/tsconfig.spec-lint.tsbuildinfo +1 -0
  168. package/tsconfig.tsbuildinfo +1 -0
  169. package/vite.config.ts +50 -0
  170. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { Dialog, DialogPanel, DialogTitle, Transition } from "@headlessui/react";
4
+ import clsx from "clsx";
5
+ import { FC, Fragment, PropsWithChildren, type ReactNode, useCallback, useRef } from "react";
6
+ import styles from "./MessageModal.module.css";
7
+ import { Button } from "../../";
8
+ import { useScrollable } from "../../hooks/useScrollable";
9
+ import { VisuallyHidden } from "../../sharedComponents/VisuallyHidden/VisuallyHidden";
10
+ import { CustomDataAttributeProps } from "../../types/attributes";
11
+ import { opacityToClassName } from "../../utils/style";
12
+
13
+ type Opacity = "normal" | "darker";
14
+
15
+ type Props = {
16
+ /**
17
+ * プライマリーアクションボタンのラベル
18
+ */
19
+ header?: string;
20
+ /**
21
+ * 閉じるアクションが実行された場合のコールバック
22
+ */
23
+ onClose: () => void;
24
+ /**
25
+ * 閉じるボタンのラベル
26
+ * @default 閉じる
27
+ */
28
+ closeLabel?: string;
29
+ /**
30
+ * オーバーレイの透過度
31
+ * @default normal
32
+ */
33
+ overlayOpacity?: Opacity;
34
+ /**
35
+ * 画面を占有する高さで固定する
36
+ */
37
+ fixedHeight?: boolean;
38
+ /**
39
+ * モーダルダイアログを開くかどうか
40
+ * @default true
41
+ */
42
+ open?: boolean;
43
+ /**
44
+ * openを無視してモーダルを開いたままにする。アニメーションライブラリとの連携で、ActionHalfModal自身が開閉に関与しない場合に使用
45
+ */
46
+ isStatic?: boolean;
47
+ /**
48
+ * ネイティブ要素のid属性。ページで固有のIDを指定
49
+ */
50
+ id?: string;
51
+ /**
52
+ * ネイティブのaria-labelledby属性。独自の見出しを実装する場合にダイアログとの紐づけに使用。ページで固有のIDを指定
53
+ */
54
+ ariaLabelledby?: string;
55
+ /**
56
+ * ヒーローエリア(見出しの更に上)に配置するコンテンツ
57
+ */
58
+ hero?: ReactNode;
59
+ /**
60
+ * ヘッダーを固定表示
61
+ * heroが指定されている場合は無効
62
+ */
63
+ stickyHeader?: boolean;
64
+ /**
65
+ * フッターを固定表示
66
+ */
67
+ stickyFooter?: boolean;
68
+ } & PropsWithChildren &
69
+ CustomDataAttributeProps;
70
+
71
+ export const MessageModal: FC<Props> = ({
72
+ header,
73
+ children,
74
+ onClose,
75
+ overlayOpacity = "normal",
76
+ closeLabel = "閉じる",
77
+ fixedHeight = false,
78
+ open = true,
79
+ isStatic = false,
80
+ ariaLabelledby,
81
+ hero,
82
+ stickyHeader = false,
83
+ stickyFooter = false,
84
+ ...otherProps
85
+ }) => {
86
+ const opacityClassName = opacityToClassName(overlayOpacity);
87
+
88
+ const initialFocusRef = useRef(null);
89
+
90
+ const dialogRef = useCallback(
91
+ (node: HTMLDivElement | null) => {
92
+ if (node !== null && header == null && ariaLabelledby != null) {
93
+ node.setAttribute("aria-labelledby", ariaLabelledby);
94
+ } else if (node !== null && header == null && ariaLabelledby == null) {
95
+ node.removeAttribute("aria-labelledby");
96
+ }
97
+ },
98
+ [ariaLabelledby, header],
99
+ );
100
+
101
+ const { scrollContainerRef, canScrollUp, canScrollDown } = useScrollable();
102
+
103
+ return (
104
+ <Transition
105
+ show={open}
106
+ as={Fragment}
107
+ enter={styles.panelEnter}
108
+ enterFrom={styles.panelEnterFrom}
109
+ enterTo={styles.panelEnterTo}
110
+ leave={styles.panelLeave}
111
+ leaveFrom={styles.panelLeaveFrom}
112
+ leaveTo={styles.panelLeaveTo}
113
+ >
114
+ <Dialog
115
+ ref={dialogRef}
116
+ static={isStatic}
117
+ onClose={onClose}
118
+ className={styles.modal}
119
+ initialFocus={initialFocusRef}
120
+ {...otherProps}
121
+ >
122
+ <div className={clsx(styles.overlay, styles[opacityClassName])} />
123
+ <DialogPanel
124
+ className={clsx(styles.dialog, {
125
+ [styles.fixedHeight]: fixedHeight,
126
+ })}
127
+ >
128
+ {header === undefined ? (
129
+ <VisuallyHidden as="p" tabIndex={-1} ref={initialFocusRef}>
130
+ ダイアログ
131
+ </VisuallyHidden>
132
+ ) : null}
133
+ <div className={styles.scrollContainer} ref={scrollContainerRef}>
134
+ <div
135
+ className={clsx(styles.mainContent, {
136
+ [styles.headerLess]: header === undefined && hero === undefined,
137
+ [styles.fixedHeight]: fixedHeight,
138
+ })}
139
+ >
140
+ {hero !== undefined ? <div className={styles.hero}>{hero}</div> : null}
141
+ {header !== undefined ? (
142
+ <DialogTitle
143
+ tabIndex={-1}
144
+ ref={initialFocusRef}
145
+ className={clsx(
146
+ styles.header,
147
+ !hero && stickyHeader && styles.sticky,
148
+ canScrollUp && styles.canScroll,
149
+ )}
150
+ >
151
+ {header}
152
+ </DialogTitle>
153
+ ) : null}
154
+ <div
155
+ className={clsx(styles.body, {
156
+ [styles.fixedHeight]: fixedHeight,
157
+ })}
158
+ >
159
+ {children}
160
+ </div>
161
+ <footer
162
+ className={clsx(
163
+ styles.footer,
164
+ stickyFooter && styles.sticky,
165
+ canScrollDown && styles.canScroll,
166
+ )}
167
+ >
168
+ <Button block onClick={onClose} aria-label={closeLabel}>
169
+ {closeLabel}
170
+ </Button>
171
+ </footer>
172
+ </div>
173
+ </div>
174
+ </DialogPanel>
175
+ </Dialog>
176
+ </Transition>
177
+ );
178
+ };
@@ -0,0 +1,8 @@
1
+ .pre {
2
+ display: block;
3
+ white-space: var(--white-space);
4
+ }
5
+
6
+ .pre.inline {
7
+ display: inline-block;
8
+ }
@@ -0,0 +1,11 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { Pre } from "./Pre";
3
+
4
+ describe("<Pre>", () => {
5
+ it("receives data attributes", () => {
6
+ render(<Pre data-testid="pre" />);
7
+ const pre = screen.getByTestId("pre");
8
+
9
+ expect(pre).toBeInTheDocument();
10
+ });
11
+ });
@@ -0,0 +1,76 @@
1
+ import { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Pre } from "./Pre";
3
+ import { Box } from "../Box/Box";
4
+
5
+ export default {
6
+ title: "typography/Pre",
7
+ component: Pre,
8
+ } satisfies Meta<typeof Pre>;
9
+
10
+ type Story = StoryObj<typeof Pre>;
11
+
12
+ const defaultArgs = {
13
+ children: `Lorem Ipsum is simply dummy text of the printing and typesetting industry.
14
+ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
15
+ It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
16
+ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.`,
17
+ };
18
+
19
+ export const Default: Story = {
20
+ render: (args) => (
21
+ <div style={{ width: 500 }}>
22
+ <Box border="black" p="md">
23
+ <Pre {...args} />
24
+ </Box>
25
+ </div>
26
+ ),
27
+ args: defaultArgs,
28
+ };
29
+
30
+ export const WhiteSpacePre: Story = {
31
+ render: (args) => (
32
+ <div style={{ width: 500 }}>
33
+ <Box border="black" p="md">
34
+ <Pre {...args} />
35
+ </Box>
36
+ </div>
37
+ ),
38
+ args: {
39
+ ...defaultArgs,
40
+ whiteSpace: "pre",
41
+ },
42
+ };
43
+
44
+ export const PreWrap: Story = {
45
+ render: (args) => (
46
+ <div style={{ width: 500 }}>
47
+ <Box border="black" p="md">
48
+ <Pre {...args} />
49
+ </Box>
50
+ </div>
51
+ ),
52
+ args: {
53
+ ...defaultArgs,
54
+ whiteSpace: "pre-wrap",
55
+ },
56
+ };
57
+
58
+ export const Inline: Story = {
59
+ render: (args) => (
60
+ <p>
61
+ Text
62
+ <Pre {...args} />
63
+ Text
64
+ </p>
65
+ ),
66
+ args: {
67
+ ...defaultArgs,
68
+ children: `Lorem Ipsum
69
+ is simply
70
+ dummy text
71
+ of the
72
+ printing and
73
+ typesetting industry.`,
74
+ inline: true,
75
+ },
76
+ };
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { clsx } from "clsx";
4
+ import { CSSProperties, forwardRef, type HTMLAttributes, type PropsWithChildren } from "react";
5
+ import styles from "./Pre.module.css";
6
+
7
+ type AllowedSpanAttributes = Omit<HTMLAttributes<HTMLSpanElement>, "className">;
8
+
9
+ type Props = {
10
+ /**
11
+ * 折り返しや空白、改行の扱い
12
+ * @default 'pre-line'
13
+ */
14
+ whiteSpace?: "pre" | "pre-wrap" | "pre-line" | "break-spaces";
15
+ /**
16
+ * inline-blockとして扱う
17
+ */
18
+ inline?: boolean;
19
+ } & AllowedSpanAttributes;
20
+
21
+ export const Pre = forwardRef<HTMLSpanElement, PropsWithChildren<Props>>(
22
+ ({ children, whiteSpace = "pre-line", inline = false, ...rest }, ref) => {
23
+ return (
24
+ <span
25
+ ref={ref}
26
+ className={clsx(styles.pre, inline && styles.inline)}
27
+ style={
28
+ {
29
+ "--white-space": whiteSpace,
30
+ } as CSSProperties
31
+ }
32
+ {...rest}
33
+ >
34
+ {children}
35
+ </span>
36
+ );
37
+ },
38
+ );
39
+
40
+ Pre.displayName = "Pre";
@@ -0,0 +1,92 @@
1
+ .icon {
2
+ box-sizing: border-box;
3
+ display: inline-flex;
4
+ flex: none;
5
+ align-items: center;
6
+ justify-content: center;
7
+ background-color: var(--color-surface-container);
8
+ border: 2px solid var(--color-outline);
9
+ border-radius: 24px;
10
+ }
11
+
12
+ .text {
13
+ hyphens: auto;
14
+ overflow-wrap: anywhere;
15
+ }
16
+
17
+ .medium .icon {
18
+ width: 1.5rem;
19
+ height: 1.5rem;
20
+ }
21
+
22
+ .small .icon {
23
+ width: 1.25rem;
24
+ height: 1.25rem;
25
+ }
26
+
27
+ @media (hover: hover) {
28
+ .label:hover .icon {
29
+ border-color: var(--color-ubie-blue-500);
30
+ }
31
+ }
32
+
33
+ .icon::before {
34
+ content: "";
35
+ border-radius: 16px;
36
+ }
37
+
38
+ .medium .icon::before {
39
+ width: 1rem;
40
+ height: 1rem;
41
+ }
42
+
43
+ .small .icon::before {
44
+ width: 0.75rem;
45
+ height: 0.75rem;
46
+ }
47
+
48
+ .radio {
49
+ position: absolute;
50
+ opacity: 0;
51
+ }
52
+
53
+ .radio:checked + .icon {
54
+ border-color: var(--color-ubie-blue-600);
55
+ }
56
+
57
+ .radio:checked + .icon::before {
58
+ background: var(--color-ubie-blue-600);
59
+ }
60
+
61
+ .radio:disabled + .icon + .text {
62
+ color: var(--color-placeholder);
63
+ }
64
+
65
+ .radio:disabled + .icon {
66
+ border-color: var(--color-placeholder);
67
+ }
68
+
69
+ .radio:checked:disabled + .icon::before {
70
+ background: var(--color-placeholder);
71
+ }
72
+
73
+ .radio:focus-visible + .icon {
74
+ border-color: var(--color-ubie-pink-500);
75
+ }
76
+
77
+ .label {
78
+ display: flex;
79
+ gap: var(--size-spacing-xs);
80
+ align-items: center;
81
+ cursor: pointer;
82
+ }
83
+
84
+ .medium .label {
85
+ font-size: var(--text-body-md-size);
86
+ line-height: var(--text-body-md-narrow-line);
87
+ }
88
+
89
+ .small .label {
90
+ font-size: var(--text-body-sm-size);
91
+ line-height: var(--text-body-sm-narrow-line);
92
+ }
@@ -0,0 +1,25 @@
1
+ import { render } from "@testing-library/react";
2
+ import { createRef } from "react";
3
+ import { RadioButton } from "./RadioButton";
4
+
5
+ describe("RadioButton", () => {
6
+ it("access to DOM through ref prop", () => {
7
+ const ref = createRef<HTMLInputElement>();
8
+ render(
9
+ <RadioButton
10
+ name="test"
11
+ value="test"
12
+ ref={ref}
13
+ onChange={() => {
14
+ /**/
15
+ }}
16
+ checked={false}
17
+ >
18
+ Test
19
+ </RadioButton>,
20
+ );
21
+ expect(ref.current).not.toBeNull();
22
+ expect(ref.current?.tagName).toBe("INPUT");
23
+ expect(ref.current?.type).toBe("radio");
24
+ });
25
+ });
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { forwardRef, type InputHTMLAttributes } from "react";
5
+ import styles from "./RadioButton.module.css";
6
+ import { CustomDataAttributeProps } from "../../types/attributes";
7
+
8
+ type RadioProps = Required<Pick<InputHTMLAttributes<HTMLInputElement>, "onChange" | "checked">> &
9
+ Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "children" | "value" | "name">;
10
+
11
+ type Props = {
12
+ /**
13
+ * グループ化(排他制御)したい要素には同じ名前をつける
14
+ */
15
+ name: string;
16
+ /**
17
+ * 選択時のコールバックで渡される値
18
+ */
19
+ value: string | number;
20
+ /**
21
+ * ラベルに表示されるテキストまたは要素
22
+ */
23
+ children: InputHTMLAttributes<HTMLInputElement>["children"];
24
+ /**
25
+ * サイズ
26
+ * @default medium
27
+ */
28
+ size?: "medium" | "small";
29
+ } & RadioProps &
30
+ CustomDataAttributeProps;
31
+
32
+ export const RadioButton = forwardRef<HTMLInputElement, Props>(
33
+ ({ size = "medium", checked, onChange, value, name, children, ...otherProps }, ref) => {
34
+ return (
35
+ <div className={clsx(styles[size])}>
36
+ <label className={styles.label}>
37
+ <input
38
+ type="radio"
39
+ checked={checked}
40
+ name={name}
41
+ value={value}
42
+ className={styles.radio}
43
+ onChange={onChange}
44
+ ref={ref}
45
+ {...otherProps}
46
+ />
47
+ <span className={styles.icon} />
48
+ <span className={styles.text}>{children}</span>
49
+ </label>
50
+ </div>
51
+ );
52
+ },
53
+ );
54
+
55
+ RadioButton.displayName = "RadioButton";
@@ -0,0 +1,109 @@
1
+ .block {
2
+ width: 100%;
3
+ }
4
+
5
+ .radio {
6
+ position: absolute;
7
+ opacity: 0;
8
+ }
9
+
10
+ .label {
11
+ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
12
+
13
+ position: relative;
14
+ box-sizing: border-box;
15
+ display: inline-block;
16
+ padding: 16px 12px 16px 44px;
17
+ font-size: var(--text-body-md-size);
18
+ font-weight: bold;
19
+ hyphens: auto;
20
+ line-height: var(--text-body-md-narrow-line);
21
+ color: var(--color-on-surface);
22
+ overflow-wrap: anywhere;
23
+ vertical-align: text-top;
24
+ cursor: pointer;
25
+ background: #fff;
26
+ border-radius: 8px;
27
+ outline: 1px solid var(--color-outline);
28
+ transition:
29
+ outline-color 0.3s var(--ease-out-quint),
30
+ background-color 0.3s var(--ease-out-quint);
31
+ }
32
+
33
+ .label.checked {
34
+ background: var(--color-ubie-blue-100);
35
+ outline: 2px solid var(--color-ubie-blue-500);
36
+ }
37
+
38
+ .label::before {
39
+ position: absolute;
40
+ top: 50%;
41
+ left: 12px;
42
+ box-sizing: border-box;
43
+ display: block;
44
+ width: 20px;
45
+ height: 20px;
46
+ content: "";
47
+ border-radius: 50%;
48
+ outline: 2px solid var(--color-outline);
49
+ transform: translateY(-50%);
50
+ }
51
+
52
+ @media (hover: hover) {
53
+ .label:hover:not(.disabled, .checked)::before {
54
+ outline: 2px solid var(--color-ubie-blue-500);
55
+ }
56
+ }
57
+
58
+ .label.checked::before {
59
+ outline: 2px solid var(--color-ubie-blue-600);
60
+ }
61
+
62
+ .label.checked::after {
63
+ position: absolute;
64
+ top: 50%;
65
+ left: 14px;
66
+ box-sizing: border-box;
67
+ display: block;
68
+ width: 16px;
69
+ height: 16px;
70
+ content: "";
71
+ background: var(--color-ubie-blue-600);
72
+ border-radius: 50%;
73
+ transform: translateY(-50%);
74
+ }
75
+
76
+ .label::after {
77
+ position: absolute;
78
+ top: 50%;
79
+ left: 12px;
80
+ box-sizing: border-box;
81
+ display: block;
82
+ content: "";
83
+ border-radius: 50%;
84
+ transform: translateY(-50%);
85
+ }
86
+
87
+ .label.checked.disabled::before {
88
+ outline-color: var(--color-outline);
89
+ }
90
+
91
+ .label.checked.disabled::after {
92
+ background-color: var(--color-placeholder);
93
+ }
94
+
95
+ .label:has(.radio:focus-visible) {
96
+ box-shadow: 0 0 0 4px var(--color-ubie-pink-500);
97
+ }
98
+
99
+ @media (hover: hover) {
100
+ .label:hover {
101
+ background-color: var(--color-ubie-blue-100);
102
+ }
103
+ }
104
+
105
+ .label.disabled {
106
+ color: var(--color-placeholder);
107
+ background-color: var(--color-outline-variant);
108
+ outline-color: var(--color-placeholder);
109
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { clsx } from "clsx";
4
+ import { FC, forwardRef } from "react";
5
+ import styles from "./RadioCard.module.css";
6
+ import { CustomDataAttributeProps } from "../../types/attributes"; // 追加したインポート
7
+
8
+ type Props = Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "children"> &
9
+ CustomDataAttributeProps & {
10
+ /**
11
+ * グループ化(排他制御)したい要素には同じ名前をつける
12
+ */
13
+ name: string;
14
+ /**
15
+ * 選択時のコールバックで渡される値
16
+ */
17
+ value: string | number;
18
+ /**
19
+ * ラベルに表示されるテキストまたは要素
20
+ */
21
+ children: React.InputHTMLAttributes<HTMLInputElement>["children"];
22
+ /**
23
+ * 横幅を100%占有する
24
+ */
25
+ block?: boolean;
26
+ };
27
+
28
+ /**
29
+ * アクセシビリティに配慮したラジオボタン。
30
+ * 選択した後自動で遷移しないタイプのラジオボタン(選択してもなにもおきないボタン)に使用
31
+ */
32
+ const RadioCard: FC<Props> = forwardRef<HTMLInputElement, Props>(
33
+ ({ name, value, checked, children, className, block = false, disabled, ...otherProps }, ref) => {
34
+ return (
35
+ <label
36
+ className={clsx(
37
+ styles.label,
38
+ block && styles.block,
39
+ checked && styles.checked,
40
+ disabled && styles.disabled,
41
+ )}
42
+ >
43
+ <input
44
+ type="radio"
45
+ name={name}
46
+ value={value}
47
+ checked={checked}
48
+ className={clsx(className, styles.radio)}
49
+ ref={ref}
50
+ disabled={disabled}
51
+ {...otherProps}
52
+ />
53
+ {children}
54
+ </label>
55
+ );
56
+ },
57
+ );
58
+
59
+ RadioCard.displayName = "RadioCard";
60
+
61
+ export { RadioCard };
@@ -0,0 +1,16 @@
1
+ .wrapper {
2
+ padding: 0;
3
+ margin: 0;
4
+ border: none;
5
+ }
6
+
7
+ .legend {
8
+ display: flex;
9
+ gap: var(--size-spacing-xs);
10
+ align-items: center;
11
+ padding: 0;
12
+ margin-bottom: var(--size-spacing-md);
13
+ font-size: var(--text-body-sm-size);
14
+ font-weight: bold;
15
+ color: var(--color-on-surface-variant);
16
+ }
@@ -0,0 +1,17 @@
1
+ import { render } from "@testing-library/react";
2
+ import { createRef } from "react";
3
+ import { RadioGroup } from "./RadioGroup";
4
+
5
+ describe("RadioGroup", () => {
6
+ it("access to DOM through ref prop", () => {
7
+ const ref = createRef<HTMLFieldSetElement>();
8
+ render(
9
+ <RadioGroup label="test" ref={ref}>
10
+ <p>Test</p>
11
+ <p>Test</p>
12
+ </RadioGroup>,
13
+ );
14
+ expect(ref.current).not.toBeNull();
15
+ expect(ref.current?.tagName).toBe("FIELDSET");
16
+ });
17
+ });