@moontra/moonui 6.14.0 → 6.15.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/hooks/index.global.js +29 -0
- package/dist/hooks/index.global.js.map +1 -1
- package/dist/index.d.mts +38 -4
- package/dist/index.d.ts +38 -4
- package/dist/index.global.js +23 -23
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +98 -104
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +98 -104
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/alert.test.tsx +30 -3
- package/src/components/ui/__tests__/heading-level-policy.test.tsx +71 -0
- package/src/components/ui/alert.tsx +34 -14
- package/src/components/ui/card.tsx +26 -10
- package/src/components/ui/date-picker.tsx +36 -36
package/package.json
CHANGED
|
@@ -287,14 +287,34 @@ describe('Alert Components', () => {
|
|
|
287
287
|
})
|
|
288
288
|
|
|
289
289
|
describe('AlertTitle Component', () => {
|
|
290
|
-
|
|
290
|
+
/**
|
|
291
|
+
* #345: bu assertion eskiden `H5` bekliyordu — yani KUSURU KORUYORDU.
|
|
292
|
+
*
|
|
293
|
+
* Bir component'in belge başlık SEVİYESİNİ dayatması yanlıştır: doğru seviye
|
|
294
|
+
* component'in sayfada nereye konduğuna bağlıdır. Sabit `h5`, docs alert
|
|
295
|
+
* sayfasında H1 → H2 → H5 sıçraması üretiyordu (10 `heading-order` ihlali) ve
|
|
296
|
+
* tüketicinin bunu düzeltmesi imkânsızdı.
|
|
297
|
+
*
|
|
298
|
+
* Varsayılan artık `div`: bir uyarının başlığı belge ana hattına ait değildir,
|
|
299
|
+
* `Alert` zaten `role="alert"` ile duyurulur. Görsel çıktı DEĞİŞMEDİ (aşağıdaki
|
|
300
|
+
* "has correct default styling" testi bunu doğruluyor).
|
|
301
|
+
*/
|
|
302
|
+
it('varsayılanda başlık DEĞİL, düz kap render eder', () => {
|
|
291
303
|
render(<AlertTitle data-testid="alert-title">Alert Title</AlertTitle>)
|
|
292
304
|
const title = screen.getByTestId('alert-title')
|
|
293
305
|
expect(title).toBeInTheDocument()
|
|
294
|
-
expect(title.tagName).toBe('
|
|
306
|
+
expect(title.tagName).toBe('DIV')
|
|
295
307
|
expect(title).toHaveTextContent('Alert Title')
|
|
296
308
|
})
|
|
297
309
|
|
|
310
|
+
it('`as` ile sayfaya uyan başlık seviyesi seçilebilir', () => {
|
|
311
|
+
render(<AlertTitle as="h3" data-testid="alert-title">Alert Title</AlertTitle>)
|
|
312
|
+
const title = screen.getByTestId('alert-title')
|
|
313
|
+
expect(title.tagName).toBe('H3')
|
|
314
|
+
// Görsel sınıflar `as`ten bağımsız korunur.
|
|
315
|
+
expect(title).toHaveClass('font-semibold', 'mb-1')
|
|
316
|
+
})
|
|
317
|
+
|
|
298
318
|
it('applies custom className', () => {
|
|
299
319
|
render(<AlertTitle className="custom-title" data-testid="alert-title">Title</AlertTitle>)
|
|
300
320
|
const title = screen.getByTestId('alert-title')
|
|
@@ -311,8 +331,15 @@ describe('Alert Components', () => {
|
|
|
311
331
|
})
|
|
312
332
|
|
|
313
333
|
it('forwards ref correctly', () => {
|
|
314
|
-
const ref = React.createRef<
|
|
334
|
+
const ref = React.createRef<HTMLElement>()
|
|
315
335
|
render(<AlertTitle ref={ref} data-testid="alert-title">Title</AlertTitle>)
|
|
336
|
+
// Varsayılan artık `div` → HTMLHeadingElement DEĞİL.
|
|
337
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('`as` verildiğinde ref O elemente işaret eder', () => {
|
|
341
|
+
const ref = React.createRef<HTMLElement>()
|
|
342
|
+
render(<AlertTitle as="h3" ref={ref} data-testid="alert-title">Title</AlertTitle>)
|
|
316
343
|
expect(ref.current).toBeInstanceOf(HTMLHeadingElement)
|
|
317
344
|
})
|
|
318
345
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #345 — başlık SEVİYESİ component'in değil, TÜKETİCİNİN kararıdır.
|
|
3
|
+
*
|
|
4
|
+
* Doğru seviye, component'in sayfada nereye konduğuna bağlıdır; bunu yalnız
|
|
5
|
+
* tüketici bilir. Sabit seviye, tüketicinin düzeltemeyeceği `heading-order`
|
|
6
|
+
* sıçramaları üretir — docs alert sayfasında tam olarak bu oldu (10 ihlal).
|
|
7
|
+
*
|
|
8
|
+
* İki component'te İKİ FARKLI karar verildi, çünkü durumları farklı:
|
|
9
|
+
*
|
|
10
|
+
* - `AlertTitle` → varsayılan `div`e ÇEKİLDİ. Bir uyarının başlığı belge ana
|
|
11
|
+
* hattına ait değildir; `Alert` zaten `role="alert"` ile duyurulur ve başlık
|
|
12
|
+
* o duyurunun parçası olarak okunur.
|
|
13
|
+
* - `CardTitle` → varsayılan `h3` KORUNDU. Bir kart çoğu zaman gerçekten bir
|
|
14
|
+
* bölümdür ve `CardTitle` kütüphanenin her yerinde kullanılıyor; varsayılanı
|
|
15
|
+
* çevirmek geniş bir kırılma yüzeyi olurdu. `as` yalnız kapıyı açar.
|
|
16
|
+
*
|
|
17
|
+
* Ortak değişmez: `as` ne olursa olsun GÖRSEL ÇIKTI AYNIDIR.
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react'
|
|
20
|
+
import { render, screen } from '@testing-library/react'
|
|
21
|
+
import '@testing-library/jest-dom'
|
|
22
|
+
|
|
23
|
+
import { AlertTitle } from '../alert'
|
|
24
|
+
import { CardTitle } from '../card'
|
|
25
|
+
|
|
26
|
+
describe('#345 AlertTitle — varsayılan başlık DEĞİL', () => {
|
|
27
|
+
it('varsayılan `div`', () => {
|
|
28
|
+
render(<AlertTitle data-testid="t">Başlık</AlertTitle>)
|
|
29
|
+
expect(screen.getByTestId('t').tagName).toBe('DIV')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('`as` ile başlık seviyesine yükseltilebilir', () => {
|
|
33
|
+
render(<AlertTitle as="h2" data-testid="t">Başlık</AlertTitle>)
|
|
34
|
+
expect(screen.getByTestId('t').tagName).toBe('H2')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('görsel sınıflar `as`ten BAĞIMSIZ', () => {
|
|
38
|
+
const { unmount } = render(<AlertTitle data-testid="t">B</AlertTitle>)
|
|
39
|
+
const varsayilan = screen.getByTestId('t').className
|
|
40
|
+
unmount()
|
|
41
|
+
render(<AlertTitle as="h2" data-testid="t">B</AlertTitle>)
|
|
42
|
+
expect(screen.getByTestId('t').className).toBe(varsayilan)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('#345 CardTitle — varsayılan KORUNDU', () => {
|
|
47
|
+
it('varsayılan hâlâ `h3` (kırıcı değişiklik YOK)', () => {
|
|
48
|
+
// Bu assertion bilinçli: CardTitle'ın varsayılanını çevirmek geniş bir
|
|
49
|
+
// kırılma yüzeyi olurdu. Değişirse bu bilinçli bir karar olmalı.
|
|
50
|
+
render(<CardTitle data-testid="t">Başlık</CardTitle>)
|
|
51
|
+
expect(screen.getByTestId('t').tagName).toBe('H3')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('`as` ile sayfaya uyan seviye seçilebilir', () => {
|
|
55
|
+
render(<CardTitle as="h2" data-testid="t">Başlık</CardTitle>)
|
|
56
|
+
expect(screen.getByTestId('t').tagName).toBe('H2')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('salt-görsel başlık için `as="div"`', () => {
|
|
60
|
+
render(<CardTitle as="div" data-testid="t">Başlık</CardTitle>)
|
|
61
|
+
expect(screen.getByTestId('t').tagName).toBe('DIV')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('görsel sınıflar `as`ten BAĞIMSIZ', () => {
|
|
65
|
+
const { unmount } = render(<CardTitle data-testid="t">B</CardTitle>)
|
|
66
|
+
const varsayilan = screen.getByTestId('t').className
|
|
67
|
+
unmount()
|
|
68
|
+
render(<CardTitle as="div" data-testid="t">B</CardTitle>)
|
|
69
|
+
expect(screen.getByTestId('t').className).toBe(varsayilan)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -130,20 +130,40 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
|
|
130
130
|
);
|
|
131
131
|
Alert.displayName = "Alert";
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
export interface AlertTitleProps
|
|
134
|
+
extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
135
|
+
/**
|
|
136
|
+
* Render edilecek element. Varsayılan `"div"`.
|
|
137
|
+
*
|
|
138
|
+
* #345: Eskiden sabit `<h5>` idi. Bir component'in belge başlık SEVİYESİNİ
|
|
139
|
+
* dayatması yanlıştır — doğru seviye, component'in sayfada nereye konduğuna
|
|
140
|
+
* bağlıdır ve bunu yalnız tüketici bilir. Sabit `h5`, docs alert sayfasında
|
|
141
|
+
* H1 → H2 → H5 sıçraması üretiyordu (10 `heading-order` ihlali) ve tüketici
|
|
142
|
+
* bunu düzeltemiyordu.
|
|
143
|
+
*
|
|
144
|
+
* Varsayılanın `div` olması ayrıca semantik olarak daha doğru: bir uyarının
|
|
145
|
+
* başlığı belge ANA HATTINA ait değildir; `Alert` zaten `role="alert"` ile
|
|
146
|
+
* duyurulur ve başlık o duyurunun parçası olarak okunur.
|
|
147
|
+
*
|
|
148
|
+
* Başlık semantiği gerçekten isteniyorsa sayfaya uyan seviye verilir:
|
|
149
|
+
* `<AlertTitle as="h3">`. Görsel çıktı her iki durumda da AYNIDIR.
|
|
150
|
+
*/
|
|
151
|
+
as?: React.ElementType;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const AlertTitle = React.forwardRef<HTMLHeadingElement, AlertTitleProps>(
|
|
155
|
+
({ className, as: Comp = "div", ...props }, ref) => (
|
|
156
|
+
<Comp
|
|
157
|
+
ref={ref}
|
|
158
|
+
className={cn(
|
|
159
|
+
"moonui-theme",
|
|
160
|
+
"font-semibold leading-tight tracking-tight mb-1",
|
|
161
|
+
className
|
|
162
|
+
)}
|
|
163
|
+
{...props}
|
|
164
|
+
/>
|
|
165
|
+
)
|
|
166
|
+
);
|
|
147
167
|
AlertTitle.displayName = "AlertTitle";
|
|
148
168
|
|
|
149
169
|
const AlertDescription = React.forwardRef<
|
|
@@ -158,16 +158,32 @@ const CardHeader = React.forwardRef<
|
|
|
158
158
|
});
|
|
159
159
|
CardHeader.displayName = "CardHeader";
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
HTMLHeadingElement
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
161
|
+
export interface CardTitleProps
|
|
162
|
+
extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
163
|
+
/**
|
|
164
|
+
* Render edilecek element. Varsayılan `"h3"` — DEĞİŞMEDİ.
|
|
165
|
+
*
|
|
166
|
+
* #345: `AlertTitle`ın aksine burada varsayılan bilerek korundu. Bir kart
|
|
167
|
+
* çoğu zaman gerçekten bir bölümdür ve başlığının belge ana hattında olması
|
|
168
|
+
* doğrudur; ayrıca `CardTitle` kütüphanenin her yerinde kullanılıyor.
|
|
169
|
+
*
|
|
170
|
+
* Ama seviye SABİT olduğu için, kart bir H1'in hemen altına konduğunda
|
|
171
|
+
* `heading-order` sıçraması oluşabiliyor ve tüketici bunu düzeltemiyordu.
|
|
172
|
+
* `as` o kapıyı açar: `<CardTitle as="h2">` ya da salt-görsel bir başlık için
|
|
173
|
+
* `<CardTitle as="div">`. Görsel çıktı her durumda aynıdır.
|
|
174
|
+
*/
|
|
175
|
+
as?: React.ElementType;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
|
|
179
|
+
({ className, as: Comp = "h3", ...props }, ref) => (
|
|
180
|
+
<Comp
|
|
181
|
+
ref={ref}
|
|
182
|
+
className={cn("moonui-theme", "text-lg font-semibold leading-none tracking-tight", className)}
|
|
183
|
+
{...props}
|
|
184
|
+
/>
|
|
185
|
+
)
|
|
186
|
+
);
|
|
171
187
|
CardTitle.displayName = "CardTitle";
|
|
172
188
|
|
|
173
189
|
const CardDescription = React.forwardRef<
|
|
@@ -200,6 +200,13 @@ export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(func
|
|
|
200
200
|
|
|
201
201
|
return (
|
|
202
202
|
<div ref={ref} className="moonui-theme">
|
|
203
|
+
{/* #345: temizle kontrolü tetikleyici <button>ın İÇİNDEydi ve kendisi de
|
|
204
|
+
role="button" + tabIndex taşıyordu — hem `nested-interactive` (ARIA'da bir
|
|
205
|
+
button başka etkileşimli öğe içeremez) hem de geçersiz HTML (interactive
|
|
206
|
+
content model). #332'de tabs-pro kapat butonunda uygulanan desenle KARDEŞ
|
|
207
|
+
konuma alındı ve gerçek <button> oldu: rol taklidi, elle yazılmış Enter/Space
|
|
208
|
+
işleyicisi ve tabIndex artık gereksiz — tarayıcı hepsini kendisi verir. */}
|
|
209
|
+
<div className="relative">
|
|
203
210
|
<Popover open={open} onOpenChange={setOpen}>
|
|
204
211
|
<PopoverTrigger asChild>
|
|
205
212
|
<Button
|
|
@@ -207,6 +214,9 @@ export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(func
|
|
|
207
214
|
className={cn(
|
|
208
215
|
datePickerVariants({ variant, size, state }),
|
|
209
216
|
!displayValue && "text-muted-foreground",
|
|
217
|
+
// #345: temizle butonu artık MUTLAK konumlu bir kardeş — uzun tarih
|
|
218
|
+
// metninin altına girmemesi için sağda yer ayrılıyor.
|
|
219
|
+
allowClear && displayValue && !readOnly && !disabled && "pr-9",
|
|
210
220
|
className
|
|
211
221
|
)}
|
|
212
222
|
disabled={disabled || readOnly}
|
|
@@ -228,41 +238,6 @@ export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(func
|
|
|
228
238
|
{iconPosition === "right" && iconElement}
|
|
229
239
|
</motion.div>
|
|
230
240
|
</AnimatePresence>
|
|
231
|
-
{allowClear && displayValue && !readOnly && !disabled && (
|
|
232
|
-
<motion.div
|
|
233
|
-
initial={{ opacity: 0, scale: 0.8 }}
|
|
234
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
235
|
-
exit={{ opacity: 0, scale: 0.8 }}
|
|
236
|
-
whileHover={{ scale: 1.1 }}
|
|
237
|
-
whileTap={{ scale: 0.9 }}
|
|
238
|
-
className="ml-2 h-4 w-4 shrink-0 opacity-50 hover:opacity-100 transition-opacity cursor-pointer"
|
|
239
|
-
onClick={handleClear}
|
|
240
|
-
role="button"
|
|
241
|
-
tabIndex={0}
|
|
242
|
-
onKeyDown={(e) => {
|
|
243
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
244
|
-
e.preventDefault();
|
|
245
|
-
handleClear(e);
|
|
246
|
-
}
|
|
247
|
-
}}
|
|
248
|
-
>
|
|
249
|
-
<span className="sr-only">Clear date</span>
|
|
250
|
-
<svg
|
|
251
|
-
width="15"
|
|
252
|
-
height="15"
|
|
253
|
-
viewBox="0 0 15 15"
|
|
254
|
-
fill="none"
|
|
255
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
256
|
-
>
|
|
257
|
-
<path
|
|
258
|
-
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
|
259
|
-
fill="currentColor"
|
|
260
|
-
fillRule="evenodd"
|
|
261
|
-
clipRule="evenodd"
|
|
262
|
-
/>
|
|
263
|
-
</svg>
|
|
264
|
-
</motion.div>
|
|
265
|
-
)}
|
|
266
241
|
</Button>
|
|
267
242
|
</PopoverTrigger>
|
|
268
243
|
<PopoverContent
|
|
@@ -297,7 +272,32 @@ export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(func
|
|
|
297
272
|
)}
|
|
298
273
|
</motion.div>
|
|
299
274
|
</PopoverContent>
|
|
300
|
-
|
|
275
|
+
</Popover>
|
|
276
|
+
{allowClear && displayValue && !readOnly && !disabled && (
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
onClick={handleClear}
|
|
280
|
+
aria-label="Clear date"
|
|
281
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 shrink-0 opacity-50 hover:opacity-100 transition-opacity"
|
|
282
|
+
>
|
|
283
|
+
<svg
|
|
284
|
+
width="15"
|
|
285
|
+
height="15"
|
|
286
|
+
viewBox="0 0 15 15"
|
|
287
|
+
fill="none"
|
|
288
|
+
aria-hidden="true"
|
|
289
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
290
|
+
>
|
|
291
|
+
<path
|
|
292
|
+
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
|
293
|
+
fill="currentColor"
|
|
294
|
+
fillRule="evenodd"
|
|
295
|
+
clipRule="evenodd"
|
|
296
|
+
/>
|
|
297
|
+
</svg>
|
|
298
|
+
</button>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
301
|
</div>
|
|
302
302
|
);
|
|
303
303
|
});
|