@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui",
3
- "version": "6.14.0",
3
+ "version": "6.15.0",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -287,14 +287,34 @@ describe('Alert Components', () => {
287
287
  })
288
288
 
289
289
  describe('AlertTitle Component', () => {
290
- it('renders correctly with default props', () => {
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('H5')
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<HTMLParagraphElement>()
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
- const AlertTitle = React.forwardRef<
134
- HTMLParagraphElement,
135
- React.HTMLAttributes<HTMLHeadingElement>
136
- >(({ className, ...props }, ref) => (
137
- <h5
138
- ref={ref}
139
- className={cn(
140
- "moonui-theme",
141
- "font-semibold leading-tight tracking-tight mb-1",
142
- className
143
- )}
144
- {...props}
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
- const CardTitle = React.forwardRef<
162
- HTMLHeadingElement,
163
- React.HTMLAttributes<HTMLHeadingElement>
164
- >(({ className, ...props }, ref) => (
165
- <h3
166
- ref={ref}
167
- className={cn("moonui-theme", "text-lg font-semibold leading-none tracking-tight", className)}
168
- {...props}
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
- </Popover>
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
  });