Skip to content

Commit 03fdd7c

Browse files
committed
Refactor Code components to extend LitElements
1 parent 97f4cb8 commit 03fdd7c

7 files changed

Lines changed: 202 additions & 108 deletions

File tree

_TODO.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,6 @@ This article has different approaches to [print pagination](https://www.customjs
274274

275275
A bunch of our web components extend directly from HTMLElement instead of following the instructions to extend LitElement.
276276

277-
- Code/CodeBlock
278-
- Code/CodeTabs
279277
- Consent/Banner
280278
- Consent/Checkbox
281279
- Consent/Preferences
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
import CodeBlockRegister from '@components/Code/CodeBlock/index.astro'
3+
---
4+
5+
<CodeBlockRegister />
6+
7+
<code-block>
8+
<pre data-language="typescript"><code>const message = 'hello'</code></pre>
9+
</code-block>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { beforeEach, describe, expect, it } from 'vitest'
2+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
4+
import { executeRender } from '@test/unit/helpers/litRuntime'
5+
import CodeBlockFixture from '@components/Code/CodeBlock/client/__fixtures__/codeBlock.fixture.astro'
6+
import type { CodeBlockElement } from '../index'
7+
8+
type CodeBlockModule = WebComponentModule<CodeBlockElement>
9+
10+
describe('CodeBlock web component', () => {
11+
let container: AstroContainer
12+
13+
beforeEach(async () => {
14+
container = await AstroContainer.create()
15+
})
16+
17+
const renderCodeBlock = async (
18+
assertion: (_context: { element: CodeBlockElement }) => Promise<void> | void
19+
): Promise<void> => {
20+
await executeRender<CodeBlockModule>({
21+
container,
22+
component: CodeBlockFixture,
23+
moduleSpecifier: '@components/Code/CodeBlock/client/index',
24+
selector: 'code-block',
25+
waitForReady: async (element: CodeBlockElement) => {
26+
await element.updateComplete
27+
},
28+
assert: async ({ element }) => {
29+
await assertion({ element })
30+
},
31+
})
32+
}
33+
34+
it('marks the element as enhanced on connect', async () => {
35+
await renderCodeBlock(async ({ element }) => {
36+
expect(element.dataset['enhanced']).toBe('true')
37+
})
38+
})
39+
})
Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1-
export function registerCodeBlockWebComponent(): void {
2-
if (customElements.get('code-block')) return
3-
customElements.define('code-block', CodeBlockElement)
4-
}
1+
import { LitElement } from 'lit'
2+
import { defineCustomElement } from '@components/scripts/utils'
3+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
4+
5+
export class CodeBlockElement extends LitElement {
6+
static registeredName = 'code-block'
7+
8+
protected override createRenderRoot(): HTMLElement {
9+
return this
10+
}
11+
12+
override connectedCallback(): void {
13+
super.connectedCallback()
514

6-
class CodeBlockElement extends HTMLElement {
7-
connectedCallback(): void {
815
if (this.dataset['enhanced'] === 'true') return
916
this.dataset['enhanced'] = 'true'
1017
}
1118
}
19+
20+
export const registerWebComponent = async (
21+
tagName = CodeBlockElement.registeredName
22+
): Promise<void> => {
23+
if (typeof window === 'undefined') return
24+
defineCustomElement(tagName, CodeBlockElement)
25+
}
26+
27+
export const registerCodeBlockWebComponent = registerWebComponent
28+
29+
export const webComponentModule: WebComponentModule<CodeBlockElement> = {
30+
registeredName: CodeBlockElement.registeredName,
31+
componentCtor: CodeBlockElement,
32+
registerWebComponent,
33+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
import CodeTabsRegister from '@components/Code/CodeTabs/index.astro'
3+
4+
export interface Props {
5+
variant?: 'single' | 'multi' | 'excluded'
6+
}
7+
8+
const { variant = 'multi' } = Astro.props as Props
9+
---
10+
11+
<CodeTabsRegister />
12+
13+
<code-tabs>
14+
{variant === 'multi' ? (
15+
<>
16+
<pre data-code-tabs-tab="A" data-language="javascript">
17+
<code>console.log('A')</code>
18+
</pre>
19+
<pre data-code-tabs-tab="B" data-language="typescript">
20+
<code>console.log('B')</code>
21+
</pre>
22+
</>
23+
) : variant === 'excluded' ? (
24+
<pre data-language="text">
25+
<code>plain text</code>
26+
</pre>
27+
) : (
28+
<pre data-language="typescript">
29+
<code>const x: number = 1</code>
30+
</pre>
31+
)}
32+
</code-tabs>

src/components/Code/CodeTabs/client/__tests__/index.spec.ts

Lines changed: 65 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
1-
import { describe, expect, it, vi } from 'vitest'
2-
import { withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
4+
import { executeRender, withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
5+
import CodeTabsFixture from '@components/Code/CodeTabs/client/__fixtures__/codeTabs.fixture.astro'
6+
import type { CodeTabsElement } from '../index'
7+
8+
type CodeTabsModule = WebComponentModule<CodeTabsElement>
39

410
describe('CodeTabs web component', () => {
11+
let container: AstroContainer
12+
13+
beforeEach(async () => {
14+
container = await AstroContainer.create()
15+
})
16+
17+
afterEach(() => {
18+
vi.restoreAllMocks()
19+
})
20+
21+
const renderCodeTabs = async (
22+
props: { variant?: 'single' | 'multi' | 'excluded' },
23+
assertion: (_context: { element: CodeTabsElement; window: Window }) => Promise<void> | void
24+
): Promise<void> => {
25+
await executeRender<CodeTabsModule>({
26+
container,
27+
component: CodeTabsFixture,
28+
moduleSpecifier: '@components/Code/CodeTabs/client/index',
29+
args: { props },
30+
selector: 'code-tabs',
31+
waitForReady: async (element: CodeTabsElement) => {
32+
await element.updateComplete
33+
},
34+
assert: async ({ element, window }) => {
35+
await assertion({ element, window: window ?? globalThis.window })
36+
},
37+
})
38+
}
39+
540
it('skips registration when customElements is unavailable', async () => {
641
await withJsdomEnvironment(async () => {
742
const globalRef = globalThis as unknown as { customElements?: CustomElementRegistry }
@@ -28,7 +63,7 @@ describe('CodeTabs web component', () => {
2863
})
2964

3065
it('copies the active code block when clipboard is available', async () => {
31-
await withJsdomEnvironment(async ({ window }) => {
66+
await renderCodeTabs({ variant: 'single' }, async ({ element, window }) => {
3267
const globalRef = globalThis as unknown as { navigator?: Navigator }
3368
const originalNavigator = globalRef.navigator
3469

@@ -45,18 +80,6 @@ describe('CodeTabs web component', () => {
4580
})
4681

4782
try {
48-
const { registerCodeTabsWebComponent } =
49-
await import('@components/Code/CodeTabs/client/index')
50-
registerCodeTabsWebComponent()
51-
52-
const element = window.document.createElement('code-tabs')
53-
const pre = window.document.createElement('pre')
54-
const code = window.document.createElement('code')
55-
code.textContent = 'console.log("hello")'
56-
pre.appendChild(code)
57-
element.appendChild(pre)
58-
window.document.body.appendChild(element)
59-
6083
const copyButton = element.querySelector(
6184
'button[aria-label="Copy"]'
6285
) as HTMLButtonElement | null
@@ -65,7 +88,7 @@ describe('CodeTabs web component', () => {
6588
copyButton?.click()
6689

6790
await vi.waitFor(() => {
68-
expect(writeText).toHaveBeenCalledWith('console.log("hello")')
91+
expect(writeText).toHaveBeenCalledWith('const x: number = 1')
6992
})
7093
} finally {
7194
Object.defineProperty(globalThis, 'navigator', {
@@ -78,48 +101,40 @@ describe('CodeTabs web component', () => {
78101
})
79102

80103
it('does not throw when clipboard is unavailable', async () => {
81-
await withJsdomEnvironment(async ({ window }) => {
104+
await renderCodeTabs({ variant: 'single' }, ({ element, window }) => {
105+
const globalRef = globalThis as unknown as { navigator?: Navigator }
106+
const originalNavigator = globalRef.navigator
107+
108+
Object.defineProperty(globalThis, 'navigator', {
109+
configurable: true,
110+
writable: true,
111+
value: window.navigator,
112+
})
113+
82114
Object.defineProperty(window.navigator, 'clipboard', {
83115
configurable: true,
84116
value: undefined,
85117
})
86118

87-
const { registerCodeTabsWebComponent } =
88-
await import('@components/Code/CodeTabs/client/index')
89-
registerCodeTabsWebComponent()
90-
91-
const element = window.document.createElement('code-tabs')
92-
const pre = window.document.createElement('pre')
93-
pre.textContent = 'no clipboard'
94-
element.appendChild(pre)
95-
window.document.body.appendChild(element)
96-
97-
const copyButton = element.querySelector(
98-
'button[aria-label="Copy"]'
99-
) as HTMLButtonElement | null
100-
expect(copyButton, 'CodeTabs should render a copy button').toBeTruthy()
119+
try {
120+
const copyButton = element.querySelector(
121+
'button[aria-label="Copy"]'
122+
) as HTMLButtonElement | null
123+
expect(copyButton, 'CodeTabs should render a copy button').toBeTruthy()
101124

102-
expect(() => copyButton?.click()).not.toThrow()
125+
expect(() => copyButton?.click()).not.toThrow()
126+
} finally {
127+
Object.defineProperty(globalThis, 'navigator', {
128+
configurable: true,
129+
writable: true,
130+
value: originalNavigator,
131+
})
132+
}
103133
})
104134
})
105135

106136
it('renders a single tab label for a single named code block', async () => {
107-
await withJsdomEnvironment(async ({ window }) => {
108-
const { registerCodeTabsWebComponent } =
109-
await import('@components/Code/CodeTabs/client/index')
110-
registerCodeTabsWebComponent()
111-
112-
const element = window.document.createElement('code-tabs')
113-
const pre = window.document.createElement('pre')
114-
pre.setAttribute('data-language', 'typescript')
115-
116-
const code = window.document.createElement('code')
117-
code.textContent = 'const x: number = 1'
118-
pre.appendChild(code)
119-
120-
element.appendChild(pre)
121-
window.document.body.appendChild(element)
122-
137+
await renderCodeTabs({ variant: 'single' }, ({ element }) => {
123138
const tabButton = element.querySelector('button[data-code-tabs-button="0"]')
124139
expect(tabButton, 'CodeTabs should render a tab button for a single named code block').toBeTruthy()
125140
expect(tabButton?.textContent).toBe('TypeScript')
@@ -131,27 +146,7 @@ describe('CodeTabs web component', () => {
131146
})
132147

133148
it('does not apply hover color change to the active tab when multiple tabs exist', async () => {
134-
await withJsdomEnvironment(async ({ window }) => {
135-
const { registerCodeTabsWebComponent } =
136-
await import('@components/Code/CodeTabs/client/index')
137-
registerCodeTabsWebComponent()
138-
139-
const element = window.document.createElement('code-tabs')
140-
141-
const preA = window.document.createElement('pre')
142-
preA.setAttribute('data-code-tabs-tab', 'A')
143-
preA.setAttribute('data-language', 'javascript')
144-
preA.appendChild(window.document.createElement('code'))
145-
146-
const preB = window.document.createElement('pre')
147-
preB.setAttribute('data-code-tabs-tab', 'B')
148-
preB.setAttribute('data-language', 'typescript')
149-
preB.appendChild(window.document.createElement('code'))
150-
151-
element.appendChild(preA)
152-
element.appendChild(preB)
153-
window.document.body.appendChild(element)
154-
149+
await renderCodeTabs({ variant: 'multi' }, ({ element }) => {
155150
const activeButton = element.querySelector('button[data-code-tabs-button="0"]')
156151
const inactiveButton = element.querySelector('button[data-code-tabs-button="1"]')
157152

@@ -171,18 +166,7 @@ describe('CodeTabs web component', () => {
171166
})
172167

173168
it('does not render tabs for excluded single-language blocks', async () => {
174-
await withJsdomEnvironment(async ({ window }) => {
175-
const { registerCodeTabsWebComponent } =
176-
await import('@components/Code/CodeTabs/client/index')
177-
registerCodeTabsWebComponent()
178-
179-
const element = window.document.createElement('code-tabs')
180-
const pre = window.document.createElement('pre')
181-
pre.setAttribute('data-language', 'text')
182-
pre.textContent = 'plain text'
183-
element.appendChild(pre)
184-
window.document.body.appendChild(element)
185-
169+
await renderCodeTabs({ variant: 'excluded' }, ({ element }) => {
186170
const anyTabButton = element.querySelector('button[data-code-tabs-button]')
187171
expect(anyTabButton, 'CodeTabs should not render tab buttons for excluded languages').toBeFalsy()
188172
})

0 commit comments

Comments
 (0)