@moontra/moonui 3.4.0 → 4.1.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 +34 -19
- package/dist/index.d.ts +34 -19
- package/dist/index.global.js +355 -42
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +539 -387
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +505 -354
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.global.js +14 -1
- package/dist/lib/theme.global.js.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__/label.test.tsx +3 -2
- package/src/components/ui/__tests__/select.test.tsx +24 -2
- package/src/components/ui/__tests__/switch.test.tsx +36 -3
- package/src/components/ui/__tests__/tags-input.test.tsx +163 -0
- package/src/components/ui/alert.tsx +5 -2
- package/src/components/ui/aspect-ratio.tsx +9 -3
- package/src/components/ui/avatar.tsx +18 -6
- package/src/components/ui/badge.tsx +18 -11
- package/src/components/ui/button.tsx +3 -0
- package/src/components/ui/date-picker.tsx +38 -19
- package/src/components/ui/file-upload.tsx +4 -0
- package/src/components/ui/index.ts +22 -0
- package/src/components/ui/input-otp.tsx +5 -0
- package/src/components/ui/input.tsx +7 -2
- package/src/components/ui/label.tsx +6 -3
- package/src/components/ui/select.tsx +59 -23
- package/src/components/ui/simple-editor.tsx +18 -3
- package/src/components/ui/switch.tsx +108 -51
- package/src/components/ui/tags-input.tsx +86 -14
- package/src/components/ui/textarea.tsx +8 -4
|
@@ -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
|
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue #272 — TagsInput erişilebilirlik ve klavye gezintisi.
|
|
3
|
+
*
|
|
4
|
+
* Bileşenin hiç testi yoktu. Aşağıdakiler denetimde bulunan üç boşluğu
|
|
5
|
+
* kilitler: X butonunun erişilebilir adı, tag ekleme input'unun erişilebilir
|
|
6
|
+
* adı ve etiketler arası klavye gezintisi (önceden yalnızca Backspace ile SON
|
|
7
|
+
* etiket silinebiliyordu; aradaki bir etiketi klavyeyle silmenin yolu yoktu).
|
|
8
|
+
*/
|
|
9
|
+
import * as React from 'react'
|
|
10
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
11
|
+
import '@testing-library/jest-dom'
|
|
12
|
+
import { TagsInput } from '../tags-input'
|
|
13
|
+
|
|
14
|
+
/** Kontrollü sarmalayıcı — bileşen controlled API'ye sahip. */
|
|
15
|
+
function Harness({
|
|
16
|
+
initial = ['alpha', 'beta', 'gamma'],
|
|
17
|
+
onChangeSpy,
|
|
18
|
+
...rest
|
|
19
|
+
}: {
|
|
20
|
+
initial?: string[]
|
|
21
|
+
onChangeSpy?: jest.Mock
|
|
22
|
+
} & Partial<React.ComponentProps<typeof TagsInput>>) {
|
|
23
|
+
const [tags, setTags] = React.useState<string[]>(initial)
|
|
24
|
+
return (
|
|
25
|
+
<TagsInput
|
|
26
|
+
value={tags}
|
|
27
|
+
onChange={(next) => {
|
|
28
|
+
onChangeSpy?.(next)
|
|
29
|
+
setTags(next)
|
|
30
|
+
}}
|
|
31
|
+
{...rest}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const input = () => screen.getByRole('textbox')
|
|
37
|
+
const removeBtn = (tag: string) => screen.getByRole('button', { name: `Remove ${tag}` })
|
|
38
|
+
|
|
39
|
+
describe('TagsInput — erişilebilir adlar', () => {
|
|
40
|
+
it('tag ekleme input\'u erişilebilir ad taşır', () => {
|
|
41
|
+
render(<Harness />)
|
|
42
|
+
// Etiket eklendikçe placeholder kalkıyor; aria-label olmadan input adsız kalıyordu.
|
|
43
|
+
expect(input()).toHaveAccessibleName('Add tag')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('tüketici kendi aria-label\'ını verebilir', () => {
|
|
47
|
+
render(<Harness aria-label="Etiketler" />)
|
|
48
|
+
expect(input()).toHaveAccessibleName('Etiketler')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('her X butonu hangi etiketi sildiğini duyurur', () => {
|
|
52
|
+
render(<Harness />)
|
|
53
|
+
expect(removeBtn('alpha')).toBeInTheDocument()
|
|
54
|
+
expect(removeBtn('beta')).toBeInTheDocument()
|
|
55
|
+
expect(removeBtn('gamma')).toBeInTheDocument()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('disabled iken silme butonları render edilmez', () => {
|
|
59
|
+
render(<Harness disabled />)
|
|
60
|
+
expect(screen.queryByRole('button', { name: /^Remove / })).not.toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('TagsInput — etiket ekleme/silme', () => {
|
|
65
|
+
it('Enter ile etiket ekler', () => {
|
|
66
|
+
const spy = jest.fn()
|
|
67
|
+
render(<Harness initial={[]} onChangeSpy={spy} />)
|
|
68
|
+
|
|
69
|
+
fireEvent.change(input(), { target: { value: 'yeni' } })
|
|
70
|
+
fireEvent.keyDown(input(), { key: 'Enter' })
|
|
71
|
+
|
|
72
|
+
expect(spy).toHaveBeenCalledWith(['yeni'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('X butonuna tıklayınca doğru etiket silinir', () => {
|
|
76
|
+
const spy = jest.fn()
|
|
77
|
+
render(<Harness onChangeSpy={spy} />)
|
|
78
|
+
|
|
79
|
+
fireEvent.click(removeBtn('beta'))
|
|
80
|
+
expect(spy).toHaveBeenCalledWith(['alpha', 'gamma'])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('boş input\'ta Backspace son etiketi siler', () => {
|
|
84
|
+
const spy = jest.fn()
|
|
85
|
+
render(<Harness onChangeSpy={spy} />)
|
|
86
|
+
|
|
87
|
+
fireEvent.keyDown(input(), { key: 'Backspace' })
|
|
88
|
+
expect(spy).toHaveBeenCalledWith(['alpha', 'beta'])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('yinelenen etiket eklenmez ve hata duyurulur', () => {
|
|
92
|
+
render(<Harness />)
|
|
93
|
+
|
|
94
|
+
fireEvent.change(input(), { target: { value: 'alpha' } })
|
|
95
|
+
fireEvent.keyDown(input(), { key: 'Enter' })
|
|
96
|
+
|
|
97
|
+
const err = screen.getByRole('status')
|
|
98
|
+
expect(err).toHaveTextContent('This tag already exists')
|
|
99
|
+
// Hata input ile ilişkilendirilmeli.
|
|
100
|
+
expect(input()).toHaveAttribute('aria-invalid', 'true')
|
|
101
|
+
expect(input().getAttribute('aria-describedby')).toBe(err.id)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('TagsInput — klavye gezintisi (#272)', () => {
|
|
106
|
+
it('boş input\'ta sol ok son etikete odaklanır', () => {
|
|
107
|
+
render(<Harness />)
|
|
108
|
+
|
|
109
|
+
fireEvent.keyDown(input(), { key: 'ArrowLeft' })
|
|
110
|
+
expect(removeBtn('gamma')).toHaveFocus()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('sol/sağ ok etiketler arasında gezinir', () => {
|
|
114
|
+
render(<Harness />)
|
|
115
|
+
|
|
116
|
+
removeBtn('gamma').focus()
|
|
117
|
+
fireEvent.keyDown(removeBtn('gamma'), { key: 'ArrowLeft' })
|
|
118
|
+
expect(removeBtn('beta')).toHaveFocus()
|
|
119
|
+
|
|
120
|
+
fireEvent.keyDown(removeBtn('beta'), { key: 'ArrowRight' })
|
|
121
|
+
expect(removeBtn('gamma')).toHaveFocus()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('son etiketten sağ ok input\'a döner', () => {
|
|
125
|
+
render(<Harness />)
|
|
126
|
+
|
|
127
|
+
removeBtn('gamma').focus()
|
|
128
|
+
fireEvent.keyDown(removeBtn('gamma'), { key: 'ArrowRight' })
|
|
129
|
+
expect(input()).toHaveFocus()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('Home/End uç etiketlere gider', () => {
|
|
133
|
+
render(<Harness />)
|
|
134
|
+
|
|
135
|
+
removeBtn('beta').focus()
|
|
136
|
+
fireEvent.keyDown(removeBtn('beta'), { key: 'Home' })
|
|
137
|
+
expect(removeBtn('alpha')).toHaveFocus()
|
|
138
|
+
|
|
139
|
+
fireEvent.keyDown(removeBtn('alpha'), { key: 'End' })
|
|
140
|
+
expect(removeBtn('gamma')).toHaveFocus()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('odaklı etikette Delete o etiketi siler (hedefli silme)', () => {
|
|
144
|
+
const spy = jest.fn()
|
|
145
|
+
render(<Harness onChangeSpy={spy} />)
|
|
146
|
+
|
|
147
|
+
removeBtn('beta').focus()
|
|
148
|
+
fireEvent.keyDown(removeBtn('beta'), { key: 'Delete' })
|
|
149
|
+
|
|
150
|
+
// Backspace her zaman SONU siliyordu; ortadaki etiket klavyeyle silinemiyordu.
|
|
151
|
+
expect(spy).toHaveBeenCalledWith(['alpha', 'gamma'])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('odaklı etikette Backspace de siler', () => {
|
|
155
|
+
const spy = jest.fn()
|
|
156
|
+
render(<Harness onChangeSpy={spy} />)
|
|
157
|
+
|
|
158
|
+
removeBtn('alpha').focus()
|
|
159
|
+
fireEvent.keyDown(removeBtn('alpha'), { key: 'Backspace' })
|
|
160
|
+
|
|
161
|
+
expect(spy).toHaveBeenCalledWith(['beta', 'gamma'])
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -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>
|
|
@@ -17,11 +17,15 @@ const aspectRatioVariants = cva(
|
|
|
17
17
|
"relative overflow-hidden",
|
|
18
18
|
{
|
|
19
19
|
variants: {
|
|
20
|
+
// `variant` yalnız yüzey/kenarlık eksenini yönetir. Önceden her varyant
|
|
21
|
+
// ayrıca `rounded-md` taşıyordu ve `radius` ekseniyle çakışıyordu
|
|
22
|
+
// (aynı anda iki `rounded-*` sınıfı üretiliyordu). Köşe yuvarlaması artık
|
|
23
|
+
// TEK kaynaktan: `radius`.
|
|
20
24
|
variant: {
|
|
21
|
-
default: "
|
|
25
|
+
default: "bg-muted/10",
|
|
22
26
|
ghost: "bg-transparent",
|
|
23
|
-
outline: "
|
|
24
|
-
card: "
|
|
27
|
+
outline: "border border-border",
|
|
28
|
+
card: "bg-card shadow-sm",
|
|
25
29
|
},
|
|
26
30
|
radius: {
|
|
27
31
|
none: "rounded-none",
|
|
@@ -33,6 +37,8 @@ const aspectRatioVariants = cva(
|
|
|
33
37
|
},
|
|
34
38
|
defaultVariants: {
|
|
35
39
|
variant: "default",
|
|
40
|
+
// Önceki davranış korunur: her varyant `rounded-md` taşıyordu.
|
|
41
|
+
radius: "md",
|
|
36
42
|
},
|
|
37
43
|
}
|
|
38
44
|
)
|
|
@@ -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 };
|
|
@@ -205,6 +205,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
205
205
|
className={cn("moonui-theme", buttonVariants({ variant, size, rounded, fullWidth, className }))}
|
|
206
206
|
ref={ref}
|
|
207
207
|
disabled={disabled || loading}
|
|
208
|
+
// Görsel spinner + data-loading vardı ama yardımcı teknolojiye meşgul
|
|
209
|
+
// durumu hiç duyurulmuyordu.
|
|
210
|
+
aria-busy={loading || undefined}
|
|
208
211
|
data-loading={loading ? "" : undefined}
|
|
209
212
|
{...props}
|
|
210
213
|
>
|
|
@@ -121,7 +121,12 @@ export interface DatePickerProps
|
|
|
121
121
|
calendarProps?: React.ComponentProps<typeof Calendar>;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
// NOT (issue #264): dört seçici de düz fonksiyondu, ref geçme yolu yoktu.
|
|
125
|
+
// Her biri KENDİ gerçek kök DOM elemanına bağlanır — DOM yapısı değişmez.
|
|
126
|
+
// DatePicker/DateTimePicker kök bir <div> render eder; DateRangePicker ve
|
|
127
|
+
// MonthPicker'ın kökü Radix `Popover`'dır ve kendisi DOM üretmez, bu yüzden
|
|
128
|
+
// onlarda ref tetikleyici butona gider.
|
|
129
|
+
export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(function DatePicker({
|
|
125
130
|
className,
|
|
126
131
|
variant,
|
|
127
132
|
size,
|
|
@@ -143,7 +148,7 @@ export function DatePicker({
|
|
|
143
148
|
calendarProps,
|
|
144
149
|
disabled,
|
|
145
150
|
...props
|
|
146
|
-
}: DatePickerProps) {
|
|
151
|
+
}: DatePickerProps, ref: React.ForwardedRef<HTMLDivElement>) {
|
|
147
152
|
const [open, setOpen] = React.useState(false);
|
|
148
153
|
const [internalDate, setInternalDate] = React.useState<Date | undefined>(value);
|
|
149
154
|
|
|
@@ -163,7 +168,11 @@ export function DatePicker({
|
|
|
163
168
|
}
|
|
164
169
|
};
|
|
165
170
|
|
|
166
|
-
|
|
171
|
+
// Hem fare hem klavye (Enter/Space) yolundan çağrılıyor; yalnızca
|
|
172
|
+
// `stopPropagation` kullanıldığı için ortak taban tipi yeterli.
|
|
173
|
+
// (Önceden `React.MouseEvent` bekliyordu ve klavye yolunda `as any` ile
|
|
174
|
+
// zorlanıyordu.)
|
|
175
|
+
const handleClear = (e: React.SyntheticEvent) => {
|
|
167
176
|
e.stopPropagation();
|
|
168
177
|
handleSelect(undefined);
|
|
169
178
|
};
|
|
@@ -188,7 +197,7 @@ export function DatePicker({
|
|
|
188
197
|
const iconElement = icon || <CalendarIcon className="h-4 w-4" />;
|
|
189
198
|
|
|
190
199
|
return (
|
|
191
|
-
<div className="moonui-theme">
|
|
200
|
+
<div ref={ref} className="moonui-theme">
|
|
192
201
|
<Popover open={open} onOpenChange={setOpen}>
|
|
193
202
|
<PopoverTrigger asChild>
|
|
194
203
|
<Button
|
|
@@ -231,7 +240,7 @@ export function DatePicker({
|
|
|
231
240
|
onKeyDown={(e) => {
|
|
232
241
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
233
242
|
e.preventDefault();
|
|
234
|
-
handleClear(e
|
|
243
|
+
handleClear(e);
|
|
235
244
|
}
|
|
236
245
|
}}
|
|
237
246
|
>
|
|
@@ -255,7 +264,7 @@ export function DatePicker({
|
|
|
255
264
|
</Button>
|
|
256
265
|
</PopoverTrigger>
|
|
257
266
|
<PopoverContent
|
|
258
|
-
className="w-[360px] p-0 shadow-2xl rounded-2xl border border-
|
|
267
|
+
className="w-[360px] p-0 shadow-2xl rounded-2xl border border-border bg-background/95 backdrop-blur-sm !z-[9999]"
|
|
259
268
|
align="start"
|
|
260
269
|
sideOffset={12}
|
|
261
270
|
>
|
|
@@ -289,7 +298,8 @@ export function DatePicker({
|
|
|
289
298
|
</Popover>
|
|
290
299
|
</div>
|
|
291
300
|
);
|
|
292
|
-
}
|
|
301
|
+
});
|
|
302
|
+
DatePicker.displayName = "DatePicker";
|
|
293
303
|
|
|
294
304
|
/**
|
|
295
305
|
* DateRangePicker Component
|
|
@@ -313,7 +323,7 @@ export interface DateRangePickerProps
|
|
|
313
323
|
separator?: string;
|
|
314
324
|
}
|
|
315
325
|
|
|
316
|
-
export function DateRangePicker({
|
|
326
|
+
export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(function DateRangePicker({
|
|
317
327
|
className,
|
|
318
328
|
value,
|
|
319
329
|
onChange,
|
|
@@ -321,7 +331,7 @@ export function DateRangePicker({
|
|
|
321
331
|
formatString = "LLL dd, y",
|
|
322
332
|
separator = " - ",
|
|
323
333
|
...props
|
|
324
|
-
}: DateRangePickerProps) {
|
|
334
|
+
}: DateRangePickerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
|
|
325
335
|
const [open, setOpen] = React.useState(false);
|
|
326
336
|
const [internalRange, setInternalRange] = React.useState(value);
|
|
327
337
|
|
|
@@ -352,6 +362,7 @@ export function DateRangePicker({
|
|
|
352
362
|
<Popover open={open} onOpenChange={setOpen}>
|
|
353
363
|
<PopoverTrigger asChild>
|
|
354
364
|
<Button
|
|
365
|
+
ref={ref}
|
|
355
366
|
variant={props.variant === "ghost" ? "ghost" : "outline"}
|
|
356
367
|
className={cn(
|
|
357
368
|
datePickerVariants({
|
|
@@ -370,7 +381,7 @@ export function DateRangePicker({
|
|
|
370
381
|
</span>
|
|
371
382
|
</Button>
|
|
372
383
|
</PopoverTrigger>
|
|
373
|
-
<PopoverContent className="w-auto p-0 shadow-2xl rounded-2xl border border-
|
|
384
|
+
<PopoverContent className="w-auto p-0 shadow-2xl rounded-2xl border border-border bg-background/95 backdrop-blur-sm !z-[9999]" align="start" sideOffset={12}>
|
|
374
385
|
<Calendar
|
|
375
386
|
mode="range"
|
|
376
387
|
defaultMonth={internalRange?.from}
|
|
@@ -383,7 +394,8 @@ export function DateRangePicker({
|
|
|
383
394
|
</PopoverContent>
|
|
384
395
|
</Popover>
|
|
385
396
|
);
|
|
386
|
-
}
|
|
397
|
+
});
|
|
398
|
+
DateRangePicker.displayName = "DateRangePicker";
|
|
387
399
|
|
|
388
400
|
/**
|
|
389
401
|
* DateTimePicker Component
|
|
@@ -406,7 +418,7 @@ export interface DateTimePickerProps extends DatePickerProps {
|
|
|
406
418
|
timeInterval?: number;
|
|
407
419
|
}
|
|
408
420
|
|
|
409
|
-
export function DateTimePicker({
|
|
421
|
+
export const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(function DateTimePicker({
|
|
410
422
|
value,
|
|
411
423
|
onChange,
|
|
412
424
|
formatString = "PPP p",
|
|
@@ -414,7 +426,7 @@ export function DateTimePicker({
|
|
|
414
426
|
timeFormat = "24",
|
|
415
427
|
timeInterval = 15,
|
|
416
428
|
...props
|
|
417
|
-
}: DateTimePickerProps) {
|
|
429
|
+
}: DateTimePickerProps, ref: React.ForwardedRef<HTMLDivElement>) {
|
|
418
430
|
const [date, setDate] = React.useState<Date | undefined>(value);
|
|
419
431
|
const [time, setTime] = React.useState<string>(
|
|
420
432
|
value ? format(value, timeFormat === "24" ? "HH:mm" : "hh:mm a") : "00:00"
|
|
@@ -469,7 +481,7 @@ export function DateTimePicker({
|
|
|
469
481
|
}, [timeFormat, timeInterval]);
|
|
470
482
|
|
|
471
483
|
return (
|
|
472
|
-
<div className="flex flex-col gap-2">
|
|
484
|
+
<div ref={ref} className="flex flex-col gap-2">
|
|
473
485
|
<DatePicker
|
|
474
486
|
{...props}
|
|
475
487
|
value={date}
|
|
@@ -500,7 +512,8 @@ export function DateTimePicker({
|
|
|
500
512
|
)}
|
|
501
513
|
</div>
|
|
502
514
|
);
|
|
503
|
-
}
|
|
515
|
+
});
|
|
516
|
+
DateTimePicker.displayName = "DateTimePicker";
|
|
504
517
|
|
|
505
518
|
/**
|
|
506
519
|
* MonthPicker Component
|
|
@@ -515,13 +528,13 @@ export interface MonthPickerProps extends Omit<DatePickerProps, "formatString">
|
|
|
515
528
|
formatString?: string;
|
|
516
529
|
}
|
|
517
530
|
|
|
518
|
-
export function MonthPicker({
|
|
531
|
+
export const MonthPicker = React.forwardRef<HTMLButtonElement, MonthPickerProps>(function MonthPicker({
|
|
519
532
|
value,
|
|
520
533
|
onChange,
|
|
521
534
|
placeholder = "Pick a month",
|
|
522
535
|
formatString = "MMMM yyyy",
|
|
523
536
|
...props
|
|
524
|
-
}: MonthPickerProps) {
|
|
537
|
+
}: MonthPickerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
|
|
525
538
|
const [open, setOpen] = React.useState(false);
|
|
526
539
|
const [viewDate, setViewDate] = React.useState(value || new Date());
|
|
527
540
|
|
|
@@ -550,6 +563,7 @@ export function MonthPicker({
|
|
|
550
563
|
<Popover open={open} onOpenChange={setOpen}>
|
|
551
564
|
<PopoverTrigger asChild>
|
|
552
565
|
<Button
|
|
566
|
+
ref={ref}
|
|
553
567
|
variant={props.variant === "default" ? "outline" : (props.variant || "outline")}
|
|
554
568
|
className={cn(
|
|
555
569
|
datePickerVariants({
|
|
@@ -566,12 +580,15 @@ export function MonthPicker({
|
|
|
566
580
|
{displayValue || placeholder}
|
|
567
581
|
</Button>
|
|
568
582
|
</PopoverTrigger>
|
|
569
|
-
<PopoverContent className="w-64 p-0 shadow-2xl border border-
|
|
583
|
+
<PopoverContent className="w-64 p-0 shadow-2xl border border-border bg-background !z-[9999]" align="start">
|
|
570
584
|
<div className="p-3">
|
|
571
585
|
<div className="flex items-center justify-between mb-3">
|
|
586
|
+
{/* İkon-only butonlar: erişilebilir ad olmadan ekran okuyucu
|
|
587
|
+
yalnızca "button" der, hangi yöne gidileceği anlaşılmaz. */}
|
|
572
588
|
<Button
|
|
573
589
|
variant="outline"
|
|
574
590
|
size="icon"
|
|
591
|
+
aria-label={`Previous year, ${viewDate.getFullYear() - 1}`}
|
|
575
592
|
onClick={() => handleYearChange(-1)}
|
|
576
593
|
>
|
|
577
594
|
<ChevronLeft className="h-4 w-4" />
|
|
@@ -582,6 +599,7 @@ export function MonthPicker({
|
|
|
582
599
|
<Button
|
|
583
600
|
variant="outline"
|
|
584
601
|
size="icon"
|
|
602
|
+
aria-label={`Next year, ${viewDate.getFullYear() + 1}`}
|
|
585
603
|
onClick={() => handleYearChange(1)}
|
|
586
604
|
>
|
|
587
605
|
<ChevronRight className="h-4 w-4" />
|
|
@@ -604,7 +622,8 @@ export function MonthPicker({
|
|
|
604
622
|
</PopoverContent>
|
|
605
623
|
</Popover>
|
|
606
624
|
);
|
|
607
|
-
}
|
|
625
|
+
});
|
|
626
|
+
MonthPicker.displayName = "MonthPicker";
|
|
608
627
|
|
|
609
628
|
// Re-export utilities
|
|
610
629
|
export { format } from "date-fns";
|
|
@@ -325,6 +325,10 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
325
325
|
multiple={multiple}
|
|
326
326
|
disabled={disabled}
|
|
327
327
|
onChange={handleFileSelect}
|
|
328
|
+
// Input görsel olarak gizli (opacity-0) ve ilişkili bir <label>'ı
|
|
329
|
+
// yok — erişilebilir ad olmadan ekran okuyucu bunu adsız bir dosya
|
|
330
|
+
// alanı olarak duyuruyordu.
|
|
331
|
+
aria-label={multiple ? "Choose files to upload" : "Choose a file to upload"}
|
|
328
332
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
329
333
|
/>
|
|
330
334
|
|
|
@@ -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
|
|
@@ -256,6 +257,10 @@ export type {
|
|
|
256
257
|
// Label
|
|
257
258
|
export { Label as MoonUILabel } from "./label";
|
|
258
259
|
|
|
260
|
+
export type {
|
|
261
|
+
LabelProps as MoonUILabelProps,
|
|
262
|
+
} from "./label";
|
|
263
|
+
|
|
259
264
|
// LockedComponent
|
|
260
265
|
export {
|
|
261
266
|
LockedComponent as MoonUILockedComponent,
|
|
@@ -340,12 +345,21 @@ export {
|
|
|
340
345
|
SelectScrollDownButton as MoonUISelectScrollDownButton,
|
|
341
346
|
} from "./select";
|
|
342
347
|
|
|
348
|
+
export type {
|
|
349
|
+
SelectTriggerProps as MoonUISelectTriggerProps,
|
|
350
|
+
SelectItemProps as MoonUISelectItemProps,
|
|
351
|
+
} from "./select";
|
|
352
|
+
|
|
343
353
|
// Separator
|
|
344
354
|
export { Separator as MoonUISeparator } from "./separator";
|
|
345
355
|
|
|
346
356
|
// SimpleEditor
|
|
347
357
|
export { SimpleEditor as MoonUISimpleEditor } from "./simple-editor";
|
|
348
358
|
|
|
359
|
+
export type {
|
|
360
|
+
SimpleEditorProps as MoonUISimpleEditorProps,
|
|
361
|
+
} from "./simple-editor";
|
|
362
|
+
|
|
349
363
|
// Skeleton
|
|
350
364
|
export {
|
|
351
365
|
Skeleton as MoonUISkeleton,
|
|
@@ -400,6 +414,14 @@ export {
|
|
|
400
414
|
// TagsInput
|
|
401
415
|
export { TagsInput as MoonUITagsInput } from "./tags-input";
|
|
402
416
|
|
|
417
|
+
export type {
|
|
418
|
+
TagsInputProps as MoonUITagsInputProps,
|
|
419
|
+
} from "./tags-input";
|
|
420
|
+
|
|
421
|
+
export type {
|
|
422
|
+
ToggleProps as MoonUIToggleProps,
|
|
423
|
+
} from "./toggle";
|
|
424
|
+
|
|
403
425
|
// Textarea
|
|
404
426
|
export { Textarea as MoonUITextarea } from "./textarea";
|
|
405
427
|
|
|
@@ -32,6 +32,11 @@ const InputOTP = React.forwardRef<
|
|
|
32
32
|
>(({ className, containerClassName, ...props }, ref) => (
|
|
33
33
|
<OTPInput
|
|
34
34
|
ref={ref}
|
|
35
|
+
// input-otp gerçek girişi görsel olarak gizli bir <input> ile yapar ve o
|
|
36
|
+
// input'un ilişkili bir <label>'ı yok — erişilebilir ad olmadan ekran
|
|
37
|
+
// okuyucu adsız bir metin alanı duyuruyordu. Tüketici kendi aria-label'ını
|
|
38
|
+
// verirse aşağıdaki {...props} yayılımı bunu ezer.
|
|
39
|
+
aria-label="One-time password"
|
|
35
40
|
containerClassName={cn(
|
|
36
41
|
"moonui-theme",
|
|
37
42
|
"flex items-center gap-2 has-[:disabled]:opacity-50",
|