@fpkit/acss 0.5.11 → 0.5.13
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/README.md +514 -18
- package/libs/chunk-23ANBDCR.js +8 -0
- package/libs/chunk-23ANBDCR.js.map +1 -0
- package/libs/chunk-2LTJ7HHX.cjs +18 -0
- package/libs/chunk-2LTJ7HHX.cjs.map +1 -0
- package/libs/chunk-2Y7W75TT.js +9 -0
- package/libs/chunk-2Y7W75TT.js.map +1 -0
- package/libs/chunk-3MKLDCKQ.cjs +31 -0
- package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
- package/libs/chunk-5M57K4SW.js +8 -0
- package/libs/chunk-5M57K4SW.js.map +1 -0
- package/libs/chunk-5S4ORA4C.cjs +15 -0
- package/libs/chunk-5S4ORA4C.cjs.map +1 -0
- package/libs/chunk-772NRB75.js +9 -0
- package/libs/chunk-772NRB75.js.map +1 -0
- package/libs/chunk-AHDJGCG5.cjs +15 -0
- package/libs/chunk-AHDJGCG5.cjs.map +1 -0
- package/libs/chunk-B7F5FS6D.cjs +16 -0
- package/libs/chunk-B7F5FS6D.cjs.map +1 -0
- package/libs/chunk-BHRQBJRY.js +8 -0
- package/libs/chunk-BHRQBJRY.js.map +1 -0
- package/libs/chunk-D4YLRWAO.cjs +18 -0
- package/libs/chunk-D4YLRWAO.cjs.map +1 -0
- package/libs/chunk-ETFLFC2S.js +10 -0
- package/libs/chunk-ETFLFC2S.js.map +1 -0
- package/libs/chunk-G55UJ53G.cjs +16 -0
- package/libs/chunk-G55UJ53G.cjs.map +1 -0
- package/libs/chunk-GZ4QFPRY.js +9 -0
- package/libs/chunk-GZ4QFPRY.js.map +1 -0
- package/libs/chunk-IYUN2EW3.cjs +15 -0
- package/libs/chunk-IYUN2EW3.cjs.map +1 -0
- package/libs/chunk-J32EZPYD.cjs +15 -0
- package/libs/chunk-J32EZPYD.cjs.map +1 -0
- package/libs/chunk-JJ43O4Y5.js +8 -0
- package/libs/chunk-JJ43O4Y5.js.map +1 -0
- package/libs/chunk-KUKIVRC2.js +7 -0
- package/libs/chunk-KUKIVRC2.js.map +1 -0
- package/libs/chunk-L75OQKEI.cjs +13 -0
- package/libs/chunk-L75OQKEI.cjs.map +1 -0
- package/libs/chunk-LT5KZ2QW.cjs +22 -0
- package/libs/chunk-LT5KZ2QW.cjs.map +1 -0
- package/libs/chunk-M5RRNTVX.cjs +15 -0
- package/libs/chunk-M5RRNTVX.cjs.map +1 -0
- package/libs/chunk-NGTJDDFO.js +8 -0
- package/libs/chunk-NGTJDDFO.js.map +1 -0
- package/libs/chunk-OK5QEIMD.cjs +17 -0
- package/libs/chunk-OK5QEIMD.cjs.map +1 -0
- package/libs/chunk-P2DC76ZZ.cjs +18 -0
- package/libs/chunk-P2DC76ZZ.cjs.map +1 -0
- package/libs/chunk-P7TTEYCD.js +7 -0
- package/libs/chunk-P7TTEYCD.js.map +1 -0
- package/libs/chunk-PQ2K3BM6.cjs +17 -0
- package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
- package/libs/chunk-QLZWHAMK.js +8 -0
- package/libs/chunk-QLZWHAMK.js.map +1 -0
- package/libs/chunk-RIVUMPOG.js +8 -0
- package/libs/chunk-RIVUMPOG.js.map +1 -0
- package/libs/chunk-ROZI23GS.cjs +15 -0
- package/libs/chunk-ROZI23GS.cjs.map +1 -0
- package/libs/chunk-S7BABR7Z.cjs +13 -0
- package/libs/chunk-S7BABR7Z.cjs.map +1 -0
- package/libs/chunk-SMYRLO3E.js +8 -0
- package/libs/chunk-SMYRLO3E.js.map +1 -0
- package/libs/chunk-TYRCEX2L.js +8 -0
- package/libs/chunk-TYRCEX2L.js.map +1 -0
- package/libs/chunk-VUH3FXGJ.js +11 -0
- package/libs/chunk-VUH3FXGJ.js.map +1 -0
- package/libs/chunk-XBA562WW.js +8 -0
- package/libs/chunk-XBA562WW.js.map +1 -0
- package/libs/chunk-XTQKWY7W.cjs +32 -0
- package/libs/chunk-XTQKWY7W.cjs.map +1 -0
- package/libs/chunk-ZANSFMTD.js +9 -0
- package/libs/chunk-ZANSFMTD.js.map +1 -0
- package/libs/component-props-a8a2f97e.d.ts +38 -0
- package/libs/components/alert/alert.css +1 -1
- package/libs/components/alert/alert.css.map +1 -1
- package/libs/components/alert/alert.min.css +2 -2
- package/libs/components/badge/badge.css +1 -1
- package/libs/components/badge/badge.css.map +1 -1
- package/libs/components/badge/badge.min.css +2 -2
- package/libs/components/breadcrumbs/breadcrumb.cjs +24 -0
- package/libs/components/breadcrumbs/breadcrumb.cjs.map +1 -0
- package/libs/components/breadcrumbs/breadcrumb.d.cts +290 -0
- package/libs/components/breadcrumbs/breadcrumb.d.ts +290 -0
- package/libs/components/breadcrumbs/breadcrumb.js +5 -0
- package/libs/components/breadcrumbs/breadcrumb.js.map +1 -0
- package/libs/components/button.cjs +19 -0
- package/libs/components/button.cjs.map +1 -0
- package/libs/components/button.d.cts +16 -0
- package/libs/components/button.d.ts +16 -0
- package/libs/components/button.js +4 -0
- package/libs/components/button.js.map +1 -0
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/card.cjs +31 -0
- package/libs/components/card.cjs.map +1 -0
- package/libs/components/card.d.cts +302 -0
- package/libs/components/card.d.ts +302 -0
- package/libs/components/card.js +4 -0
- package/libs/components/card.js.map +1 -0
- package/libs/components/cards/card.css +1 -1
- package/libs/components/cards/card.css.map +1 -1
- package/libs/components/cards/card.min.css +2 -2
- package/libs/components/details/details.css +1 -1
- package/libs/components/details/details.css.map +1 -1
- package/libs/components/details/details.min.css +2 -2
- package/libs/components/dialog/dialog.cjs +22 -0
- package/libs/components/dialog/dialog.cjs.map +1 -0
- package/libs/components/dialog/dialog.css +1 -1
- package/libs/components/dialog/dialog.css.map +1 -1
- package/libs/components/dialog/dialog.d.cts +105 -0
- package/libs/components/dialog/dialog.d.ts +105 -0
- package/libs/components/dialog/dialog.js +7 -0
- package/libs/components/dialog/dialog.js.map +1 -0
- package/libs/components/dialog/dialog.min.css +2 -2
- package/libs/components/form/fields.cjs +19 -0
- package/libs/components/form/fields.cjs.map +1 -0
- package/libs/components/form/fields.d.cts +24 -0
- package/libs/components/form/fields.d.ts +24 -0
- package/libs/components/form/fields.js +4 -0
- package/libs/components/form/fields.js.map +1 -0
- package/libs/components/form/inputs.cjs +19 -0
- package/libs/components/form/inputs.cjs.map +1 -0
- package/libs/components/form/inputs.d.cts +2 -0
- package/libs/components/form/inputs.d.ts +2 -0
- package/libs/components/form/inputs.js +4 -0
- package/libs/components/form/inputs.js.map +1 -0
- package/libs/components/form/textarea.cjs +19 -0
- package/libs/components/form/textarea.cjs.map +1 -0
- package/libs/components/form/textarea.d.cts +29 -0
- package/libs/components/form/textarea.d.ts +29 -0
- package/libs/components/form/textarea.js +4 -0
- package/libs/components/form/textarea.js.map +1 -0
- package/libs/components/heading/heading.cjs +10 -0
- package/libs/components/heading/heading.cjs.map +1 -0
- package/libs/components/heading/heading.d.cts +3 -0
- package/libs/components/heading/heading.d.ts +3 -0
- package/libs/components/heading/heading.js +4 -0
- package/libs/components/heading/heading.js.map +1 -0
- package/libs/components/icons/icon.cjs +19 -0
- package/libs/components/icons/icon.cjs.map +1 -0
- package/libs/{icons-31ace3de.d.ts → components/icons/icon.d.cts} +151 -61
- package/libs/components/icons/icon.d.ts +445 -0
- package/libs/components/icons/icon.js +4 -0
- package/libs/components/icons/icon.js.map +1 -0
- package/libs/components/images/img.css +1 -1
- package/libs/components/images/img.css.map +1 -1
- package/libs/components/images/img.min.css +2 -2
- package/libs/components/link/link.cjs +19 -0
- package/libs/components/link/link.cjs.map +1 -0
- package/libs/components/link/link.d.cts +19 -0
- package/libs/components/link/link.d.ts +19 -0
- package/libs/components/link/link.js +4 -0
- package/libs/components/link/link.js.map +1 -0
- package/libs/components/list/list.cjs +23 -0
- package/libs/components/list/list.cjs.map +1 -0
- package/libs/components/list/list.d.cts +39 -0
- package/libs/components/list/list.d.ts +39 -0
- package/libs/components/list/list.js +4 -0
- package/libs/components/list/list.js.map +1 -0
- package/libs/components/modal.cjs +14 -0
- package/libs/components/modal.cjs.map +1 -0
- package/libs/components/modal.d.cts +35 -0
- package/libs/components/modal.d.ts +35 -0
- package/libs/components/modal.js +5 -0
- package/libs/components/modal.js.map +1 -0
- package/libs/components/nav/nav.cjs +28 -0
- package/libs/components/nav/nav.cjs.map +1 -0
- package/libs/components/nav/nav.d.cts +44 -0
- package/libs/components/nav/nav.d.ts +44 -0
- package/libs/components/nav/nav.js +5 -0
- package/libs/components/nav/nav.js.map +1 -0
- package/libs/components/popover/popover.cjs +23 -0
- package/libs/components/popover/popover.cjs.map +1 -0
- package/libs/components/popover/popover.d.cts +40 -0
- package/libs/components/popover/popover.d.ts +40 -0
- package/libs/components/popover/popover.js +4 -0
- package/libs/components/popover/popover.js.map +1 -0
- package/libs/components/tables/table.cjs +21 -0
- package/libs/components/tables/table.cjs.map +1 -0
- package/libs/components/tables/table.d.cts +36 -0
- package/libs/components/tables/table.d.ts +36 -0
- package/libs/components/tables/table.js +4 -0
- package/libs/components/tables/table.js.map +1 -0
- package/libs/components/text/text.cjs +23 -0
- package/libs/components/text/text.cjs.map +1 -0
- package/libs/components/text/text.d.cts +30 -0
- package/libs/components/text/text.d.ts +30 -0
- package/libs/components/text/text.js +4 -0
- package/libs/components/text/text.js.map +1 -0
- package/libs/heading-3648c538.d.ts +250 -0
- package/libs/hooks.cjs +7 -0
- package/libs/hooks.d.cts +5 -0
- package/libs/hooks.d.ts +5 -0
- package/libs/hooks.js +3 -0
- package/libs/icons.cjs +3 -2
- package/libs/icons.d.cts +3 -1
- package/libs/icons.d.ts +3 -1
- package/libs/icons.js +2 -1
- package/libs/index.cjs +174 -62
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +529 -446
- package/libs/index.d.ts +529 -446
- package/libs/index.js +36 -7
- package/libs/index.js.map +1 -1
- package/libs/inputs-f3a216db.d.ts +45 -0
- package/libs/ui-645f95b5.d.ts +285 -0
- package/package.json +2 -2
- package/src/components/README-UI.mdx +416 -0
- package/src/components/alert/ACCESSIBILITY.md +319 -0
- package/src/components/alert/README.mdx +475 -19
- package/src/components/alert/alert.scss +113 -6
- package/src/components/alert/alert.stories.tsx +372 -0
- package/src/components/alert/alert.test.tsx +762 -0
- package/src/components/alert/alert.tsx +331 -66
- package/src/components/alert/views/alert-actions.tsx +13 -0
- package/src/components/alert/views/alert-content.tsx +17 -0
- package/src/components/alert/views/alert-icon.tsx +53 -0
- package/src/components/alert/views/alert-screen-reader-text.tsx +30 -0
- package/src/components/alert/views/alert-title.tsx +23 -0
- package/src/components/alert/views/alert-view.tsx +158 -0
- package/src/components/alert/views/index.ts +12 -0
- package/src/components/badge/badge.mdx +186 -49
- package/src/components/badge/badge.scss +20 -2
- package/src/components/badge/badge.stories.tsx +160 -14
- package/src/components/badge/badge.test.tsx +179 -0
- package/src/components/badge/badge.tsx +97 -4
- package/src/components/breadcrumbs/README.mdx +364 -45
- package/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +152 -0
- package/src/components/breadcrumbs/breadcrumb.stories.tsx +7 -3
- package/src/components/breadcrumbs/breadcrumb.test.tsx +490 -0
- package/src/components/breadcrumbs/breadcrumb.tsx +427 -170
- package/src/components/button.ts +2 -0
- package/src/components/buttons/button.scss +34 -31
- package/src/components/buttons/button.stories.tsx +35 -0
- package/src/components/card.ts +2 -0
- package/src/components/cards/README.mdx +657 -0
- package/src/components/cards/card.scss +22 -0
- package/src/components/cards/card.stories.tsx +167 -5
- package/src/components/cards/card.test.tsx +360 -20
- package/src/components/cards/card.tsx +200 -79
- package/src/components/cards/card.types.ts +135 -0
- package/src/components/cards/card.utils.ts +79 -0
- package/src/components/details/ACCESSIBILITY-REVIEW-LIVE.md +1050 -0
- package/src/components/details/ACCESSIBILITY-REVIEW.md +502 -0
- package/src/components/details/README.mdx +437 -69
- package/src/components/details/details.scss +16 -0
- package/src/components/details/details.test.tsx +385 -0
- package/src/components/details/details.tsx +101 -69
- package/src/components/details/details.types.ts +76 -0
- package/src/components/dialog/README.mdx +513 -110
- package/src/components/dialog/dialog-modal.tsx +79 -56
- package/src/components/dialog/dialog.scss +53 -3
- package/src/components/dialog/dialog.stories.tsx +10 -7
- package/src/components/dialog/dialog.test.tsx +450 -0
- package/src/components/dialog/dialog.tsx +69 -59
- package/src/components/dialog/dialog.types.ts +133 -0
- package/src/components/dialog/views/dialog-footer.tsx +54 -11
- package/src/components/dialog/views/dialog-header.tsx +20 -15
- package/src/components/heading/heading.stories.tsx +44 -4
- package/src/components/heading/heading.tsx +89 -23
- package/src/components/icons/README.mdx +332 -0
- package/src/components/icons/icon.stories.tsx +74 -1
- package/src/components/icons/icon.tsx +89 -1
- package/src/components/icons/types.ts +47 -0
- package/src/components/images/README.mdx +340 -24
- package/src/components/images/img.scss +19 -3
- package/src/components/images/img.stories.tsx +424 -15
- package/src/components/images/img.test.tsx +354 -25
- package/src/components/images/img.tsx +186 -63
- package/src/components/images/img.types.ts +211 -0
- package/src/components/modal.ts +1 -0
- package/src/components/title/MIGRATION.md +199 -0
- package/src/components/title/README.md +326 -0
- package/src/components/title/README.mdx +452 -0
- package/src/components/title/title.stories.tsx +393 -0
- package/src/components/title/title.test.tsx +251 -0
- package/src/components/title/title.tsx +219 -0
- package/src/components/ui.stories.tsx +894 -0
- package/src/components/ui.test.tsx +559 -0
- package/src/components/ui.tsx +266 -15
- package/src/components/word-count/README.md +240 -0
- package/src/hooks.ts +1 -0
- package/src/index.ts +51 -19
- package/src/sass/_properties.scss +1 -0
- package/src/styles/alert/alert.css +94 -4
- package/src/styles/alert/alert.css.map +1 -1
- package/src/styles/badge/badge.css +20 -2
- package/src/styles/badge/badge.css.map +1 -1
- package/src/styles/buttons/button.css +31 -31
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/cards/card.css +16 -0
- package/src/styles/cards/card.css.map +1 -1
- package/src/styles/details/details.css +19 -0
- package/src/styles/details/details.css.map +1 -1
- package/src/styles/dialog/dialog.css +43 -2
- package/src/styles/dialog/dialog.css.map +1 -1
- package/src/styles/images/img.css +15 -3
- package/src/styles/images/img.css.map +1 -1
- package/src/styles/index.css +240 -43
- package/src/styles/index.css.map +1 -1
- package/src/test/setup.d.ts +9 -0
- package/src/test/setup.ts +53 -1
- package/libs/chunk-PWVRDQ3R.js +0 -8
- package/libs/chunk-PWVRDQ3R.js.map +0 -1
- package/libs/chunk-SVS4MX3U.cjs +0 -31
- package/libs/chunk-SVS4MX3U.cjs.map +0 -1
- package/src/components/cards/README.md +0 -80
- package/src/components/dialog/hooks/useClickOutside.ts +0 -33
|
@@ -1,43 +1,372 @@
|
|
|
1
1
|
import { Img } from './img'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
4
|
-
import
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
5
|
+
|
|
5
6
|
describe('Img', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
describe('Basic Rendering', () => {
|
|
8
|
+
it('should render an img element with passed props', () => {
|
|
9
|
+
const src = 'test.jpg'
|
|
10
|
+
const alt = 'Test image'
|
|
11
|
+
const width = 100
|
|
12
|
+
|
|
13
|
+
render(<Img src={src} alt={alt} width={width} />)
|
|
14
|
+
|
|
15
|
+
const img = screen.getByRole('img')
|
|
16
|
+
expect(img).toBeInTheDocument()
|
|
17
|
+
expect(img).toHaveAttribute('src', src)
|
|
18
|
+
expect(img).toHaveAttribute('alt', alt)
|
|
19
|
+
expect(img).toHaveAttribute('width', width.toString())
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should render with default props when none provided', () => {
|
|
23
|
+
render(<Img />)
|
|
24
|
+
|
|
25
|
+
const img = screen.getByRole('img')
|
|
26
|
+
expect(img).toBeInTheDocument()
|
|
27
|
+
expect(img).toHaveAttribute('src', '//')
|
|
28
|
+
expect(img).toHaveAttribute('width', '480')
|
|
29
|
+
expect(img).toHaveAttribute('loading', 'lazy')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should apply custom width and height', () => {
|
|
33
|
+
render(<Img src="test.jpg" alt="Test" width={200} height={150} />)
|
|
34
|
+
|
|
35
|
+
const img = screen.getByRole('img')
|
|
36
|
+
expect(img).toHaveAttribute('width', '200')
|
|
37
|
+
expect(img).toHaveAttribute('height', '150')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('Accessibility', () => {
|
|
42
|
+
it('should support decorative images with empty alt', () => {
|
|
43
|
+
render(<Img src="decorative.png" alt="" />)
|
|
44
|
+
|
|
45
|
+
const img = screen.getByRole('img', { hidden: true })
|
|
46
|
+
expect(img).toHaveAttribute('alt', '')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should support semantic images with descriptive alt text', () => {
|
|
50
|
+
const altText = 'Sales chart showing 30% growth in Q4'
|
|
51
|
+
render(<Img src="chart.png" alt={altText} />)
|
|
52
|
+
|
|
53
|
+
const img = screen.getByRole('img')
|
|
54
|
+
expect(img).toHaveAccessibleName(altText)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should allow missing alt attribute', () => {
|
|
58
|
+
render(<Img src="test.jpg" />)
|
|
59
|
+
|
|
60
|
+
const img = screen.getByRole('img')
|
|
61
|
+
expect(img).toBeInTheDocument()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('Responsive Images', () => {
|
|
66
|
+
it('should support srcSet attribute', () => {
|
|
67
|
+
const srcSet = 'image-320w.jpg 320w, image-640w.jpg 640w'
|
|
68
|
+
render(<Img src="image.jpg" srcSet={srcSet} alt="Responsive image" />)
|
|
69
|
+
|
|
70
|
+
const img = screen.getByRole('img')
|
|
71
|
+
expect(img).toHaveAttribute('srcset', srcSet)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should support sizes attribute', () => {
|
|
75
|
+
const sizes = '(max-width: 640px) 100vw, 640px'
|
|
76
|
+
render(
|
|
77
|
+
<Img
|
|
78
|
+
src="image.jpg"
|
|
79
|
+
srcSet="image-320w.jpg 320w, image-640w.jpg 640w"
|
|
80
|
+
sizes={sizes}
|
|
81
|
+
alt="Responsive image"
|
|
82
|
+
/>,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const img = screen.getByRole('img')
|
|
86
|
+
expect(img).toHaveAttribute('sizes', sizes)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should work without srcSet and sizes', () => {
|
|
90
|
+
render(<Img src="image.jpg" alt="Regular image" />)
|
|
91
|
+
|
|
92
|
+
const img = screen.getByRole('img')
|
|
93
|
+
expect(img).not.toHaveAttribute('srcset')
|
|
94
|
+
expect(img).not.toHaveAttribute('sizes')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('Loading and Performance', () => {
|
|
99
|
+
it('should default to lazy loading', () => {
|
|
100
|
+
render(<Img src="test.jpg" alt="Test" />)
|
|
101
|
+
|
|
102
|
+
const img = screen.getByRole('img')
|
|
103
|
+
expect(img).toHaveAttribute('loading', 'lazy')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should support eager loading', () => {
|
|
107
|
+
render(<Img src="hero.jpg" alt="Hero" loading="eager" />)
|
|
108
|
+
|
|
109
|
+
const img = screen.getByRole('img')
|
|
110
|
+
expect(img).toHaveAttribute('loading', 'eager')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should support fetchpriority attribute', () => {
|
|
114
|
+
render(
|
|
115
|
+
<Img src="hero.jpg" alt="Hero" fetchpriority="high" loading="eager" />,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const img = screen.getByRole('img')
|
|
119
|
+
expect(img).toHaveAttribute('fetchpriority', 'high')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should support decoding attribute', () => {
|
|
123
|
+
render(<Img src="test.jpg" alt="Test" decoding="async" />)
|
|
124
|
+
|
|
125
|
+
const img = screen.getByRole('img')
|
|
126
|
+
expect(img).toHaveAttribute('decoding', 'async')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('Error Handling', () => {
|
|
131
|
+
it('should call onError callback and apply fallback placeholder', () => {
|
|
132
|
+
const onError = vi.fn()
|
|
133
|
+
render(<Img src="bad.jpg" alt="Test" onError={onError} />)
|
|
134
|
+
|
|
135
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
136
|
+
fireEvent.error(img)
|
|
137
|
+
|
|
138
|
+
// Should call custom handler
|
|
139
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
140
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Object))
|
|
141
|
+
|
|
142
|
+
// Should still apply default fallback
|
|
143
|
+
expect(img.src).toContain('data:image/svg+xml')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should prevent default fallback when preventDefault is called', () => {
|
|
147
|
+
const customSrc = 'https://custom-fallback.jpg'
|
|
148
|
+
const onError = vi.fn((e) => {
|
|
149
|
+
e.preventDefault()
|
|
150
|
+
e.currentTarget.src = customSrc
|
|
151
|
+
})
|
|
152
|
+
render(<Img src="bad.jpg" alt="Test" onError={onError} />)
|
|
153
|
+
|
|
154
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
155
|
+
fireEvent.error(img)
|
|
156
|
+
|
|
157
|
+
// Should call custom handler
|
|
158
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
159
|
+
|
|
160
|
+
// Should use custom fallback, not default SVG
|
|
161
|
+
expect(img.src).toContain(customSrc)
|
|
162
|
+
expect(img.src).not.toContain('data:image/svg+xml')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('should fallback to placeholder when error occurs and no onError handler', () => {
|
|
166
|
+
render(<Img src="bad.jpg" alt="Test" placeholder="/fallback.png" />)
|
|
167
|
+
|
|
168
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
169
|
+
fireEvent.error(img)
|
|
10
170
|
|
|
11
|
-
|
|
171
|
+
expect(img.src).toContain('fallback.png')
|
|
172
|
+
})
|
|
12
173
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
174
|
+
it('should use default SVG placeholder when none provided', () => {
|
|
175
|
+
render(<Img src="bad.jpg" alt="Test" width={300} />)
|
|
176
|
+
|
|
177
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
178
|
+
fireEvent.error(img)
|
|
179
|
+
|
|
180
|
+
// Should use SVG data URI placeholder
|
|
181
|
+
expect(img.src).toContain('data:image/svg+xml')
|
|
182
|
+
expect(img.src).toContain('300') // Width in dimension text
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should not enter infinite loop when placeholder also fails', () => {
|
|
186
|
+
render(<Img src="bad.jpg" alt="Test" placeholder="/also-bad.png" />)
|
|
187
|
+
|
|
188
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
189
|
+
|
|
190
|
+
// First error: switches to placeholder
|
|
191
|
+
fireEvent.error(img)
|
|
192
|
+
const firstSrc = img.src
|
|
193
|
+
|
|
194
|
+
// Second error: should not change src again
|
|
195
|
+
fireEvent.error(img)
|
|
196
|
+
expect(img.src).toBe(firstSrc)
|
|
197
|
+
})
|
|
18
198
|
})
|
|
19
199
|
|
|
20
|
-
|
|
21
|
-
|
|
200
|
+
describe('Event Handlers', () => {
|
|
201
|
+
it('should call onLoad callback when image loads', () => {
|
|
202
|
+
const onLoad = vi.fn()
|
|
203
|
+
render(<Img src="good.jpg" alt="Test" onLoad={onLoad} />)
|
|
204
|
+
|
|
205
|
+
const img = screen.getByRole('img')
|
|
206
|
+
fireEvent.load(img)
|
|
207
|
+
|
|
208
|
+
expect(onLoad).toHaveBeenCalledTimes(1)
|
|
209
|
+
expect(onLoad).toHaveBeenCalledWith(expect.any(Object))
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should work without onLoad callback', () => {
|
|
213
|
+
render(<Img src="test.jpg" alt="Test" />)
|
|
214
|
+
|
|
215
|
+
const img = screen.getByRole('img')
|
|
216
|
+
expect(() => fireEvent.load(img)).not.toThrow()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should work without onError callback', () => {
|
|
220
|
+
render(<Img src="test.jpg" alt="Test" />)
|
|
221
|
+
|
|
222
|
+
const img = screen.getByRole('img')
|
|
223
|
+
expect(() => fireEvent.error(img)).not.toThrow()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('Styling', () => {
|
|
228
|
+
it('should apply inline styles', () => {
|
|
229
|
+
const styles = { border: '1px solid red', borderRadius: '0.5rem' }
|
|
230
|
+
render(<Img src="test.jpg" alt="Test" styles={styles} />)
|
|
231
|
+
|
|
232
|
+
const img = screen.getByRole('img')
|
|
233
|
+
expect(img).toHaveStyle({ border: '1px solid red' })
|
|
234
|
+
expect(img).toHaveStyle({ borderRadius: '0.5rem' })
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('should forward additional props', () => {
|
|
238
|
+
render(
|
|
239
|
+
<Img
|
|
240
|
+
src="test.jpg"
|
|
241
|
+
alt="Test"
|
|
242
|
+
data-testid="custom-img"
|
|
243
|
+
className="custom-class"
|
|
244
|
+
/>,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const img = screen.getByTestId('custom-img')
|
|
248
|
+
expect(img).toHaveClass('custom-class')
|
|
249
|
+
})
|
|
22
250
|
})
|
|
23
251
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
252
|
+
describe('Edge Cases', () => {
|
|
253
|
+
it('should handle string width values', () => {
|
|
254
|
+
render(<Img src="test.jpg" alt="Test" width="100%" />)
|
|
255
|
+
|
|
256
|
+
const img = screen.getByRole('img')
|
|
257
|
+
expect(img).toHaveAttribute('width', '100%')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should default height to auto when not provided', () => {
|
|
261
|
+
render(<Img src="test.jpg" alt="Test" />)
|
|
27
262
|
|
|
28
|
-
|
|
29
|
-
|
|
263
|
+
const img = screen.getByRole('img')
|
|
264
|
+
expect(img).toHaveAttribute('height', 'auto')
|
|
265
|
+
})
|
|
30
266
|
|
|
31
|
-
|
|
267
|
+
it('should accept custom height', () => {
|
|
268
|
+
render(<Img src="test.jpg" alt="Test" height={200} />)
|
|
269
|
+
|
|
270
|
+
const img = screen.getByRole('img')
|
|
271
|
+
expect(img).toHaveAttribute('height', '200')
|
|
272
|
+
})
|
|
32
273
|
})
|
|
33
274
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
275
|
+
describe('SVG Placeholder', () => {
|
|
276
|
+
it('should generate SVG placeholder with correct dimensions', () => {
|
|
277
|
+
render(<Img src="bad.jpg" alt="Test" width={800} height={600} />)
|
|
278
|
+
|
|
279
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
280
|
+
fireEvent.error(img)
|
|
281
|
+
|
|
282
|
+
// Should be SVG data URI
|
|
283
|
+
expect(img.src).toContain('data:image/svg+xml')
|
|
284
|
+
// Should contain both dimensions in the text
|
|
285
|
+
expect(img.src).toContain('800')
|
|
286
|
+
expect(img.src).toContain('600')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('should generate SVG with 4:3 aspect ratio when height not provided', () => {
|
|
290
|
+
render(<Img src="bad.jpg" alt="Test" width={400} />)
|
|
291
|
+
|
|
292
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
293
|
+
fireEvent.error(img)
|
|
294
|
+
|
|
295
|
+
expect(img.src).toContain('data:image/svg+xml')
|
|
296
|
+
// 400 * 0.75 = 300
|
|
297
|
+
expect(img.src).toContain('400')
|
|
298
|
+
expect(img.src).toContain('300')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should generate SVG with gradient elements', () => {
|
|
302
|
+
render(<Img src="bad.jpg" alt="Test" width={500} />)
|
|
303
|
+
|
|
304
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
305
|
+
fireEvent.error(img)
|
|
306
|
+
|
|
307
|
+
const decodedSvg = decodeURIComponent(img.src.replace('data:image/svg+xml,', ''))
|
|
308
|
+
|
|
309
|
+
// Should contain gradient definition
|
|
310
|
+
expect(decodedSvg).toContain('linearGradient')
|
|
311
|
+
expect(decodedSvg).toContain('#6366f1') // Indigo
|
|
312
|
+
expect(decodedSvg).toContain('#8b5cf6') // Purple
|
|
313
|
+
expect(decodedSvg).toContain('#ec4899') // Pink
|
|
314
|
+
|
|
315
|
+
// Should contain decorative elements
|
|
316
|
+
expect(decodedSvg).toContain('circle') // Sun
|
|
317
|
+
expect(decodedSvg).toContain('path') // Mountain wave
|
|
318
|
+
expect(decodedSvg).toContain('text') // Dimension text
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('should generate unique gradient IDs for different dimensions', () => {
|
|
322
|
+
const { rerender } = render(<Img src="bad1.jpg" alt="Test" width={400} height={300} />)
|
|
323
|
+
const img1 = screen.getByRole('img') as HTMLImageElement
|
|
324
|
+
fireEvent.error(img1)
|
|
325
|
+
const svg1 = decodeURIComponent(img1.src.replace('data:image/svg+xml,', ''))
|
|
326
|
+
|
|
327
|
+
rerender(<Img src="bad2.jpg" alt="Test" width={800} height={600} />)
|
|
328
|
+
const img2 = screen.getByRole('img') as HTMLImageElement
|
|
329
|
+
fireEvent.error(img2)
|
|
330
|
+
const svg2 = decodeURIComponent(img2.src.replace('data:image/svg+xml,', ''))
|
|
331
|
+
|
|
332
|
+
// Different gradient IDs to prevent conflicts
|
|
333
|
+
expect(svg1).toContain('grad-400-300')
|
|
334
|
+
expect(svg2).toContain('grad-800-600')
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('should handle string width by defaulting to 480', () => {
|
|
338
|
+
render(<Img src="bad.jpg" alt="Test" width="100%" />)
|
|
339
|
+
|
|
340
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
341
|
+
fireEvent.error(img)
|
|
342
|
+
|
|
343
|
+
expect(img.src).toContain('data:image/svg+xml')
|
|
344
|
+
// Should default to 480 when width is string
|
|
345
|
+
expect(img.src).toContain('480')
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('should be smaller than external placeholder', () => {
|
|
349
|
+
render(<Img src="bad.jpg" alt="Test" width={800} />)
|
|
350
|
+
|
|
351
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
352
|
+
fireEvent.error(img)
|
|
353
|
+
|
|
354
|
+
// SVG data URI should be relatively small (< 2KB)
|
|
355
|
+
expect(img.src.length).toBeLessThan(2000)
|
|
356
|
+
// Should start with data URI scheme
|
|
357
|
+
expect(img.src).toMatch(/^data:image\/svg\+xml,/)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should contain viewBox for responsiveness', () => {
|
|
361
|
+
render(<Img src="bad.jpg" alt="Test" width={600} height={400} />)
|
|
362
|
+
|
|
363
|
+
const img = screen.getByRole('img') as HTMLImageElement
|
|
364
|
+
fireEvent.error(img)
|
|
37
365
|
|
|
38
|
-
|
|
39
|
-
fireEvent.load(img)
|
|
366
|
+
const decodedSvg = decodeURIComponent(img.src.replace('data:image/svg+xml,', ''))
|
|
40
367
|
|
|
41
|
-
|
|
368
|
+
// Should have viewBox for perfect scaling
|
|
369
|
+
expect(decodedSvg).toContain('viewBox="0 0 600 400"')
|
|
370
|
+
})
|
|
42
371
|
})
|
|
43
372
|
})
|
|
@@ -1,75 +1,196 @@
|
|
|
1
|
-
import UI from
|
|
2
|
-
import React from
|
|
3
|
-
|
|
4
|
-
* ImageProps interface
|
|
5
|
-
*
|
|
6
|
-
* Extends ComponentProps and defines additional props for the Img component.
|
|
7
|
-
*
|
|
8
|
-
* @property {string} [src] - The image source URL
|
|
9
|
-
* @property {string} alt - Required alt text for image accessibility
|
|
10
|
-
* @property {number} width - Required width of image
|
|
11
|
-
* @property {number} [height] - Optional height of image
|
|
12
|
-
* @property {"eager" | "lazy"} [loading="lazy"] - Loading behavior
|
|
13
|
-
* @property {string} [placeholder] - Fallback placeholder image
|
|
14
|
-
* @property {"high" | "low"} [fetchpriority="low"] - Image fetch priority
|
|
15
|
-
* @property {"sync" | "async" | "auto"} [decoding="auto"] - Decode setting
|
|
16
|
-
* @property {function} [imgError] - Error callback
|
|
17
|
-
* @property {function} [imgLoaded] - Loaded callback
|
|
18
|
-
*/
|
|
19
|
-
export type ImageProps = React.ComponentProps<'img'> &
|
|
20
|
-
React.ComponentProps<typeof UI>
|
|
1
|
+
import UI from "../ui";
|
|
2
|
+
import React, { useMemo } from "react";
|
|
3
|
+
import type { ImgProps } from "./img.types";
|
|
21
4
|
|
|
22
|
-
|
|
23
|
-
* Img component
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
5
|
+
/**
|
|
6
|
+
* Img - A semantic image component with accessibility and performance best practices.
|
|
7
|
+
*
|
|
8
|
+
* This component wraps the native `<img>` element with enhanced features:
|
|
9
|
+
* - **Responsive images** via optional srcset/sizes
|
|
10
|
+
* - **Lazy loading** by default for performance
|
|
11
|
+
* - **Error handling** with configurable fallback placeholders
|
|
12
|
+
* - **Type safety** with full TypeScript support
|
|
13
|
+
*
|
|
14
|
+
* ## Accessibility Patterns (WCAG 2.1 AA)
|
|
15
|
+
*
|
|
16
|
+
* ### Decorative Images
|
|
17
|
+
* Images that are purely visual decoration should use an empty alt attribute.
|
|
18
|
+
* These images are typically borders, patterns, or visual separators.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* // ✅ GOOD: Decorative border image
|
|
23
|
+
* <Img src="/decorative-border.png" alt="" />
|
|
24
|
+
*
|
|
25
|
+
* // ✅ GOOD: Background pattern
|
|
26
|
+
* <Img src="/pattern.svg" alt="" loading="eager" />
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ### Semantic Images
|
|
30
|
+
* Images that convey information must have descriptive alt text that explains
|
|
31
|
+
* the content and purpose of the image.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* // ✅ GOOD: Informative image with descriptive alt
|
|
36
|
+
* <Img
|
|
37
|
+
* src="/sales-chart.png"
|
|
38
|
+
* alt="Sales chart showing 30% revenue growth in Q4 2024"
|
|
39
|
+
* />
|
|
40
|
+
*
|
|
41
|
+
* // ✅ GOOD: Product photo with context
|
|
42
|
+
* <Img
|
|
43
|
+
* src="/laptop.jpg"
|
|
44
|
+
* alt="Silver MacBook Pro 14-inch on wooden desk"
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* ## Performance Optimization
|
|
49
|
+
*
|
|
50
|
+
* ### Lazy Loading
|
|
51
|
+
* By default, images use lazy loading to improve page load performance.
|
|
52
|
+
* Only use `loading="eager"` for above-the-fold images.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* // ✅ GOOD: Lazy load below-the-fold image
|
|
57
|
+
* <Img src="/photo.jpg" alt="Photo" />
|
|
58
|
+
*
|
|
59
|
+
* // ✅ GOOD: Eager load hero image
|
|
60
|
+
* <Img
|
|
61
|
+
* src="/hero.jpg"
|
|
62
|
+
* alt="Hero banner"
|
|
63
|
+
* loading="eager"
|
|
64
|
+
* fetchpriority="high"
|
|
65
|
+
* />
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* ### Responsive Images
|
|
69
|
+
* Use srcset and sizes for responsive images to serve appropriate image sizes
|
|
70
|
+
* based on viewport width, improving performance and bandwidth usage.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* // ✅ GOOD: Responsive image with multiple sizes
|
|
75
|
+
* <Img
|
|
76
|
+
* src="/photo.jpg"
|
|
77
|
+
* srcSet="/photo-320w.jpg 320w, /photo-640w.jpg 640w, /photo-1024w.jpg 1024w"
|
|
78
|
+
* sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px"
|
|
79
|
+
* alt="Responsive image adapts to viewport"
|
|
80
|
+
* />
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* ## Error Handling
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* // ✅ GOOD: Custom placeholder on error
|
|
88
|
+
* <Img
|
|
89
|
+
* src="/photo.jpg"
|
|
90
|
+
* placeholder="/fallback.png"
|
|
91
|
+
* alt="User profile photo"
|
|
92
|
+
* />
|
|
93
|
+
*
|
|
94
|
+
* // ✅ GOOD: Custom error handler
|
|
95
|
+
* <Img
|
|
96
|
+
* src="/photo.jpg"
|
|
97
|
+
* onError={(e) => {
|
|
98
|
+
* console.error('Image failed to load')
|
|
99
|
+
* logToAnalytics('image_error', { src: e.currentTarget.src })
|
|
100
|
+
* }}
|
|
101
|
+
* alt="Photo"
|
|
102
|
+
* />
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @param {ImgProps} props - Component props extending native img attributes
|
|
106
|
+
* @returns {React.ReactElement} Image element with enhanced functionality
|
|
107
|
+
*
|
|
108
|
+
* @see {@link ImgProps} for complete prop documentation
|
|
109
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html
|
|
41
110
|
*/
|
|
42
111
|
export const Img = ({
|
|
43
|
-
src =
|
|
112
|
+
src = "//",
|
|
44
113
|
alt,
|
|
45
114
|
width = 480,
|
|
46
115
|
height,
|
|
47
116
|
styles,
|
|
48
|
-
loading =
|
|
49
|
-
placeholder
|
|
50
|
-
fetchpriority =
|
|
51
|
-
decoding =
|
|
52
|
-
|
|
53
|
-
|
|
117
|
+
loading = "lazy",
|
|
118
|
+
placeholder,
|
|
119
|
+
fetchpriority = "low",
|
|
120
|
+
decoding = "auto",
|
|
121
|
+
srcSet,
|
|
122
|
+
sizes,
|
|
123
|
+
onError,
|
|
124
|
+
onLoad,
|
|
54
125
|
...props
|
|
55
|
-
}:
|
|
126
|
+
}: ImgProps) => {
|
|
127
|
+
/**
|
|
128
|
+
* Generates a performant, responsive SVG gradient placeholder.
|
|
129
|
+
* Uses data URI to avoid network requests and memoizes based on dimensions.
|
|
130
|
+
* The SVG uses viewBox for perfect scaling at any size.
|
|
131
|
+
*
|
|
132
|
+
* Features:
|
|
133
|
+
* - Zero network requests (works offline)
|
|
134
|
+
* - ~900 bytes vs. 5-10KB external image
|
|
135
|
+
* - Responsive with viewBox
|
|
136
|
+
* - Attractive gradient (indigo → purple → pink)
|
|
137
|
+
* - Dimension text for debugging
|
|
138
|
+
*/
|
|
139
|
+
const defaultPlaceholder = useMemo(() => {
|
|
140
|
+
const w = typeof width === "number" ? width : 480;
|
|
141
|
+
const h = typeof height === "number" ? height : Math.round(w * 0.75);
|
|
142
|
+
|
|
143
|
+
// Responsive SVG with attractive gradient and dimension text
|
|
144
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}">
|
|
145
|
+
<defs>
|
|
146
|
+
<linearGradient id="grad-${w}-${h}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
147
|
+
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
|
|
148
|
+
<stop offset="50%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
|
149
|
+
<stop offset="100%" style="stop-color:#ec4899;stop-opacity:1" />
|
|
150
|
+
</linearGradient>
|
|
151
|
+
</defs>
|
|
152
|
+
<rect width="${w}" height="${h}" fill="url(#grad-${w}-${h})"/>
|
|
153
|
+
<circle cx="${w * 0.15}" cy="${h * 0.2}" r="${Math.min(w, h) * 0.08}" fill="rgba(255,255,255,0.2)"/>
|
|
154
|
+
<path d="M0,${h * 0.75} Q${w * 0.25},${h * 0.65} ${w * 0.5},${h * 0.75} T${w},${h * 0.75} L${w},${h} L0,${h} Z" fill="rgba(0,0,0,0.15)"/>
|
|
155
|
+
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="${Math.max(16, Math.min(w, h) * 0.05)}" font-weight="500" fill="rgba(255,255,255,0.9)">${w}×${h}</text>
|
|
156
|
+
</svg>`;
|
|
157
|
+
|
|
158
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
159
|
+
}, [width, height]);
|
|
160
|
+
|
|
161
|
+
const fallbackPlaceholder = placeholder ?? defaultPlaceholder;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handles image load errors.
|
|
165
|
+
* Calls custom error handler if provided, then applies fallback placeholder.
|
|
166
|
+
* The custom handler can prevent the default fallback by calling e.preventDefault().
|
|
167
|
+
*/
|
|
56
168
|
const handleImgError = (
|
|
57
|
-
e: React.SyntheticEvent<HTMLImageElement, Event
|
|
169
|
+
e: React.SyntheticEvent<HTMLImageElement, Event>
|
|
58
170
|
): void => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
171
|
+
// Call custom error handler first (for logging, analytics, etc.)
|
|
172
|
+
if (onError) {
|
|
173
|
+
onError(e);
|
|
62
174
|
}
|
|
63
|
-
|
|
64
|
-
|
|
175
|
+
|
|
176
|
+
// Apply fallback unless preventDefault() was called
|
|
177
|
+
if (!e.defaultPrevented) {
|
|
178
|
+
// Avoid infinite error loop by checking if already showing placeholder
|
|
179
|
+
if (e.currentTarget.src !== fallbackPlaceholder) {
|
|
180
|
+
e.currentTarget.src = fallbackPlaceholder;
|
|
181
|
+
}
|
|
65
182
|
}
|
|
66
|
-
}
|
|
183
|
+
};
|
|
67
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Handles successful image load.
|
|
187
|
+
* Calls custom load handler if provided.
|
|
188
|
+
*/
|
|
68
189
|
const handleImgLoad = (
|
|
69
|
-
e: React.SyntheticEvent<HTMLImageElement, Event
|
|
190
|
+
e: React.SyntheticEvent<HTMLImageElement, Event>
|
|
70
191
|
): void => {
|
|
71
|
-
|
|
72
|
-
}
|
|
192
|
+
onLoad?.(e);
|
|
193
|
+
};
|
|
73
194
|
|
|
74
195
|
return (
|
|
75
196
|
<UI
|
|
@@ -77,17 +198,19 @@ export const Img = ({
|
|
|
77
198
|
src={src}
|
|
78
199
|
alt={alt}
|
|
79
200
|
width={width}
|
|
80
|
-
height={height ||
|
|
201
|
+
height={height || "auto"}
|
|
81
202
|
loading={loading}
|
|
82
203
|
style={styles}
|
|
204
|
+
srcSet={srcSet}
|
|
205
|
+
sizes={sizes}
|
|
83
206
|
onError={handleImgError}
|
|
84
207
|
onLoad={handleImgLoad}
|
|
85
|
-
fetchPriority={fetchpriority}
|
|
86
208
|
decoding={decoding}
|
|
87
209
|
{...props}
|
|
210
|
+
{...(fetchpriority && { fetchpriority })}
|
|
88
211
|
/>
|
|
89
|
-
)
|
|
90
|
-
}
|
|
212
|
+
);
|
|
213
|
+
};
|
|
91
214
|
|
|
92
|
-
export default Img
|
|
93
|
-
Img.displayName =
|
|
215
|
+
export default Img;
|
|
216
|
+
Img.displayName = "Img";
|