sparkle-design 1.0.7 → 2.0.0-beta.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/dist/components/ui/badge/index.d.ts +2 -2
- package/dist/components/ui/badge/index.js +8 -8
- package/dist/components/ui/breadcrumb/index.d.ts +6 -6
- package/dist/components/ui/breadcrumb/index.js +2 -2
- package/dist/components/ui/button/index.d.ts +70 -5
- package/dist/components/ui/button/index.js +257 -103
- package/dist/components/ui/card/index.d.ts +119 -23
- package/dist/components/ui/card/index.js +155 -45
- package/dist/components/ui/checkbox/index.d.ts +1 -1
- package/dist/components/ui/checkbox/index.js +23 -23
- package/dist/components/ui/dialog/index.d.ts +15 -15
- package/dist/components/ui/dialog/index.js +6 -6
- package/dist/components/ui/divider/index.d.ts +5 -2
- package/dist/components/ui/divider/index.js +6 -11
- package/dist/components/ui/form/index.d.ts +10 -7
- package/dist/components/ui/form/index.js +17 -7
- package/dist/components/ui/form/label.d.ts +1 -1
- package/dist/components/ui/icon/index.d.ts +6 -3
- package/dist/components/ui/icon/index.js +4 -9
- package/dist/components/ui/icon-button/index.d.ts +118 -5
- package/dist/components/ui/icon-button/index.js +270 -52
- package/dist/components/ui/inline-message/index.d.ts +13 -4
- package/dist/components/ui/inline-message/index.js +20 -20
- package/dist/components/ui/input/index.d.ts +6 -3
- package/dist/components/ui/input/index.js +14 -14
- package/dist/components/ui/input-password/index.d.ts +4 -1
- package/dist/components/ui/input-password/index.js +14 -2
- package/dist/components/ui/link/index.d.ts +13 -6
- package/dist/components/ui/link/index.js +16 -13
- package/dist/components/ui/modal/index.d.ts +11 -11
- package/dist/components/ui/modal/index.js +2 -2
- package/dist/components/ui/overlay/index.d.ts +5 -2
- package/dist/components/ui/overlay/index.js +4 -5
- package/dist/components/ui/radio/index.d.ts +10 -4
- package/dist/components/ui/radio/index.js +90 -20
- package/dist/components/ui/select/index.d.ts +11 -11
- package/dist/components/ui/select/index.js +13 -13
- package/dist/components/ui/skeleton/index.d.ts +1 -1
- package/dist/components/ui/skeleton/index.js +1 -1
- package/dist/components/ui/slider/index.d.ts +53 -1
- package/dist/components/ui/slider/index.js +75 -13
- package/dist/components/ui/spinner/index.d.ts +6 -3
- package/dist/components/ui/spinner/index.js +5 -10
- package/dist/components/ui/switch/index.d.ts +1 -1
- package/dist/components/ui/switch/index.js +11 -7
- package/dist/components/ui/tabs/index.d.ts +8 -4
- package/dist/components/ui/tabs/index.js +40 -37
- package/dist/components/ui/tag/index.d.ts +6 -3
- package/dist/components/ui/tag/index.js +19 -24
- package/dist/components/ui/textarea/index.d.ts +5 -2
- package/dist/components/ui/textarea/index.js +9 -14
- package/dist/components/ui/toast/index.d.ts +1 -1
- package/dist/components/ui/toast/index.js +1 -1
- package/dist/components/ui/tooltip/index.d.ts +4 -4
- package/dist/components/ui/tooltip/index.js +1 -1
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +60 -1
- package/dist/test/helpers.d.ts +3 -2
- package/dist/test/helpers.js +23 -4
- package/dist/test/setup.js +37 -31
- package/package.json +17 -10
|
@@ -24,14 +24,16 @@ import { Spinner } from "../../../components/ui/spinner/index.js";
|
|
|
24
24
|
const buttonVariants = cva([
|
|
25
25
|
// `relative` is required because we absolutely-position the loading spinner at the center.
|
|
26
26
|
"relative inline-flex items-center justify-center gap-0.5 whitespace-nowrap rounded-action transition-colors",
|
|
27
|
-
"cursor-pointer disabled:cursor-not-allowed",
|
|
27
|
+
"cursor-pointer disabled:cursor-not-allowed aria-disabled:cursor-not-allowed",
|
|
28
28
|
"shrink-0 outline-none",
|
|
29
|
-
"focus-visible:ring-2 focus-visible:ring-
|
|
29
|
+
"focus-visible:ring-2 focus-visible:ring-border-ring focus-visible:ring-offset-2",
|
|
30
30
|
].join(" "), {
|
|
31
31
|
variants: {
|
|
32
32
|
variant: {
|
|
33
|
-
solid
|
|
34
|
-
|
|
33
|
+
// solid の 1px 枠線は Figma の刷新で削除された(shadow も base = 影なし)
|
|
34
|
+
// en: The 1px border on solid was removed in the Figma refresh (shadow is base = none).
|
|
35
|
+
solid: "shadow-base",
|
|
36
|
+
outline: "border shadow-base",
|
|
35
37
|
ghost: "",
|
|
36
38
|
},
|
|
37
39
|
size: {
|
|
@@ -56,22 +58,20 @@ const buttonVariants = cva([
|
|
|
56
58
|
theme: "primary",
|
|
57
59
|
isLoading: false,
|
|
58
60
|
className: [
|
|
59
|
-
"bg-primary-
|
|
60
|
-
"text-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"active:bg-primary-700",
|
|
65
|
-
"active:border-primary-800",
|
|
66
|
-
"disabled:bg-primary-200",
|
|
67
|
-
"disabled:border-none",
|
|
61
|
+
"bg-surface-primary-high-enabled",
|
|
62
|
+
"text-text-inverse",
|
|
63
|
+
"hover:bg-surface-primary-high-hover",
|
|
64
|
+
"active:bg-surface-primary-high-active",
|
|
65
|
+
"disabled:bg-surface-primary-high-disabled aria-disabled:bg-surface-primary-high-disabled",
|
|
68
66
|
].join(" "),
|
|
69
67
|
},
|
|
70
68
|
{
|
|
69
|
+
// ローディング中に見えるのはスピナー(currentColor 継承)なので object トークンで着色する
|
|
70
|
+
// en: Only the spinner (inherits currentColor) is visible while loading, so color it with the object token.
|
|
71
71
|
variant: "solid",
|
|
72
72
|
theme: "primary",
|
|
73
73
|
isLoading: true,
|
|
74
|
-
className: "disabled:bg-primary-
|
|
74
|
+
className: "disabled:bg-surface-primary-high-enabled aria-disabled:bg-surface-primary-high-enabled disabled:text-object-inverse aria-disabled:text-object-inverse",
|
|
75
75
|
},
|
|
76
76
|
// solid neutral
|
|
77
77
|
{
|
|
@@ -79,22 +79,18 @@ const buttonVariants = cva([
|
|
|
79
79
|
theme: "neutral",
|
|
80
80
|
isLoading: false,
|
|
81
81
|
className: [
|
|
82
|
-
"bg-
|
|
83
|
-
"text-
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"active:bg-[var(--color-black-alpha-800)]",
|
|
88
|
-
"active:border-neutral-800",
|
|
89
|
-
"disabled:bg-[var(--color-black-alpha-200)]",
|
|
90
|
-
"disabled:border-none",
|
|
82
|
+
"bg-surface-neutral-high-enabled",
|
|
83
|
+
"text-text-inverse",
|
|
84
|
+
"hover:bg-surface-neutral-high-hover",
|
|
85
|
+
"active:bg-surface-neutral-high-active",
|
|
86
|
+
"disabled:bg-surface-neutral-high-disabled aria-disabled:bg-surface-neutral-high-disabled",
|
|
91
87
|
].join(" "),
|
|
92
88
|
},
|
|
93
89
|
{
|
|
94
90
|
variant: "solid",
|
|
95
91
|
theme: "neutral",
|
|
96
92
|
isLoading: true,
|
|
97
|
-
className: "disabled:bg-
|
|
93
|
+
className: "disabled:bg-surface-neutral-high-enabled aria-disabled:bg-surface-neutral-high-enabled disabled:text-object-inverse aria-disabled:text-object-inverse",
|
|
98
94
|
},
|
|
99
95
|
// solid negative
|
|
100
96
|
{
|
|
@@ -102,22 +98,18 @@ const buttonVariants = cva([
|
|
|
102
98
|
theme: "negative",
|
|
103
99
|
isLoading: false,
|
|
104
100
|
className: [
|
|
105
|
-
"bg-negative-
|
|
106
|
-
"text-
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"active:bg-negative-700",
|
|
111
|
-
"active:border-negative-800",
|
|
112
|
-
"disabled:bg-negative-200",
|
|
113
|
-
"disabled:border-none",
|
|
101
|
+
"bg-surface-negative-high-enabled",
|
|
102
|
+
"text-text-inverse",
|
|
103
|
+
"hover:bg-surface-negative-high-hover",
|
|
104
|
+
"active:bg-surface-negative-high-active",
|
|
105
|
+
"disabled:bg-surface-negative-high-disabled aria-disabled:bg-surface-negative-high-disabled",
|
|
114
106
|
].join(" "),
|
|
115
107
|
},
|
|
116
108
|
{
|
|
117
109
|
variant: "solid",
|
|
118
110
|
theme: "negative",
|
|
119
111
|
isLoading: true,
|
|
120
|
-
className: "disabled:bg-negative-
|
|
112
|
+
className: "disabled:bg-surface-negative-high-enabled aria-disabled:bg-surface-negative-high-enabled disabled:text-object-inverse aria-disabled:text-object-inverse",
|
|
121
113
|
},
|
|
122
114
|
// outline primary
|
|
123
115
|
{
|
|
@@ -125,23 +117,23 @@ const buttonVariants = cva([
|
|
|
125
117
|
theme: "primary",
|
|
126
118
|
isLoading: false,
|
|
127
119
|
className: [
|
|
128
|
-
"bg-
|
|
129
|
-
"text-primary-
|
|
130
|
-
"border-primary-
|
|
131
|
-
"hover:bg-primary-
|
|
132
|
-
"
|
|
133
|
-
"active:
|
|
134
|
-
"active:text-primary-
|
|
135
|
-
"disabled:bg-
|
|
136
|
-
"disabled:border-primary-
|
|
137
|
-
"disabled:text-primary-
|
|
120
|
+
"bg-surface-base-0",
|
|
121
|
+
"text-text-primary-enabled",
|
|
122
|
+
"border-border-primary-high",
|
|
123
|
+
"hover:bg-surface-primary-low-hover",
|
|
124
|
+
"hover:text-text-primary-hover",
|
|
125
|
+
"active:bg-surface-primary-low-active",
|
|
126
|
+
"active:text-text-primary-active",
|
|
127
|
+
"disabled:bg-surface-primary-low-disabled aria-disabled:bg-surface-primary-low-disabled",
|
|
128
|
+
"disabled:border-border-primary-low aria-disabled:border-border-primary-low",
|
|
129
|
+
"disabled:text-text-primary-disabled aria-disabled:text-text-primary-disabled",
|
|
138
130
|
].join(" "),
|
|
139
131
|
},
|
|
140
132
|
{
|
|
141
133
|
variant: "outline",
|
|
142
134
|
theme: "primary",
|
|
143
135
|
isLoading: true,
|
|
144
|
-
className: "disabled:
|
|
136
|
+
className: "disabled:bg-surface-base-0 aria-disabled:bg-surface-base-0 disabled:text-object-primary-enabled aria-disabled:text-object-primary-enabled disabled:border-border-primary-high aria-disabled:border-border-primary-high",
|
|
145
137
|
},
|
|
146
138
|
// outline neutral
|
|
147
139
|
{
|
|
@@ -149,22 +141,23 @@ const buttonVariants = cva([
|
|
|
149
141
|
theme: "neutral",
|
|
150
142
|
isLoading: false,
|
|
151
143
|
className: [
|
|
152
|
-
"bg-
|
|
153
|
-
"text-neutral-
|
|
154
|
-
"border-neutral-
|
|
155
|
-
"hover:bg-
|
|
156
|
-
"
|
|
157
|
-
"active:
|
|
158
|
-
"
|
|
159
|
-
"disabled:
|
|
160
|
-
"disabled:
|
|
144
|
+
"bg-surface-base-0",
|
|
145
|
+
"text-text-neutral-middle",
|
|
146
|
+
"border-border-neutral-high",
|
|
147
|
+
"hover:bg-surface-neutral-low-hover",
|
|
148
|
+
"hover:text-text-neutral-high",
|
|
149
|
+
"active:bg-surface-neutral-low-active",
|
|
150
|
+
"active:text-text-neutral-high",
|
|
151
|
+
"disabled:bg-surface-neutral-low-disabled aria-disabled:bg-surface-neutral-low-disabled",
|
|
152
|
+
"disabled:border-border-neutral-low aria-disabled:border-border-neutral-low",
|
|
153
|
+
"disabled:text-text-neutral-disabled aria-disabled:text-text-neutral-disabled",
|
|
161
154
|
].join(" "),
|
|
162
155
|
},
|
|
163
156
|
{
|
|
164
157
|
variant: "outline",
|
|
165
158
|
theme: "neutral",
|
|
166
159
|
isLoading: true,
|
|
167
|
-
className: "disabled:bg-
|
|
160
|
+
className: "disabled:bg-surface-base-0 aria-disabled:bg-surface-base-0 disabled:text-object-neutral-middle aria-disabled:text-object-neutral-middle disabled:border-border-neutral-high aria-disabled:border-border-neutral-high",
|
|
168
161
|
},
|
|
169
162
|
// outline negative
|
|
170
163
|
{
|
|
@@ -172,23 +165,23 @@ const buttonVariants = cva([
|
|
|
172
165
|
theme: "negative",
|
|
173
166
|
isLoading: false,
|
|
174
167
|
className: [
|
|
175
|
-
"bg-
|
|
176
|
-
"text-negative-
|
|
177
|
-
"border-negative-
|
|
178
|
-
"hover:bg-negative-
|
|
179
|
-
"
|
|
180
|
-
"active:
|
|
181
|
-
"active:text-negative-
|
|
182
|
-
"disabled:bg-
|
|
183
|
-
"disabled:border-negative-
|
|
184
|
-
"disabled:text-negative-
|
|
168
|
+
"bg-surface-base-0",
|
|
169
|
+
"text-text-negative-enabled",
|
|
170
|
+
"border-border-negative-high",
|
|
171
|
+
"hover:bg-surface-negative-low-hover",
|
|
172
|
+
"hover:text-text-negative-hover",
|
|
173
|
+
"active:bg-surface-negative-low-active",
|
|
174
|
+
"active:text-text-negative-active",
|
|
175
|
+
"disabled:bg-surface-negative-low-disabled aria-disabled:bg-surface-negative-low-disabled",
|
|
176
|
+
"disabled:border-border-negative-low aria-disabled:border-border-negative-low",
|
|
177
|
+
"disabled:text-text-negative-disabled aria-disabled:text-text-negative-disabled",
|
|
185
178
|
].join(" "),
|
|
186
179
|
},
|
|
187
180
|
{
|
|
188
181
|
variant: "outline",
|
|
189
182
|
theme: "negative",
|
|
190
183
|
isLoading: true,
|
|
191
|
-
className: "disabled:bg-
|
|
184
|
+
className: "disabled:bg-surface-base-0 aria-disabled:bg-surface-base-0 disabled:text-object-negative-enabled aria-disabled:text-object-negative-enabled disabled:border-border-negative-high aria-disabled:border-border-negative-high",
|
|
192
185
|
},
|
|
193
186
|
// ghost primary
|
|
194
187
|
{
|
|
@@ -196,19 +189,20 @@ const buttonVariants = cva([
|
|
|
196
189
|
theme: "primary",
|
|
197
190
|
isLoading: false,
|
|
198
191
|
className: [
|
|
199
|
-
"text-primary-
|
|
200
|
-
"hover:bg-primary-
|
|
201
|
-
"
|
|
202
|
-
"active:
|
|
203
|
-
"
|
|
204
|
-
"disabled:
|
|
192
|
+
"text-text-primary-enabled",
|
|
193
|
+
"hover:bg-surface-primary-low-hover",
|
|
194
|
+
"hover:text-text-primary-hover",
|
|
195
|
+
"active:bg-surface-primary-low-active",
|
|
196
|
+
"active:text-text-primary-active",
|
|
197
|
+
"disabled:bg-surface-primary-low-disabled aria-disabled:bg-surface-primary-low-disabled",
|
|
198
|
+
"disabled:text-text-primary-disabled aria-disabled:text-text-primary-disabled",
|
|
205
199
|
].join(" "),
|
|
206
200
|
},
|
|
207
201
|
{
|
|
208
202
|
variant: "ghost",
|
|
209
203
|
theme: "primary",
|
|
210
204
|
isLoading: true,
|
|
211
|
-
className: "disabled:text-primary-
|
|
205
|
+
className: "disabled:text-object-primary-enabled aria-disabled:text-object-primary-enabled",
|
|
212
206
|
},
|
|
213
207
|
// ghost neutral
|
|
214
208
|
{
|
|
@@ -216,18 +210,20 @@ const buttonVariants = cva([
|
|
|
216
210
|
theme: "neutral",
|
|
217
211
|
isLoading: false,
|
|
218
212
|
className: [
|
|
219
|
-
"text-neutral-
|
|
220
|
-
"hover:bg-
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"
|
|
213
|
+
"text-text-neutral-middle",
|
|
214
|
+
"hover:bg-surface-neutral-low-hover",
|
|
215
|
+
"hover:text-text-neutral-high",
|
|
216
|
+
"active:bg-surface-neutral-low-active",
|
|
217
|
+
"active:text-text-neutral-high",
|
|
218
|
+
"disabled:bg-surface-neutral-low-disabled aria-disabled:bg-surface-neutral-low-disabled",
|
|
219
|
+
"disabled:text-text-neutral-disabled aria-disabled:text-text-neutral-disabled",
|
|
224
220
|
].join(" "),
|
|
225
221
|
},
|
|
226
222
|
{
|
|
227
223
|
variant: "ghost",
|
|
228
224
|
theme: "neutral",
|
|
229
225
|
isLoading: true,
|
|
230
|
-
className: "disabled:text-neutral-
|
|
226
|
+
className: "disabled:text-object-neutral-middle aria-disabled:text-object-neutral-middle",
|
|
231
227
|
},
|
|
232
228
|
// ghost negative
|
|
233
229
|
{
|
|
@@ -235,19 +231,20 @@ const buttonVariants = cva([
|
|
|
235
231
|
theme: "negative",
|
|
236
232
|
isLoading: false,
|
|
237
233
|
className: [
|
|
238
|
-
"text-negative-
|
|
239
|
-
"hover:bg-negative-
|
|
240
|
-
"
|
|
241
|
-
"active:
|
|
242
|
-
"
|
|
243
|
-
"disabled:
|
|
234
|
+
"text-text-negative-enabled",
|
|
235
|
+
"hover:bg-surface-negative-low-hover",
|
|
236
|
+
"hover:text-text-negative-hover",
|
|
237
|
+
"active:bg-surface-negative-low-active",
|
|
238
|
+
"active:text-text-negative-active",
|
|
239
|
+
"disabled:bg-surface-negative-low-disabled aria-disabled:bg-surface-negative-low-disabled",
|
|
240
|
+
"disabled:text-text-negative-disabled aria-disabled:text-text-negative-disabled",
|
|
244
241
|
].join(" "),
|
|
245
242
|
},
|
|
246
243
|
{
|
|
247
244
|
variant: "ghost",
|
|
248
245
|
theme: "negative",
|
|
249
246
|
isLoading: true,
|
|
250
|
-
className: "disabled:text-negative-
|
|
247
|
+
className: "disabled:text-object-negative-enabled aria-disabled:text-object-negative-enabled",
|
|
251
248
|
},
|
|
252
249
|
],
|
|
253
250
|
defaultVariants: {
|
|
@@ -276,6 +273,10 @@ const buttonVariants = cva([
|
|
|
276
273
|
* en: Use `IconButton` for icon-only buttons.
|
|
277
274
|
* - `asChild` 使用時は `prefixIcon` / `suffixIcon` / `isLoading` が反映されません。必要ならスロット先でアイコンやローディング表現を構成してください。
|
|
278
275
|
* en: In `asChild` mode, `prefixIcon`, `suffixIcon`, and `isLoading` are ignored. Render icons and loading states inside the slotted child when needed.
|
|
276
|
+
* - `asChild` で差し込んだ要素が指定した props(`type` や `disabled` など)は、Slot のマージで
|
|
277
|
+
* コンポーネント側の値より優先されます。
|
|
278
|
+
* en: Props set on the slotted element itself (such as `type` or `disabled`) win over the
|
|
279
|
+
* component's own values through Slot's prop merging.
|
|
279
280
|
*
|
|
280
281
|
* ```tsx
|
|
281
282
|
* // ✅ Correct
|
|
@@ -298,21 +299,69 @@ const buttonVariants = cva([
|
|
|
298
299
|
* <Link href="/items/new">新規作成</Link>
|
|
299
300
|
* </Button>
|
|
300
301
|
* ```
|
|
301
|
-
|
|
302
|
+
*
|
|
302
303
|
* **アクセシビリティ / Accessibility**
|
|
303
304
|
*
|
|
304
305
|
* - ボタンにはアクセシブルネームが必要です(通常は `children` のテキスト)。
|
|
305
306
|
* アイコンのみの場合は `aria-label` / `aria-labelledby` を付与するか、可能なら `IconButton` を使用してください。
|
|
306
307
|
* - `isLoading` の場合でもアクセシブルネームは維持されます。
|
|
307
|
-
* - `asChild` を使う場合、子要素がボタン相当のセマンティクス(role
|
|
308
|
+
* - `asChild` を使う場合、子要素がボタン相当のセマンティクス(role/キーボード操作)を満たすようにしてください。
|
|
309
|
+
* - `asChild` で `<a>` など button 以外の要素を差し込んで無効化した場合、`aria-disabled` の付与、
|
|
310
|
+
* click / auxclick / Enter・Space の抑止、`aria-disabled:` プレフィックスによる無効時の配色まで
|
|
311
|
+
* このコンポーネントが行います(`data-disabled="true"` も利用側のスタイルフックとして出力)。
|
|
312
|
+
* 差し込み先が native の `<button>` の場合は `disabled` 属性がそのまま渡ります。
|
|
313
|
+
* - `type` / `form` / `value` など button 専用の props は、差し込み先が native の `<button>` の
|
|
314
|
+
* ときだけ転送されます。それ以外の要素では不正な属性になるため落とし、dev ビルドで警告します。
|
|
315
|
+
* 差し込み先が内部で `<button>` を描画するコンポーネントの場合のみ、その要素側に直接指定してください
|
|
316
|
+
* (Slot のマージで子の指定が優先されます)。`<a>` 等では属性自体が機能しないため指定不要です。
|
|
317
|
+
* en: Button-only props such as `type`, `form`, and `value` are forwarded only when the slot is a
|
|
318
|
+
* native `<button>`; on other elements they would be invalid attributes, so they are dropped with
|
|
319
|
+
* a dev warning. Set them on the slotted element only when that component renders a `<button>`
|
|
320
|
+
* internally (child props win in Slot's merge) — on `<a>` and friends the attributes do nothing.
|
|
321
|
+
* - `aria-disabled` を直接渡した場合も、無効時の配色が当たり操作が抑止されます(native の
|
|
322
|
+
* `disabled` 属性は付けないため、フォーカスは残ります)。無効化には基本的に `isDisabled` を
|
|
323
|
+
* 使ってください。
|
|
324
|
+
* en: Passing `aria-disabled` directly also applies the disabled colors and blocks activation
|
|
325
|
+
* (the native `disabled` attribute is not set, so the element stays focusable). Prefer
|
|
326
|
+
* `isDisabled` for disabling.
|
|
327
|
+
* なお `aria-disabled` の要素は既定でフォーカス可能なままです。無効な項目の存在と理由を
|
|
328
|
+
* 支援技術の利用者に伝えられるため通常はそのままで構いません。タブ順から外す必要がある場合のみ
|
|
329
|
+
* 差し込む要素側で `tabIndex={-1}` を指定してください。
|
|
330
|
+
* en: When disabled with `asChild` and a non-button element such as `<a>`, this component sets
|
|
331
|
+
* `aria-disabled`, blocks click / auxclick / Enter / Space, and applies the disabled colors via
|
|
332
|
+
* `aria-disabled:`-prefixed utilities (`data-disabled="true"` is also emitted as a styling hook).
|
|
333
|
+
* A slotted native `<button>` receives the real `disabled` attribute instead.
|
|
334
|
+
* - native の `<button>` かどうかは差し込んだ要素そのもので判定するため、内部で `<button>` を
|
|
335
|
+
* 描画するカスタムコンポーネントを渡した場合は非 button として扱われます。
|
|
336
|
+
* また無効時に抑止できるのは click / auxclick / Enter・Space までで、コンテキストメニューの
|
|
337
|
+
* 「新しいタブで開く」は止められません。確実に遷移させたくない場合は差し込む要素側で
|
|
338
|
+
* `href` を外してください。
|
|
339
|
+
* en: Whether the slot is a native `<button>` is decided from the slotted element itself, so a
|
|
340
|
+
* custom component that renders a `<button>` internally is treated as a non-button. Also, only
|
|
341
|
+
* click / auxclick / Enter / Space can be blocked while disabled — "open in new tab" from the
|
|
342
|
+
* context menu cannot. Drop `href` on the slotted element when navigation must not happen.
|
|
343
|
+
* An `aria-disabled` element stays focusable by design, which is usually what you want; set
|
|
344
|
+
* `tabIndex={-1}` on the slotted element only when it must leave the tab order.
|
|
308
345
|
*
|
|
309
346
|
* @param {ButtonProps} props
|
|
310
347
|
*/
|
|
311
|
-
|
|
312
|
-
var { className, variant, size, theme, isLoading = false, isDisabled = false, asChild = false, disabled, prefixIcon, suffixIcon, children } = _a, props = __rest(_a, ["className", "variant", "size", "theme", "isLoading", "isDisabled", "asChild", "disabled", "prefixIcon", "suffixIcon", "children"]);
|
|
348
|
+
function Button(_a) {
|
|
349
|
+
var { className, variant, size, theme, isLoading = false, isDisabled = false, asChild = false, disabled, prefixIcon, suffixIcon, ref, children } = _a, props = __rest(_a, ["className", "variant", "size", "theme", "isLoading", "isDisabled", "asChild", "disabled", "prefixIcon", "suffixIcon", "ref", "children"]);
|
|
313
350
|
const Comp = asChild ? SlotPrimitive.Slot : "button";
|
|
314
351
|
// disabled状態の管理(isDisabled、disabled、またはisLoadingがtrueの場合)
|
|
315
352
|
const isButtonDisabled = isLoading || isDisabled || disabled;
|
|
353
|
+
// 利用者が aria-disabled を直接指定した場合も、見た目に合わせて操作を抑止する。
|
|
354
|
+
// native の disabled 属性は付けないため、フォーカスは残る(soft disabled)
|
|
355
|
+
// en: A caller-provided aria-disabled also blocks activation so the behavior matches the look.
|
|
356
|
+
// The native `disabled` attribute is not set, so the element stays focusable (soft disabled).
|
|
357
|
+
const isSoftDisabled = props["aria-disabled"] === true || props["aria-disabled"] === "true";
|
|
358
|
+
const isActivationBlocked = Boolean(isButtonDisabled) || isSoftDisabled;
|
|
359
|
+
// asChild で差し込まれたのが native の <button> なら、button 専用の props を渡してよい。
|
|
360
|
+
// Slot は子の props を優先するため、差し込み側が明示した type はそのまま尊重される
|
|
361
|
+
// en: When the slotted element is a native <button>, button-only props can be forwarded.
|
|
362
|
+
// Slot gives the child's props precedence, so an explicit `type` on the child still wins.
|
|
363
|
+
const isSlottedNativeButton = asChild && React.isValidElement(children) && children.type === "button";
|
|
364
|
+
const canUseButtonProps = !asChild || isSlottedNativeButton;
|
|
316
365
|
// アイコンのサイズをボタンサイズに合わせて設定
|
|
317
366
|
const getIconSize = () => {
|
|
318
367
|
switch (size) {
|
|
@@ -327,14 +376,52 @@ const Button = React.forwardRef(function Button(_a, ref) {
|
|
|
327
376
|
const hasAccessibleNameProp = ("aria-label" in props && Boolean(props["aria-label"])) ||
|
|
328
377
|
("aria-labelledby" in props && Boolean(props["aria-labelledby"]));
|
|
329
378
|
const hasChildren = React.Children.count(children) > 0;
|
|
379
|
+
// asChild では children が差し込む要素そのものなので、その要素のラベル / テキストを見る
|
|
380
|
+
// en: With asChild the children are the slotted element itself, so inspect its label / text.
|
|
381
|
+
const slottedChildProps = React.isValidElement(children)
|
|
382
|
+
? children.props
|
|
383
|
+
: undefined;
|
|
384
|
+
const hasAccessibleName = asChild
|
|
385
|
+
? hasAccessibleNameProp ||
|
|
386
|
+
Boolean((slottedChildProps === null || slottedChildProps === void 0 ? void 0 : slottedChildProps["aria-label"]) ||
|
|
387
|
+
(slottedChildProps === null || slottedChildProps === void 0 ? void 0 : slottedChildProps["aria-labelledby"])) ||
|
|
388
|
+
React.Children.count(slottedChildProps === null || slottedChildProps === void 0 ? void 0 : slottedChildProps.children) > 0
|
|
389
|
+
: hasChildren || hasAccessibleNameProp;
|
|
330
390
|
if (process.env.NODE_ENV !== "production") {
|
|
331
|
-
if (!
|
|
391
|
+
if (!hasAccessibleName) {
|
|
332
392
|
// Icon-only button should use IconButton, or provide aria-label/labelledby.
|
|
333
393
|
// Keep it as a warning (not an exception) to avoid breaking existing usage.
|
|
334
394
|
console.warn("[Button] Accessible name is missing. Provide children text, or set aria-label/aria-labelledby. For icon-only actions, consider using IconButton.");
|
|
335
395
|
}
|
|
336
|
-
|
|
337
|
-
|
|
396
|
+
// asChild で単一要素以外を渡すと、Slot が何も描画しない / React が例外を投げる
|
|
397
|
+
// en: With asChild, anything other than a single element makes Slot render nothing or throw.
|
|
398
|
+
if (asChild && !React.isValidElement(children)) {
|
|
399
|
+
console.warn("[Button] asChild には単一の React 要素を children として渡してください。" +
|
|
400
|
+
"要素以外では何も描画されず、複数要素では実行時エラーになります。" +
|
|
401
|
+
" / asChild requires a single React element child: anything else renders nothing or throws.");
|
|
402
|
+
}
|
|
403
|
+
// button 専用の props は button 以外の差し込み先には渡さない(無効な属性になるため)
|
|
404
|
+
// en: Button-only props are not forwarded to non-button slots: they would be invalid attributes.
|
|
405
|
+
if (asChild && !canUseButtonProps) {
|
|
406
|
+
const droppedProps = [
|
|
407
|
+
"type",
|
|
408
|
+
"form",
|
|
409
|
+
"formAction",
|
|
410
|
+
"formEncType",
|
|
411
|
+
"formMethod",
|
|
412
|
+
"formNoValidate",
|
|
413
|
+
"formTarget",
|
|
414
|
+
"value",
|
|
415
|
+
"popoverTarget",
|
|
416
|
+
"popoverTargetAction",
|
|
417
|
+
].filter(name => props[name] !== undefined && props[name] !== false);
|
|
418
|
+
if (droppedProps.length > 0) {
|
|
419
|
+
console.warn(`[Button] asChild で button 以外の要素を差し込む場合、${droppedProps.join(" / ")} は無視されます。` +
|
|
420
|
+
"これらは button / input 専用の属性です。差し込み先が内部で <button> を描画するコンポーネントなら、" +
|
|
421
|
+
"その要素側に直接指定してください(<a> 等では属性自体が機能しません)。" +
|
|
422
|
+
` / In asChild mode with a non-button element, ${droppedProps.join(" / ")} are ignored: they only apply to button / input. ` +
|
|
423
|
+
"If the slotted component renders a <button> internally, set them on that element instead (they do nothing on <a> and friends).");
|
|
424
|
+
}
|
|
338
425
|
}
|
|
339
426
|
if (asChild && (prefixIcon || suffixIcon || isLoading)) {
|
|
340
427
|
console.warn("[Button] asChild mode does not support prefixIcon, suffixIcon, or isLoading. These props will be ignored.");
|
|
@@ -343,26 +430,93 @@ const Button = React.forwardRef(function Button(_a, ref) {
|
|
|
343
430
|
console.warn("[Button] onMouseDown/onPointerDown/onTouchStart are deprecated for accessibility reasons (WCAG 2.5.2 Pointer Cancellation). Prefer onClick (activation on release).");
|
|
344
431
|
}
|
|
345
432
|
}
|
|
346
|
-
const { onClick, onKeyDown } = props, restProps = __rest(props, ["onClick", "onKeyDown"]);
|
|
347
|
-
|
|
348
|
-
|
|
433
|
+
const { onClick, onKeyDown, onClickCapture, onKeyDownCapture, onAuxClickCapture, type, form, formAction, formEncType, formMethod, formNoValidate, formTarget, value, popoverTarget, popoverTargetAction, "aria-disabled": ariaDisabled } = props, restProps = __rest(props, ["onClick", "onKeyDown", "onClickCapture", "onKeyDownCapture", "onAuxClickCapture", "type", "form", "formAction", "formEncType", "formMethod", "formNoValidate", "formTarget", "value", "popoverTarget", "popoverTargetAction", "aria-disabled"]);
|
|
434
|
+
// button 専用の props は、差し込み先が native の <button> のときだけ渡す。
|
|
435
|
+
// <a> 等に渡すと不正な属性になるため。
|
|
436
|
+
// `name` は <a> でも受け付けられる(HTML Living Standard 上は obsolete だが UA が互換のため
|
|
437
|
+
// サポートし続けている)ので、ここでは落とさず転送したままにしている。
|
|
438
|
+
// React の型(ButtonHTMLAttributes)に button 専用 props が増えた場合はこのリストも追随すること
|
|
439
|
+
// en: Button-only props are forwarded only when the slot is a native <button>; on elements like
|
|
440
|
+
// <a> they would be invalid attributes. `name` is deliberately kept: <a name> is obsolete in the
|
|
441
|
+
// HTML Living Standard but still accepted by UAs for compatibility. Keep this list in sync when
|
|
442
|
+
// React's ButtonHTMLAttributes gains new button-only props.
|
|
443
|
+
const buttonOnlyProps = canUseButtonProps
|
|
444
|
+
? {
|
|
445
|
+
type: type || "button",
|
|
446
|
+
form,
|
|
447
|
+
formAction,
|
|
448
|
+
formEncType,
|
|
449
|
+
formMethod,
|
|
450
|
+
formNoValidate,
|
|
451
|
+
formTarget,
|
|
452
|
+
value,
|
|
453
|
+
popoverTarget,
|
|
454
|
+
popoverTargetAction,
|
|
455
|
+
}
|
|
456
|
+
: {};
|
|
457
|
+
// 無効時のガードは capture フェーズで行う。Radix Slot は「差し込んだ要素自身のハンドラ →
|
|
458
|
+
// Slot 側のハンドラ」の順で合成するため、bubble フェーズのガードでは子のハンドラを止められない
|
|
459
|
+
// en: Guard in the capture phase. Radix Slot composes handlers as "the slotted element's own
|
|
460
|
+
// handler first, then the slot's", so a bubble-phase guard cannot stop the child's handler.
|
|
461
|
+
const handleClickCapture = event => {
|
|
462
|
+
if (isActivationBlocked) {
|
|
349
463
|
event.preventDefault();
|
|
350
464
|
event.stopPropagation();
|
|
351
465
|
return;
|
|
352
466
|
}
|
|
353
|
-
|
|
467
|
+
onClickCapture === null || onClickCapture === void 0 ? void 0 : onClickCapture(event);
|
|
354
468
|
};
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
//
|
|
469
|
+
const handleKeyDownCapture = event => {
|
|
470
|
+
if (isActivationBlocked) {
|
|
471
|
+
// asChild で <a> 等を差し込んだ場合に Enter / Space での実行を止める。
|
|
472
|
+
// それ以外のキーは伝播を止めないが、無効時は利用者の onKeyDown も呼ばない
|
|
473
|
+
// en: Other keys keep propagating, but the caller's onKeyDown is not invoked
|
|
474
|
+
// while disabled.
|
|
358
475
|
if (event.key === "Enter" || event.key === " ") {
|
|
359
476
|
event.preventDefault();
|
|
360
477
|
event.stopPropagation();
|
|
361
478
|
}
|
|
362
479
|
return;
|
|
363
480
|
}
|
|
481
|
+
onKeyDownCapture === null || onKeyDownCapture === void 0 ? void 0 : onKeyDownCapture(event);
|
|
482
|
+
};
|
|
483
|
+
// 中クリックは click ではなく auxclick として飛ぶため、別途止める必要がある
|
|
484
|
+
// en: A middle click fires `auxclick`, not `click`, so it needs its own guard.
|
|
485
|
+
const handleAuxClickCapture = event => {
|
|
486
|
+
if (isActivationBlocked) {
|
|
487
|
+
event.preventDefault();
|
|
488
|
+
event.stopPropagation();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
onAuxClickCapture === null || onAuxClickCapture === void 0 ? void 0 : onAuxClickCapture(event);
|
|
492
|
+
};
|
|
493
|
+
// capture 側で stopPropagation するため、無効時にこの bubble 側ハンドラへは到達しない。
|
|
494
|
+
// capture が本体・こちらは保険として残している
|
|
495
|
+
// en: The capture guard stops propagation, so this bubble handler is not reached while
|
|
496
|
+
// disabled. The capture phase is the real guard; this one is a safety net.
|
|
497
|
+
const handleClick = event => {
|
|
498
|
+
if (isActivationBlocked) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(event);
|
|
502
|
+
};
|
|
503
|
+
const handleKeyDown = event => {
|
|
504
|
+
if (isActivationBlocked) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
364
507
|
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
|
|
365
508
|
};
|
|
509
|
+
// Slot は「差し込んだ要素自身のハンドラ → Slot のハンドラ」の順で合成するため、
|
|
510
|
+
// 無効時は子側の capture ハンドラを外してガードを確実に先に効かせる。
|
|
511
|
+
// 同様に子が明示した disabled も、無効時はコンポーネント側の状態を優先する
|
|
512
|
+
// en: Slot composes "the slotted element's own handler first, then the slot's", so while
|
|
513
|
+
// disabled we strip the child's capture handlers to keep the guard authoritative. The
|
|
514
|
+
// component's disabled state also overrides a `disabled` set on the child.
|
|
515
|
+
const slottedChildren = asChild && isActivationBlocked && React.isValidElement(children)
|
|
516
|
+
? React.cloneElement(children, Object.assign({ onClickCapture: undefined, onAuxClickCapture: undefined, onKeyDownCapture: undefined }, (canUseButtonProps && isButtonDisabled
|
|
517
|
+
? { disabled: true }
|
|
518
|
+
: {})))
|
|
519
|
+
: children;
|
|
366
520
|
return (_jsx(Comp
|
|
367
521
|
// asChild ケースで <a> 等を受け入れるため公開 API は HTMLElement で広く受けるが、
|
|
368
522
|
// 内部の Comp は <button> 固定の union が含まれるためここで narrow する。
|
|
@@ -371,13 +525,13 @@ const Button = React.forwardRef(function Button(_a, ref) {
|
|
|
371
525
|
// asChild ケースで <a> 等を受け入れるため公開 API は HTMLElement で広く受けるが、
|
|
372
526
|
// 内部の Comp は <button> 固定の union が含まれるためここで narrow する。
|
|
373
527
|
// en: Public ref is HTMLElement (covers asChild targets); inner Comp's button branch needs narrowing.
|
|
374
|
-
ref: ref, "data-slot": "button", "aria-busy": isLoading || undefined, "aria-disabled":
|
|
528
|
+
ref: ref }, buttonOnlyProps, { "data-slot": "button", "aria-busy": isLoading || undefined, "aria-disabled": !canUseButtonProps && isButtonDisabled ? true : ariaDisabled, "data-disabled": asChild && isButtonDisabled ? "true" : undefined, disabled: canUseButtonProps ? isButtonDisabled : undefined, className: cn(buttonVariants({
|
|
375
529
|
variant,
|
|
376
530
|
size,
|
|
377
531
|
theme,
|
|
378
532
|
isLoading,
|
|
379
533
|
className,
|
|
380
|
-
})),
|
|
381
|
-
}
|
|
534
|
+
})), onClickCapture: handleClickCapture, onAuxClickCapture: handleAuxClickCapture, onKeyDownCapture: handleKeyDownCapture, onClick: handleClick, onKeyDown: handleKeyDown }, restProps, { children: asChild ? (slottedChildren) : (_jsxs(_Fragment, { children: [prefixIcon && (_jsx(Icon, { icon: prefixIcon, size: getIconSize(), className: cn({ "opacity-0": isLoading }) })), isLoading ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex", children: _jsx(Spinner, { size: getIconSize(), className: "text-current" }) }), _jsx("span", { className: "opacity-0", children: children })] })) : (_jsx("span", { className: "px-1", children: children })), suffixIcon && (_jsx(Icon, { icon: suffixIcon, size: getIconSize(), className: cn({ "opacity-0": isLoading }) }))] })) })));
|
|
535
|
+
}
|
|
382
536
|
Button.displayName = "Button";
|
|
383
537
|
export { Button, buttonVariants };
|