@moontra/moonui 3.3.0 → 4.0.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/index.d.mts +25 -7
- package/dist/index.d.ts +25 -7
- package/dist/index.global.js +20 -20
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +503 -353
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +467 -318
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/alert.test.tsx +5 -5
- package/src/components/ui/__tests__/avatar.test.tsx +12 -12
- package/src/components/ui/__tests__/badge.test.tsx +17 -1
- package/src/components/ui/__tests__/checkbox.test.tsx +16 -12
- package/src/components/ui/__tests__/color-picker.test.tsx +147 -331
- package/src/components/ui/__tests__/radio-group.test.tsx +60 -0
- package/src/components/ui/__tests__/select.test.tsx +24 -2
- package/src/components/ui/__tests__/slider.test.tsx +96 -0
- package/src/components/ui/__tests__/switch.test.tsx +36 -3
- package/src/components/ui/__tests__/toggle-group.test.tsx +30 -0
- package/src/components/ui/alert.tsx +5 -2
- package/src/components/ui/avatar.tsx +18 -6
- package/src/components/ui/badge.tsx +18 -11
- package/src/components/ui/checkbox.tsx +20 -18
- package/src/components/ui/color-picker.tsx +12 -0
- package/src/components/ui/index.ts +17 -0
- package/src/components/ui/radio-group.tsx +47 -8
- package/src/components/ui/select.tsx +59 -23
- package/src/components/ui/slider.tsx +56 -2
- package/src/components/ui/switch.tsx +108 -51
- package/src/components/ui/toggle-group.tsx +4 -0
|
@@ -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
|
})
|
|
@@ -83,8 +83,8 @@ describe('Switch Component', () => {
|
|
|
83
83
|
expect(switchElement).toHaveClass('data-[state=checked]:bg-warning')
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
it('renders
|
|
87
|
-
render(<Switch variant="
|
|
86
|
+
it('renders destructive variant correctly', () => {
|
|
87
|
+
render(<Switch variant="destructive" data-testid="switch" />)
|
|
88
88
|
const switchElement = screen.getByTestId('switch')
|
|
89
89
|
expect(switchElement).toHaveClass('data-[state=checked]:bg-error')
|
|
90
90
|
})
|
|
@@ -331,7 +331,7 @@ describe('Switch Component', () => {
|
|
|
331
331
|
<Switch
|
|
332
332
|
loading
|
|
333
333
|
size="sm"
|
|
334
|
-
variant="
|
|
334
|
+
variant="destructive"
|
|
335
335
|
leftIcon={<span>Left</span>}
|
|
336
336
|
description="Loading switch"
|
|
337
337
|
data-testid="switch"
|
|
@@ -369,4 +369,37 @@ describe('Switch Component', () => {
|
|
|
369
369
|
expect(switchElement).toBeInTheDocument()
|
|
370
370
|
})
|
|
371
371
|
})
|
|
372
|
+
|
|
373
|
+
describe('description ↔ aria-describedby bağı (#271)', () => {
|
|
374
|
+
it('description verildiğinde switch ile programatik olarak ilişkilendirilir', () => {
|
|
375
|
+
// Regresyon: description düz <span> olarak render ediliyor ama Root'a
|
|
376
|
+
// hiç bağlanmıyordu — ekran okuyucu açıklamayı switch ile ilişkilendiremiyordu.
|
|
377
|
+
render(<Switch description="Bildirimleri aç" data-testid="switch" />)
|
|
378
|
+
|
|
379
|
+
const el = screen.getByTestId('switch')
|
|
380
|
+
const describedBy = el.getAttribute('aria-describedby')
|
|
381
|
+
expect(describedBy).toBeTruthy()
|
|
382
|
+
|
|
383
|
+
const descEl = document.getElementById(describedBy!)
|
|
384
|
+
expect(descEl).toHaveTextContent('Bildirimleri aç')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('description yoksa aria-describedby basılmaz', () => {
|
|
388
|
+
render(<Switch data-testid="switch" />)
|
|
389
|
+
expect(screen.getByTestId('switch')).not.toHaveAttribute('aria-describedby')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('tüketicinin kendi aria-describedby değeri korunur', () => {
|
|
393
|
+
render(
|
|
394
|
+
<>
|
|
395
|
+
<span id="dis">Harici açıklama</span>
|
|
396
|
+
<Switch description="Dahili" aria-describedby="dis" data-testid="switch" />
|
|
397
|
+
</>
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
const describedBy = screen.getByTestId('switch').getAttribute('aria-describedby')!
|
|
401
|
+
expect(describedBy.split(' ')).toContain('dis')
|
|
402
|
+
expect(describedBy.split(' ').length).toBe(2)
|
|
403
|
+
})
|
|
404
|
+
})
|
|
372
405
|
})
|
|
@@ -290,4 +290,34 @@ describe('ToggleGroup Components', () => {
|
|
|
290
290
|
expect(first).toHaveFocus()
|
|
291
291
|
})
|
|
292
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
|
+
})
|
|
293
323
|
})
|
|
@@ -23,7 +23,10 @@ const alertVariants = cva(
|
|
|
23
23
|
success: "bg-success/10 text-success border-success/30",
|
|
24
24
|
warning: "bg-warning/10 text-warning border-warning/30",
|
|
25
25
|
error: "bg-destructive/10 text-destructive border-destructive/30",
|
|
26
|
-
info
|
|
26
|
+
// `--info` token'ı (tokens.css) blue-500 ile birebir aynı değeri
|
|
27
|
+
// taşır → görsel değişiklik yok, ancak varyant artık temaya ve
|
|
28
|
+
// karanlık moda duyarlı. Kardeş varyantlarla simetrik.
|
|
29
|
+
info: "bg-info/10 text-info border-info/30",
|
|
27
30
|
},
|
|
28
31
|
size: {
|
|
29
32
|
sm: "py-2 text-xs",
|
|
@@ -114,7 +117,7 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
|
|
114
117
|
<button
|
|
115
118
|
onClick={onClose}
|
|
116
119
|
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded-full opacity-70 transition-opacity hover:opacity-100"
|
|
117
|
-
aria-label="
|
|
120
|
+
aria-label="Close alert"
|
|
118
121
|
>
|
|
119
122
|
<X className="h-4 w-4" />
|
|
120
123
|
</button>
|
|
@@ -85,16 +85,28 @@ const AvatarFallback = React.forwardRef<
|
|
|
85
85
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
|
86
86
|
|
|
87
87
|
// Avatar Group Component for displaying multiple avatars
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
//
|
|
89
|
+
// KIRICI (issue #260): prop sözleşmesi `limit`/`avatars: ReactNode[]` idi;
|
|
90
|
+
// pro paketteki kardeşi (`MoonUIAvatarGroupPro`) ise `max`/`children` kullanıyordu
|
|
91
|
+
// — aynı bileşen ailesi için iki farklı API. Standart React deseni (children)
|
|
92
|
+
// kanonik kabul edilip free tarafı pro'ya hizalandı.
|
|
93
|
+
//
|
|
94
|
+
// Geçiş: <AvatarGroup limit={3} avatars={[<Avatar/>, <Avatar/>]} />
|
|
95
|
+
// → <AvatarGroup max={3}><Avatar/><Avatar/></AvatarGroup>
|
|
96
|
+
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
97
|
+
/** Gösterilecek en fazla avatar sayısı; kalanlar "+N" olarak özetlenir. */
|
|
98
|
+
max?: number;
|
|
99
|
+
/** Avatar bileşenleri. */
|
|
100
|
+
children?: React.ReactNode;
|
|
101
|
+
/** Avatar'lar arası üst üste binme miktarı (px, negatif). */
|
|
91
102
|
overlapOffset?: number;
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
95
|
-
({ className,
|
|
96
|
-
const
|
|
97
|
-
const
|
|
106
|
+
({ className, max, children, overlapOffset = -8, ...props }, ref) => {
|
|
107
|
+
const childrenArray = React.Children.toArray(children);
|
|
108
|
+
const visibleAvatars = max ? childrenArray.slice(0, max) : childrenArray;
|
|
109
|
+
const remainingCount = max ? Math.max(0, childrenArray.length - max) : 0;
|
|
98
110
|
|
|
99
111
|
return (
|
|
100
112
|
<div
|
|
@@ -21,11 +21,15 @@ const badgeVariants = cva(
|
|
|
21
21
|
{
|
|
22
22
|
variants: {
|
|
23
23
|
variant: {
|
|
24
|
+
// KIRICI (issue #261): önceden ham `gray-900`/`white` kullanılıyordu;
|
|
25
|
+
// aynı varyant pro pakette token'lıydı → free ve pro `primary` için
|
|
26
|
+
// farklı renk üretiyordu. Artık token'a bağlı (tema/karanlık moda duyarlı).
|
|
27
|
+
// `primary` VARSAYILAN varyant olduğu için görünür bir renk değişimidir.
|
|
24
28
|
primary: [
|
|
25
|
-
"border-transparent bg-
|
|
26
|
-
"hover:bg-
|
|
27
|
-
"focus-visible:ring-
|
|
28
|
-
"dark:bg-
|
|
29
|
+
"border-transparent bg-primary text-primary-foreground",
|
|
30
|
+
"hover:bg-primary/90",
|
|
31
|
+
"focus-visible:ring-primary/30 dark:focus-visible:ring-primary/40",
|
|
32
|
+
"dark:bg-primary/90 dark:shadow-inner dark:shadow-primary/10",
|
|
29
33
|
],
|
|
30
34
|
secondary: [
|
|
31
35
|
"border-transparent bg-secondary text-secondary-foreground",
|
|
@@ -127,7 +131,7 @@ export interface BadgeProps
|
|
|
127
131
|
* @param props.rightIcon - Badge'in sağında görüntülenecek ikon
|
|
128
132
|
* @param props.noAutoSpacing - Otomatik spacing'i devre dışı bırak
|
|
129
133
|
*/
|
|
130
|
-
|
|
134
|
+
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(({
|
|
131
135
|
className,
|
|
132
136
|
variant,
|
|
133
137
|
size,
|
|
@@ -141,7 +145,7 @@ function Badge({
|
|
|
141
145
|
noAutoSpacing,
|
|
142
146
|
children,
|
|
143
147
|
...props
|
|
144
|
-
}
|
|
148
|
+
}, ref) => {
|
|
145
149
|
// Auto-assign icons and content for special variants
|
|
146
150
|
let autoLeftIcon = leftIcon;
|
|
147
151
|
let autoChildren = children;
|
|
@@ -169,14 +173,15 @@ function Badge({
|
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
return (
|
|
172
|
-
<div
|
|
176
|
+
<div
|
|
177
|
+
ref={ref}
|
|
173
178
|
className={cn(
|
|
174
|
-
"moonui-theme",
|
|
175
|
-
badgeVariants({ variant, size, radius }),
|
|
179
|
+
"moonui-theme",
|
|
180
|
+
badgeVariants({ variant, size, radius }),
|
|
176
181
|
// Remove auto-spacing if noAutoSpacing is true
|
|
177
182
|
noAutoSpacing && "mr-0 mb-0",
|
|
178
183
|
className
|
|
179
|
-
)}
|
|
184
|
+
)}
|
|
180
185
|
data-removable={removable ? "" : undefined}
|
|
181
186
|
{...props}
|
|
182
187
|
>
|
|
@@ -234,6 +239,8 @@ function Badge({
|
|
|
234
239
|
)}
|
|
235
240
|
</div>
|
|
236
241
|
);
|
|
237
|
-
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
Badge.displayName = "Badge";
|
|
238
245
|
|
|
239
246
|
export { Badge, badgeVariants };
|
|
@@ -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
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
{
|
|
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
|
)}
|
|
@@ -34,6 +34,7 @@ export {
|
|
|
34
34
|
|
|
35
35
|
export type {
|
|
36
36
|
AvatarProps as MoonUIAvatarProps,
|
|
37
|
+
AvatarGroupProps as MoonUIAvatarGroupProps,
|
|
37
38
|
} from "./avatar";
|
|
38
39
|
|
|
39
40
|
// Badge
|
|
@@ -125,6 +126,9 @@ export {
|
|
|
125
126
|
|
|
126
127
|
export type {
|
|
127
128
|
CheckboxProps as MoonUICheckboxProps,
|
|
129
|
+
CheckboxGroupProps as MoonUICheckboxGroupProps,
|
|
130
|
+
CheckboxLabelProps as MoonUICheckboxLabelProps,
|
|
131
|
+
CheckboxWithLabelProps as MoonUICheckboxWithLabelProps,
|
|
128
132
|
} from "./checkbox";
|
|
129
133
|
|
|
130
134
|
// Collapsible
|
|
@@ -302,6 +306,9 @@ export {
|
|
|
302
306
|
|
|
303
307
|
export type {
|
|
304
308
|
RadioGroupProps as MoonUIRadioGroupProps,
|
|
309
|
+
RadioGroupItemProps as MoonUIRadioGroupItemProps,
|
|
310
|
+
RadioLabelProps as MoonUIRadioLabelProps,
|
|
311
|
+
RadioItemWithLabelProps as MoonUIRadioItemWithLabelProps,
|
|
305
312
|
} from "./radio-group";
|
|
306
313
|
|
|
307
314
|
// RichTextEditor
|
|
@@ -334,6 +341,11 @@ export {
|
|
|
334
341
|
SelectScrollDownButton as MoonUISelectScrollDownButton,
|
|
335
342
|
} from "./select";
|
|
336
343
|
|
|
344
|
+
export type {
|
|
345
|
+
SelectTriggerProps as MoonUISelectTriggerProps,
|
|
346
|
+
SelectItemProps as MoonUISelectItemProps,
|
|
347
|
+
} from "./select";
|
|
348
|
+
|
|
337
349
|
// Separator
|
|
338
350
|
export { Separator as MoonUISeparator } from "./separator";
|
|
339
351
|
|
|
@@ -356,6 +368,11 @@ export type {
|
|
|
356
368
|
// Slider
|
|
357
369
|
export { Slider as MoonUISlider } from "./slider";
|
|
358
370
|
|
|
371
|
+
export type {
|
|
372
|
+
SliderProps as MoonUISliderProps,
|
|
373
|
+
SliderBaseProps as MoonUISliderBaseProps,
|
|
374
|
+
} from "./slider";
|
|
375
|
+
|
|
359
376
|
// SwipeableCard
|
|
360
377
|
export { SwipeableCard as MoonUISwipeableCard } from "./swipeable-card";
|
|
361
378
|
|
|
@@ -36,11 +36,20 @@ const radioGroupItemVariants = cva(
|
|
|
36
36
|
}
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// NOT: `React.HTMLAttributes` kendi `defaultValue`'sunu
|
|
40
|
+
// (`string | number | readonly string[]`) taşır; radio grubu için tek bir `string`
|
|
41
|
+
// anlamlı olduğundan Omit ile daraltılır.
|
|
42
|
+
export interface RadioGroupProps
|
|
43
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue"> {
|
|
40
44
|
/**
|
|
41
|
-
* Radio group value
|
|
45
|
+
* Radio group value (controlled)
|
|
42
46
|
*/
|
|
43
47
|
value?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Uncontrolled kullanımda başlangıç değeri. `value` verilmediğinde grup
|
|
50
|
+
* seçimi kendi iç state'inde tutar.
|
|
51
|
+
*/
|
|
52
|
+
defaultValue?: string;
|
|
44
53
|
/**
|
|
45
54
|
* Function to call when radio group value changes
|
|
46
55
|
*/
|
|
@@ -63,9 +72,34 @@ const RadioGroupContext = React.createContext<{
|
|
|
63
72
|
}>({});
|
|
64
73
|
|
|
65
74
|
const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
66
|
-
({ className, value, onValueChange, disabled, name, ...props }, ref) => {
|
|
75
|
+
({ className, value, defaultValue, onValueChange, disabled, name, ...props }, ref) => {
|
|
76
|
+
// Controlled/uncontrolled ikili mod: `value` verilmişse tüketici yönetir,
|
|
77
|
+
// verilmemişse grup seçimi kendi iç state'inde tutar.
|
|
78
|
+
const [internalValue, setInternalValue] = React.useState<string | undefined>(
|
|
79
|
+
defaultValue
|
|
80
|
+
);
|
|
81
|
+
const isControlled = value !== undefined;
|
|
82
|
+
const currentValue = isControlled ? value : internalValue;
|
|
83
|
+
|
|
84
|
+
const handleValueChange = React.useCallback(
|
|
85
|
+
(next: string) => {
|
|
86
|
+
if (!isControlled) {
|
|
87
|
+
setInternalValue(next);
|
|
88
|
+
}
|
|
89
|
+
onValueChange?.(next);
|
|
90
|
+
},
|
|
91
|
+
[isControlled, onValueChange]
|
|
92
|
+
);
|
|
93
|
+
|
|
67
94
|
return (
|
|
68
|
-
<RadioGroupContext.Provider
|
|
95
|
+
<RadioGroupContext.Provider
|
|
96
|
+
value={{
|
|
97
|
+
value: currentValue,
|
|
98
|
+
onValueChange: handleValueChange,
|
|
99
|
+
disabled,
|
|
100
|
+
name,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
69
103
|
<div
|
|
70
104
|
ref={ref}
|
|
71
105
|
role="radiogroup"
|
|
@@ -131,7 +165,7 @@ const RadioGroupItem = React.forwardRef<
|
|
|
131
165
|
disabled={disabled || radioGroup.disabled}
|
|
132
166
|
name={radioGroup.name}
|
|
133
167
|
onChange={handleChange}
|
|
134
|
-
className="sr-only"
|
|
168
|
+
className="peer sr-only"
|
|
135
169
|
{...props}
|
|
136
170
|
/>
|
|
137
171
|
<label
|
|
@@ -139,7 +173,12 @@ const RadioGroupItem = React.forwardRef<
|
|
|
139
173
|
className={cn(
|
|
140
174
|
radioGroupItemVariants({ variant, size }),
|
|
141
175
|
"rounded-full",
|
|
142
|
-
|
|
176
|
+
// Gerçekte odaklanan eleman görsel olarak gizli `sr-only` input'tur.
|
|
177
|
+
// Görünür label onun KARDEŞİ olduğu için label'da `:focus-visible` hiç
|
|
178
|
+
// tetiklenmiyor, dolayısıyla klavye kullanıcısı hiçbir focus izi
|
|
179
|
+
// görmüyordu. Tailwind `peer` deseniyle (input `peer` sınıfını taşır ve
|
|
180
|
+
// DOM'da label'dan önce gelir) focus izi görünür elemana taşınır.
|
|
181
|
+
"peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-primary/50",
|
|
143
182
|
"relative inline-flex shrink-0 cursor-pointer items-center justify-center overflow-hidden",
|
|
144
183
|
disabled && "cursor-not-allowed opacity-50",
|
|
145
184
|
className
|
|
@@ -158,7 +197,7 @@ const RadioGroupItem = React.forwardRef<
|
|
|
158
197
|
RadioGroupItem.displayName = "RadioGroupItem";
|
|
159
198
|
|
|
160
199
|
// Radio Label Component
|
|
161
|
-
interface RadioLabelProps extends React.HTMLAttributes<HTMLLabelElement> {
|
|
200
|
+
export interface RadioLabelProps extends React.HTMLAttributes<HTMLLabelElement> {
|
|
162
201
|
/**
|
|
163
202
|
* HTML id for radio button
|
|
164
203
|
*/
|
|
@@ -194,7 +233,7 @@ const RadioLabel = React.forwardRef<HTMLLabelElement, RadioLabelProps>(
|
|
|
194
233
|
RadioLabel.displayName = "RadioLabel";
|
|
195
234
|
|
|
196
235
|
// Radio Item and Label combination
|
|
197
|
-
interface RadioItemWithLabelProps extends RadioGroupItemProps {
|
|
236
|
+
export interface RadioItemWithLabelProps extends RadioGroupItemProps {
|
|
198
237
|
/**
|
|
199
238
|
* Label content
|
|
200
239
|
*/
|