Skip to content

Commit 2bd1198

Browse files
committed
Add Image component, refactor Diagram to use it
1 parent 17e242e commit 2bd1198

7 files changed

Lines changed: 679 additions & 17 deletions

File tree

‎src/components/Diagram/index.astro‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
---
2-
import type { ImageMetadata } from 'astro'
3-
import { Picture } from 'astro:assets'
2+
import Image from '@components/Image/index.astro'
3+
4+
type ImageSource = {
5+
src: string
6+
width: number
7+
height: number
8+
}
49
510
export interface Props {
611
/** The image source for the diagram */
7-
src?: ImageMetadata
12+
src?: ImageSource
813
/** The title of the diagram */
914
title: string
1015
/** A detailed description of the diagram for vision-impaired users */
@@ -16,20 +21,7 @@ const hasDefaultSlot = Astro.slots.has('default')
1621
---
1722

1823
<figure class="my-6 mx-auto">
19-
{src && (
20-
<Picture
21-
src={src}
22-
alt={title}
23-
widths={[480, 768, 1024, 1280]}
24-
sizes="100vw"
25-
formats={['avif', 'webp', 'jpeg']}
26-
layout="constrained"
27-
fit="contain"
28-
position="center"
29-
class="w-full"
30-
loading="lazy"
31-
/>
32-
)}
24+
{src && <Image src={src} alt={title} variant="center" />}
3325

3426
{hasDefaultSlot && (
3527
<div class="flex justify-center">
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
export type Props = {
3+
color: string
4+
classes?: string
5+
size: number
6+
accessible: boolean
7+
focusable: boolean
8+
isListMarker?: boolean
9+
id?: string
10+
}
11+
12+
const { color, classes, size, accessible, focusable, isListMarker, id } = Astro.props
13+
---
14+
15+
<svg
16+
id={id}
17+
xmlns="http://www.w3.org/2000/svg"
18+
class:list={["shrink-0", `w-${size} h-${size}`, `text-${color}`, classes, isListMarker ? "mt-0.5" : ""]}
19+
fill="currentColor"
20+
stroke="none"
21+
viewBox="0 0 24 24"
22+
aria-hidden={accessible ? "false" : "true"}
23+
focusable={focusable ? "true" : "false"}
24+
>
25+
{accessible && <title>Minus Symbol Icon</title>}
26+
<path
27+
stroke-linecap="round"
28+
stroke-linejoin="round"
29+
stroke-width="2"
30+
d="M 19,13 H 5 a 1,1 0 0 1 0,-2 h 14 a 1,1 0 0 1 0,2 z"
31+
/>
32+
</svg>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
import Image from '@components/Image/index.astro'
3+
import backstageBackground from '@assets/images/backstage-background.jpg'
4+
5+
export interface Props {
6+
alt?: string
7+
variant?: 'left' | 'right' | 'center' | 'full'
8+
}
9+
10+
const {
11+
alt = 'Backstage background test image',
12+
variant = 'center',
13+
}: Props = Astro.props
14+
---
15+
16+
<Image src={backstageBackground} alt={alt} variant={variant} />
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { beforeEach, describe, expect, test } from 'vitest'
2+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import ImageFixture from '@components/Image/client/__fixtures__/index.fixture.astro'
4+
import type { ImageElement as ImageElementInstance } from '@components/Image/client'
5+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
6+
import { executeRender } from '@test/unit/helpers/litRuntime'
7+
8+
type ImageModule = WebComponentModule<ImageElementInstance>
9+
10+
const getMainImage = (element: ImageElementInstance): HTMLImageElement | null =>
11+
element.querySelector('[data-image-figure] img')
12+
13+
const getOpenButton = (element: ImageElementInstance): HTMLButtonElement | null =>
14+
element.querySelector('[data-image-open]')
15+
16+
const getDialog = (element: ImageElementInstance): HTMLElement | null =>
17+
element.querySelector('[data-image-dialog]')
18+
19+
const getModalImage = (element: ImageElementInstance): HTMLImageElement | null =>
20+
element.querySelector('[data-image-modal-asset]')
21+
22+
describe('Image web component', () => {
23+
let container: AstroContainer
24+
25+
beforeEach(async () => {
26+
container = await AstroContainer.create()
27+
})
28+
29+
const renderImage = async (
30+
props: {
31+
alt?: string
32+
variant?: 'left' | 'right' | 'center' | 'full'
33+
},
34+
assertion: (_context: {
35+
element: ImageElementInstance
36+
window: Window & typeof globalThis
37+
}) => Promise<void> | void
38+
) => {
39+
await executeRender<ImageModule>({
40+
container,
41+
component: ImageFixture,
42+
moduleSpecifier: '@components/Image/client/index',
43+
args: { props },
44+
waitForReady: async element => {
45+
await element.updateComplete
46+
},
47+
assert: async ({ element, window }) => {
48+
await assertion({ element, window: window as Window & typeof globalThis })
49+
},
50+
})
51+
}
52+
53+
test('renders centered image markup by default', async () => {
54+
await renderImage({}, async ({ element }) => {
55+
expect(element.getAttribute('data-image-variant')).toBe('center')
56+
57+
const mainImage = getMainImage(element)
58+
expect(mainImage?.getAttribute('alt')).toBe('Backstage background test image')
59+
60+
const openButton = getOpenButton(element)
61+
expect(openButton?.getAttribute('aria-label')).toContain('Backstage background test image')
62+
63+
const dialog = getDialog(element)
64+
expect(dialog?.hasAttribute('hidden')).toBe(true)
65+
})
66+
})
67+
68+
test('opens the modal when the magnify button is activated', async () => {
69+
await renderImage({ variant: 'right' }, async ({ element, window }) => {
70+
expect(element.getAttribute('data-image-variant')).toBe('right')
71+
72+
const openButton = getOpenButton(element)
73+
openButton?.dispatchEvent(new window.Event('click', { bubbles: true }))
74+
await element.updateComplete
75+
76+
const dialog = getDialog(element)
77+
expect(dialog?.hasAttribute('hidden')).toBe(false)
78+
expect(dialog?.getAttribute('aria-label')).toContain('Backstage background test image')
79+
80+
const modalImage = getModalImage(element)
81+
expect(modalImage?.getAttribute('alt')).toBe('Backstage background test image')
82+
83+
const closeIcon = element.querySelector(
84+
'button[aria-label="Close image dialog"] svg'
85+
) as SVGElement | null
86+
expect(closeIcon?.getAttribute('aria-hidden')).toBe('true')
87+
expect(closeIcon?.getAttribute('focusable')).toBe('false')
88+
})
89+
})
90+
91+
test('closes the modal on Escape', async () => {
92+
await renderImage({}, async ({ element, window }) => {
93+
const openButton = getOpenButton(element)
94+
openButton?.dispatchEvent(new window.Event('click', { bubbles: true }))
95+
await element.updateComplete
96+
97+
window.document.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Escape' }))
98+
await element.updateComplete
99+
100+
const dialog = getDialog(element)
101+
expect(dialog?.hasAttribute('hidden')).toBe(true)
102+
})
103+
})
104+
})

0 commit comments

Comments
 (0)