@moontra/moonui 3.2.0 → 3.4.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.
@@ -490,4 +490,100 @@ describe('Slider Component', () => {
490
490
  expect(thumbs).toHaveLength(1)
491
491
  })
492
492
  })
493
+
494
+ // Regresyon koruması: thumb'lar `role="slider"` + `tabIndex=0` taşıyıp
495
+ // odaklanabiliyordu ama hiçbir klavye handler'ı yoktu — ARIA sözleşmesi
496
+ // slider vaat ediyor, bileşen yalnızca fareyle çalışıyordu (WCAG 2.1.1).
497
+ describe('Klavye ile değer değiştirme', () => {
498
+ it('ArrowRight ve ArrowUp değeri step kadar artırır', () => {
499
+ const onValueChange = jest.fn()
500
+ render(
501
+ <Slider defaultValue={[50]} min={0} max={100} step={1} onValueChange={onValueChange} />
502
+ )
503
+ const thumb = screen.getByRole('slider')
504
+
505
+ fireEvent.keyDown(thumb, { key: 'ArrowRight' })
506
+ expect(onValueChange).toHaveBeenLastCalledWith([51])
507
+
508
+ fireEvent.keyDown(thumb, { key: 'ArrowUp' })
509
+ expect(onValueChange).toHaveBeenLastCalledWith([52])
510
+ expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '52')
511
+ })
512
+
513
+ it('ArrowLeft ve ArrowDown değeri step kadar azaltır', () => {
514
+ const onValueChange = jest.fn()
515
+ render(
516
+ <Slider defaultValue={[50]} min={0} max={100} step={5} onValueChange={onValueChange} />
517
+ )
518
+ const thumb = screen.getByRole('slider')
519
+
520
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
521
+ expect(onValueChange).toHaveBeenLastCalledWith([45])
522
+
523
+ fireEvent.keyDown(thumb, { key: 'ArrowDown' })
524
+ expect(onValueChange).toHaveBeenLastCalledWith([40])
525
+ })
526
+
527
+ it('Home ve End uç değerlere taşır', () => {
528
+ const onValueChange = jest.fn()
529
+ render(
530
+ <Slider defaultValue={[50]} min={10} max={90} step={1} onValueChange={onValueChange} />
531
+ )
532
+ const thumb = screen.getByRole('slider')
533
+
534
+ fireEvent.keyDown(thumb, { key: 'Home' })
535
+ expect(onValueChange).toHaveBeenLastCalledWith([10])
536
+
537
+ fireEvent.keyDown(screen.getByRole('slider'), { key: 'End' })
538
+ expect(onValueChange).toHaveBeenLastCalledWith([90])
539
+ })
540
+
541
+ it('PageUp ve PageDown 10x step uygular', () => {
542
+ const onValueChange = jest.fn()
543
+ render(
544
+ <Slider defaultValue={[50]} min={0} max={100} step={2} onValueChange={onValueChange} />
545
+ )
546
+ fireEvent.keyDown(screen.getByRole('slider'), { key: 'PageUp' })
547
+ expect(onValueChange).toHaveBeenLastCalledWith([70])
548
+ })
549
+
550
+ it('min/max sınırlarını aşmaz', () => {
551
+ const onValueChange = jest.fn()
552
+ render(
553
+ <Slider defaultValue={[100]} min={0} max={100} step={1} onValueChange={onValueChange} />
554
+ )
555
+ fireEvent.keyDown(screen.getByRole('slider'), { key: 'ArrowRight' })
556
+ expect(onValueChange).toHaveBeenLastCalledWith([100])
557
+ })
558
+
559
+ it('disabled iken hiçbir tuş değeri değiştirmez', () => {
560
+ const onValueChange = jest.fn()
561
+ render(
562
+ <Slider defaultValue={[50]} min={0} max={100} disabled onValueChange={onValueChange} />
563
+ )
564
+ fireEvent.keyDown(screen.getByRole('slider'), { key: 'ArrowRight' })
565
+ expect(onValueChange).not.toHaveBeenCalled()
566
+ })
567
+
568
+ it('çoklu thumb komşusunun ötesine geçmez', () => {
569
+ const onValueChange = jest.fn()
570
+ render(
571
+ <Slider defaultValue={[40, 41]} min={0} max={100} step={1} onValueChange={onValueChange} />
572
+ )
573
+
574
+ // Soldaki thumb sağdakine (41) kadar çıkabilir, ötesine geçemez.
575
+ fireEvent.keyDown(screen.getAllByRole('slider')[0], { key: 'ArrowRight' })
576
+ expect(onValueChange).toHaveBeenLastCalledWith([41, 41])
577
+
578
+ fireEvent.keyDown(screen.getAllByRole('slider')[0], { key: 'ArrowRight' })
579
+ expect(onValueChange).toHaveBeenLastCalledWith([41, 41])
580
+ })
581
+
582
+ it('eşlenmemiş tuşlar değeri değiştirmez', () => {
583
+ const onValueChange = jest.fn()
584
+ render(<Slider defaultValue={[50]} onValueChange={onValueChange} />)
585
+ fireEvent.keyDown(screen.getByRole('slider'), { key: 'a' })
586
+ expect(onValueChange).not.toHaveBeenCalled()
587
+ })
588
+ })
493
589
  })
@@ -0,0 +1,323 @@
1
+ import React from 'react'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import '@testing-library/jest-dom'
4
+
5
+ import { ToggleGroup, ToggleGroupItem } from '../toggle-group'
6
+
7
+ describe('ToggleGroup Components', () => {
8
+ describe('Rendering', () => {
9
+ it('renders group with items', () => {
10
+ render(
11
+ <ToggleGroup type="single" data-testid="group">
12
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
13
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
14
+ </ToggleGroup>
15
+ )
16
+
17
+ const group = screen.getByTestId('group')
18
+ expect(group).toBeInTheDocument()
19
+ expect(screen.getByText('A')).toBeInTheDocument()
20
+ expect(screen.getByText('B')).toBeInTheDocument()
21
+ })
22
+
23
+ it('applies base + custom className on the root', () => {
24
+ render(
25
+ <ToggleGroup type="single" className="custom-group" data-testid="group">
26
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
27
+ </ToggleGroup>
28
+ )
29
+
30
+ const group = screen.getByTestId('group')
31
+ // moonui-theme + FREE token/layout base sınıfları korunur
32
+ expect(group).toHaveClass('moonui-theme', 'inline-flex', 'items-center', 'gap-1')
33
+ expect(group).toHaveClass('custom-group')
34
+ })
35
+
36
+ it('applies custom className on an item alongside toggleVariants', () => {
37
+ render(
38
+ <ToggleGroup type="single">
39
+ <ToggleGroupItem value="a" className="custom-item">
40
+ A
41
+ </ToggleGroupItem>
42
+ </ToggleGroup>
43
+ )
44
+
45
+ const item = screen.getByText('A')
46
+ expect(item).toHaveClass('custom-item')
47
+ // toggleVariants'tan gelen aktif-durum token sınıfı
48
+ expect(item).toHaveClass('data-[state=on]:bg-accent')
49
+ })
50
+
51
+ it('forwards ref on the group', () => {
52
+ const ref = React.createRef<HTMLDivElement>()
53
+ render(
54
+ <ToggleGroup type="single" ref={ref}>
55
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
56
+ </ToggleGroup>
57
+ )
58
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
59
+ })
60
+
61
+ it('forwards ref on the item', () => {
62
+ const ref = React.createRef<HTMLButtonElement>()
63
+ render(
64
+ <ToggleGroup type="single">
65
+ <ToggleGroupItem value="a" ref={ref}>
66
+ A
67
+ </ToggleGroupItem>
68
+ </ToggleGroup>
69
+ )
70
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement)
71
+ })
72
+
73
+ it('maintains displayNames', () => {
74
+ expect(ToggleGroup.displayName).toBe('ToggleGroup')
75
+ expect(ToggleGroupItem.displayName).toBe('ToggleGroupItem')
76
+ })
77
+ })
78
+
79
+ describe('Single selection (type="single")', () => {
80
+ it('marks exactly one item active via defaultValue', () => {
81
+ render(
82
+ <ToggleGroup type="single" defaultValue="b">
83
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
84
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
85
+ </ToggleGroup>
86
+ )
87
+
88
+ expect(screen.getByText('A')).toHaveAttribute('data-state', 'off')
89
+ expect(screen.getByText('B')).toHaveAttribute('data-state', 'on')
90
+ })
91
+
92
+ it('respects controlled value', () => {
93
+ render(
94
+ <ToggleGroup type="single" value="a">
95
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
96
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
97
+ </ToggleGroup>
98
+ )
99
+
100
+ expect(screen.getByText('A')).toHaveAttribute('data-state', 'on')
101
+ expect(screen.getByText('B')).toHaveAttribute('data-state', 'off')
102
+ })
103
+
104
+ it('fires onValueChange with a string on click', () => {
105
+ const onValueChange = jest.fn()
106
+ render(
107
+ <ToggleGroup type="single" onValueChange={onValueChange}>
108
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
109
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
110
+ </ToggleGroup>
111
+ )
112
+
113
+ fireEvent.click(screen.getByText('B'))
114
+ expect(onValueChange).toHaveBeenCalledWith('b')
115
+ })
116
+ })
117
+
118
+ describe('Multiple selection (type="multiple")', () => {
119
+ it('marks multiple items active via defaultValue', () => {
120
+ render(
121
+ <ToggleGroup type="multiple" defaultValue={['a', 'b']}>
122
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
123
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
124
+ <ToggleGroupItem value="c">C</ToggleGroupItem>
125
+ </ToggleGroup>
126
+ )
127
+
128
+ expect(screen.getByText('A')).toHaveAttribute('data-state', 'on')
129
+ expect(screen.getByText('B')).toHaveAttribute('data-state', 'on')
130
+ expect(screen.getByText('C')).toHaveAttribute('data-state', 'off')
131
+ })
132
+
133
+ it('fires onValueChange with an array on click', () => {
134
+ const onValueChange = jest.fn()
135
+ render(
136
+ <ToggleGroup type="multiple" defaultValue={['a']} onValueChange={onValueChange}>
137
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
138
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
139
+ </ToggleGroup>
140
+ )
141
+
142
+ fireEvent.click(screen.getByText('B'))
143
+ expect(onValueChange).toHaveBeenCalledWith(['a', 'b'])
144
+ })
145
+ })
146
+
147
+ describe('Variant / size context propagation', () => {
148
+ it('propagates group variant + size to items', () => {
149
+ render(
150
+ <ToggleGroup type="single" variant="outline" size="lg">
151
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
152
+ </ToggleGroup>
153
+ )
154
+
155
+ const item = screen.getByText('A')
156
+ // outline variant + lg size (toggleVariants ile birebir)
157
+ expect(item).toHaveClass('border', 'border-input')
158
+ expect(item).toHaveClass('h-11', 'px-5')
159
+ })
160
+
161
+ it('uses item prop when the group provides no variant/size', () => {
162
+ render(
163
+ <ToggleGroup type="single">
164
+ <ToggleGroupItem value="a" variant="outline" size="sm">
165
+ A
166
+ </ToggleGroupItem>
167
+ </ToggleGroup>
168
+ )
169
+
170
+ const item = screen.getByText('A')
171
+ // context tanımsız → item prop devreye girer
172
+ expect(item).toHaveClass('border', 'border-input')
173
+ expect(item).toHaveClass('h-9', 'px-2.5')
174
+ })
175
+
176
+ it('group variant overrides item variant (context wins)', () => {
177
+ render(
178
+ <ToggleGroup type="single" variant="outline">
179
+ <ToggleGroupItem value="a" variant="default">
180
+ A
181
+ </ToggleGroupItem>
182
+ </ToggleGroup>
183
+ )
184
+
185
+ const item = screen.getByText('A')
186
+ // context.variant ?? props.variant → outline kazanır
187
+ expect(item).toHaveClass('border', 'border-input')
188
+ })
189
+
190
+ it('falls back to default variant/size when nothing is set', () => {
191
+ render(
192
+ <ToggleGroup type="single">
193
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
194
+ </ToggleGroup>
195
+ )
196
+
197
+ const item = screen.getByText('A')
198
+ // default variant: bg-transparent, default size: h-10 px-3
199
+ expect(item).toHaveClass('bg-transparent', 'h-10', 'px-3')
200
+ })
201
+ })
202
+
203
+ describe('Disabled state', () => {
204
+ it('disables all items when the group is disabled', () => {
205
+ render(
206
+ <ToggleGroup type="single" disabled>
207
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
208
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
209
+ </ToggleGroup>
210
+ )
211
+
212
+ expect(screen.getByText('A')).toBeDisabled()
213
+ expect(screen.getByText('B')).toBeDisabled()
214
+ })
215
+
216
+ it('disables an individual item', () => {
217
+ const onValueChange = jest.fn()
218
+ render(
219
+ <ToggleGroup type="single" onValueChange={onValueChange}>
220
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
221
+ <ToggleGroupItem value="b" disabled>
222
+ B
223
+ </ToggleGroupItem>
224
+ </ToggleGroup>
225
+ )
226
+
227
+ const disabledItem = screen.getByText('B')
228
+ expect(disabledItem).toBeDisabled()
229
+ fireEvent.click(disabledItem)
230
+ expect(onValueChange).not.toHaveBeenCalled()
231
+ })
232
+ })
233
+
234
+ describe('Accessibility (Radix-provided)', () => {
235
+ it('single group exposes radiogroup + radio roles', () => {
236
+ render(
237
+ <ToggleGroup type="single" data-testid="group">
238
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
239
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
240
+ </ToggleGroup>
241
+ )
242
+
243
+ expect(screen.getByTestId('group')).toHaveAttribute('role', 'radiogroup')
244
+ expect(screen.getAllByRole('radio')).toHaveLength(2)
245
+ })
246
+
247
+ it('single active item reflects aria-checked', () => {
248
+ render(
249
+ <ToggleGroup type="single" defaultValue="a">
250
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
251
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
252
+ </ToggleGroup>
253
+ )
254
+
255
+ expect(screen.getByText('A')).toHaveAttribute('aria-checked', 'true')
256
+ expect(screen.getByText('B')).toHaveAttribute('aria-checked', 'false')
257
+ })
258
+
259
+ it('multiple group exposes toolbar role + aria-pressed items', () => {
260
+ render(
261
+ <ToggleGroup type="multiple" defaultValue={['a']} data-testid="group">
262
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
263
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
264
+ </ToggleGroup>
265
+ )
266
+
267
+ expect(screen.getByTestId('group')).toHaveAttribute('role', 'toolbar')
268
+ expect(screen.getByText('A')).toHaveAttribute('aria-pressed', 'true')
269
+ expect(screen.getByText('B')).toHaveAttribute('aria-pressed', 'false')
270
+ })
271
+
272
+ it('renders items as focusable buttons with Radix roving tabindex', () => {
273
+ render(
274
+ <ToggleGroup type="single" defaultValue="a">
275
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
276
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
277
+ </ToggleGroup>
278
+ )
279
+
280
+ const first = screen.getByText('A')
281
+ const second = screen.getByText('B')
282
+
283
+ // Radix RovingFocusGroup her item'a bir tabindex atar (roving nav altyapısı).
284
+ // Ok-tuşuyla odak taşıma gerçek tarayıcıda Radix tarafından sağlanır;
285
+ // jsdom odak olaylarını tam simüle etmediği için burada odaklanabilirlik doğrulanır.
286
+ expect(first).toHaveAttribute('tabindex')
287
+ expect(second).toHaveAttribute('tabindex')
288
+
289
+ first.focus()
290
+ expect(first).toHaveFocus()
291
+ })
292
+ })
293
+
294
+ describe('Orientation', () => {
295
+ it('varsayılan/yatay yerleşimde taban sınıflar korunur', () => {
296
+ render(
297
+ <ToggleGroup type="single" data-testid="group">
298
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
299
+ </ToggleGroup>
300
+ )
301
+
302
+ const group = screen.getByTestId('group')
303
+ expect(group).toHaveClass('inline-flex', 'items-center', 'justify-center')
304
+ })
305
+
306
+ it('orientation="vertical" dikey yerleşim sınıflarını devreye alır', () => {
307
+ render(
308
+ <ToggleGroup type="single" orientation="vertical" data-testid="group">
309
+ <ToggleGroupItem value="a">A</ToggleGroupItem>
310
+ <ToggleGroupItem value="b">B</ToggleGroupItem>
311
+ </ToggleGroup>
312
+ )
313
+
314
+ const group = screen.getByTestId('group')
315
+ // Radix roving-focus köke data-orientation yazar; dikey sınıflar buna bağlıdır.
316
+ // Eskiden kök className sabitti: `orientation` tip yüzeyinde vardı ama
317
+ // yerleşime hiç yansımıyordu (kardeş button-group bunu doğru yapıyor).
318
+ expect(group).toHaveAttribute('data-orientation', 'vertical')
319
+ expect(group.className).toContain('data-[orientation=vertical]:flex-col')
320
+ expect(group.className).toContain('data-[orientation=vertical]:items-stretch')
321
+ })
322
+ })
323
+ })
@@ -0,0 +1,125 @@
1
+ "use client"
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../../lib/utils";
6
+ import { Button, type ButtonProps } from "./button";
7
+
8
+ /**
9
+ * Premium ButtonGroup Component
10
+ *
11
+ * Bitişik (segmentli) veya boşluklu buton grupları için yüksek kaliteli,
12
+ * erişilebilir bir kapsayıcı bileşen. Yatay/dikey yerleşim, bitişik kenar
13
+ * radius birleştirme ve çocuk `<Button>`'lara size/variant yayılımı sunar.
14
+ *
15
+ * Tamamen token-tabanlıdır: kenarlık ve köşe yuvarlama değerleri mevcut Button
16
+ * token'larından gelir; hardcoded renk YOKtur.
17
+ */
18
+ const buttonGroupVariants = cva(
19
+ // Temel kapsayıcı: inline-flex; çocuklar akış içinde hizalanır
20
+ "moonui-theme inline-flex",
21
+ {
22
+ variants: {
23
+ orientation: {
24
+ // Yatay: butonlar dikeyde ortalanır (içerik-genişliği korunur)
25
+ horizontal: "flex-row items-center",
26
+ // Dikey: butonlar aynı genişliğe uzar (items-stretch) → kenarlar hizalanır
27
+ vertical: "flex-col items-stretch",
28
+ },
29
+ attached: {
30
+ // Bitişik: boşluk yok; radius birleştirme compoundVariants ile uygulanır
31
+ true: "",
32
+ // Boşluklu: normal aralık, radius korunur
33
+ false: "gap-2",
34
+ },
35
+ },
36
+ compoundVariants: [
37
+ {
38
+ // Yatay + bitişik: bitişik yatay kenarlar düzleşir, -ml-px ile kenarlık örtüşür
39
+ orientation: "horizontal",
40
+ attached: true,
41
+ class: [
42
+ "[&>*:not(:first-child)]:rounded-l-none",
43
+ "[&>*:not(:last-child)]:rounded-r-none",
44
+ "[&>*:not(:first-child)]:-ml-px",
45
+ ],
46
+ },
47
+ {
48
+ // Dikey + bitişik: bitişik dikey kenarlar düzleşir, -mt-px ile kenarlık örtüşür
49
+ orientation: "vertical",
50
+ attached: true,
51
+ class: [
52
+ "[&>*:not(:first-child)]:rounded-t-none",
53
+ "[&>*:not(:last-child)]:rounded-b-none",
54
+ "[&>*:not(:first-child)]:-mt-px",
55
+ ],
56
+ },
57
+ ],
58
+ defaultVariants: {
59
+ orientation: "horizontal",
60
+ attached: true,
61
+ },
62
+ }
63
+ );
64
+
65
+ // ButtonGroup component props
66
+ export interface ButtonGroupProps
67
+ extends React.HTMLAttributes<HTMLDivElement>,
68
+ VariantProps<typeof buttonGroupVariants> {
69
+ /** Çocuk Button'lara yayılacak boyut (yalnızca çocuk kendi size'ını vermemişse) */
70
+ size?: ButtonProps["size"];
71
+ /** Çocuk Button'lara yayılacak varyant (yalnızca çocuk kendi variant'ını vermemişse) */
72
+ variant?: ButtonProps["variant"];
73
+ }
74
+
75
+ /**
76
+ * Premium ButtonGroup Component
77
+ *
78
+ * @param props - ButtonGroup bileşeni özellikleri
79
+ * @param props.orientation - Yerleşim yönü ("horizontal" | "vertical"), varsayılan "horizontal"
80
+ * @param props.attached - Bitişik (segmentli) mi yoksa boşluklu mu, varsayılan true
81
+ * @param props.size - Çocuk Button'lara yayılacak boyut (opsiyonel)
82
+ * @param props.variant - Çocuk Button'lara yayılacak varyant (opsiyonel)
83
+ */
84
+ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
85
+ (
86
+ { className, orientation, attached, size, variant, children, ...props },
87
+ ref
88
+ ) => {
89
+ // size/variant yalnızca en az biri tanımlıysa çocuklara enjekte edilir
90
+ const shouldPropagate = size !== undefined || variant !== undefined;
91
+
92
+ const enhancedChildren = shouldPropagate
93
+ ? React.Children.map(children, (child) => {
94
+ // Geçersiz element (string/number/null) veya Button olmayan çocuklar
95
+ // GRACEFUL geçilir — geçersiz prop enjekte edilmez
96
+ if (!React.isValidElement(child) || child.type !== Button) {
97
+ return child;
98
+ }
99
+
100
+ const childProps = child.props as ButtonProps;
101
+
102
+ // Çocuğun kendi prop'u önceliklidir (child.props.size ?? size)
103
+ return React.cloneElement(child as React.ReactElement<ButtonProps>, {
104
+ size: childProps.size ?? size,
105
+ variant: childProps.variant ?? variant,
106
+ });
107
+ })
108
+ : children;
109
+
110
+ return (
111
+ <div
112
+ ref={ref}
113
+ role="group"
114
+ className={cn(buttonGroupVariants({ orientation, attached }), className)}
115
+ {...props}
116
+ >
117
+ {enhancedChildren}
118
+ </div>
119
+ );
120
+ }
121
+ );
122
+
123
+ ButtonGroup.displayName = "ButtonGroup";
124
+
125
+ export { ButtonGroup, buttonGroupVariants };
@@ -15,14 +15,14 @@ import { cn } from "../../lib/utils";
15
15
  */
16
16
 
17
17
  const checkboxVariants = cva(
18
- "peer shrink-0 border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:text-primary-foreground",
18
+ "peer shrink-0 border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:text-primary-foreground data-[state=indeterminate]:text-primary-foreground",
19
19
  {
20
20
  variants: {
21
21
  variant: {
22
- default: "border-border bg-background data-[state=checked]:bg-primary data-[state=checked]:border-primary",
23
- outline: "border-border bg-transparent data-[state=checked]:bg-primary data-[state=checked]:border-primary",
24
- muted: "border-border bg-accent data-[state=checked]:bg-primary data-[state=checked]:border-primary",
25
- ghost: "border-transparent bg-transparent hover:bg-accent data-[state=checked]:bg-primary data-[state=checked]:border-primary",
22
+ default: "border-border bg-background data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary",
23
+ outline: "border-border bg-transparent data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary",
24
+ muted: "border-border bg-accent data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary",
25
+ ghost: "border-transparent bg-transparent hover:bg-accent data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary",
26
26
  },
27
27
  size: {
28
28
  sm: "h-3.5 w-3.5",
@@ -80,16 +80,18 @@ const Checkbox = React.forwardRef<
80
80
  checked,
81
81
  ...props
82
82
  }, ref) => {
83
- // Indeterminate state yönetimi
84
- const [isIndeterminate, setIsIndeterminate] = React.useState(indeterminate);
85
-
86
- React.useEffect(() => {
87
- setIsIndeterminate(indeterminate);
88
- }, [indeterminate]);
83
+ // Radix'in `CheckedState` tipi: boolean | "indeterminate".
84
+ // "indeterminate" iletildiğinde Radix kökte `data-state="indeterminate"` +
85
+ // `aria-checked="mixed"` üretir ve Indicator'ı render eder — Minus ikonu ancak
86
+ // bu durumda görünür olur.
87
+ //
88
+ // Eskiden burada `false` iletiliyordu: kök hep `data-state="unchecked"` kalıyor,
89
+ // Radix Indicator unchecked durumda render edilmediği için Minus hiç görünmüyor
90
+ // ve ekran okuyucu "kısmen seçili" durumunu duyuramıyordu.
91
+ //
92
+ // `checked` undefined ise olduğu gibi geçirilir — uncontrolled kullanım korunur.
93
+ const effectiveChecked = indeterminate ? "indeterminate" : checked;
89
94
 
90
- // Checked state override, indeterminate olduğunda
91
- const effectiveChecked = isIndeterminate ? false : checked;
92
-
93
95
  return (
94
96
  <CheckboxPrimitive.Root
95
97
  ref={ref}
@@ -103,7 +105,7 @@ const Checkbox = React.forwardRef<
103
105
  animation === "bounce" && "data-[state=checked]:animate-bounce"
104
106
  )}
105
107
  >
106
- {isIndeterminate ? (
108
+ {indeterminate ? (
107
109
  <Minus className="h-[65%] w-[65%]" />
108
110
  ) : icon ? (
109
111
  icon
@@ -117,7 +119,7 @@ const Checkbox = React.forwardRef<
117
119
  Checkbox.displayName = CheckboxPrimitive.Root.displayName;
118
120
 
119
121
  // CheckboxGroup bileşeni
120
- interface CheckboxGroupProps extends React.HTMLAttributes<HTMLDivElement> {
122
+ export interface CheckboxGroupProps extends React.HTMLAttributes<HTMLDivElement> {
121
123
  /**
122
124
  * Grup içi yerleşim - dikey veya yatay
123
125
  */
@@ -154,7 +156,7 @@ const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
154
156
  CheckboxGroup.displayName = "CheckboxGroup";
155
157
 
156
158
  // CheckboxLabel bileşeni
157
- interface CheckboxLabelProps extends React.HTMLAttributes<HTMLLabelElement> {
159
+ export interface CheckboxLabelProps extends React.HTMLAttributes<HTMLLabelElement> {
158
160
  /**
159
161
  * Checkbox bileşeni için HTML id
160
162
  */
@@ -195,7 +197,7 @@ const CheckboxLabel = React.forwardRef<HTMLLabelElement, CheckboxLabelProps>(
195
197
  CheckboxLabel.displayName = "CheckboxLabel";
196
198
 
197
199
  // Checkbox ve Label içeren bileşen
198
- interface CheckboxWithLabelProps extends CheckboxProps {
200
+ export interface CheckboxWithLabelProps extends CheckboxProps {
199
201
  /**
200
202
  * Label içeriği
201
203
  */
@@ -528,8 +528,14 @@ export function ColorPicker({
528
528
  {presets.map((presetColor) => (
529
529
  <button
530
530
  key={presetColor}
531
+ type="button"
532
+ // Swatch'ın tek içeriği arka plan rengi — erişilebilir ad
533
+ // olmadan ekran okuyucu yalnızca "button" diye okur.
534
+ aria-label={`Select color ${presetColor}`}
535
+ aria-pressed={color === presetColor}
531
536
  className={cn(
532
537
  "h-7 w-7 rounded border-2 transition-all hover:scale-110",
538
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
533
539
  color === presetColor
534
540
  ? "border-primary"
535
541
  : "border-transparent"
@@ -578,8 +584,14 @@ export function SimpleColorPicker({
578
584
  {colors.map((color) => (
579
585
  <button
580
586
  key={color}
587
+ type="button"
588
+ // Swatch'ın tek içeriği arka plan rengi — erişilebilir ad olmadan
589
+ // ekran okuyucu yalnızca "button" diye okur.
590
+ aria-label={`Select color ${color}`}
591
+ aria-pressed={value === color}
581
592
  className={cn(
582
593
  "rounded-full border-2 transition-all hover:scale-110",
594
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
583
595
  sizeClasses[size],
584
596
  value === color ? "border-primary ring-2 ring-primary/20" : "border-transparent"
585
597
  )}