diff --git a/packages/site-components/src/__tests__/react-attributes.spec.ts b/packages/site-components/src/__tests__/react-attributes.spec.ts new file mode 100644 index 000000000..8e7a67547 --- /dev/null +++ b/packages/site-components/src/__tests__/react-attributes.spec.ts @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import '../components/td-code'; +import '../components/td-doc-copy'; +import '../components/td-doc-demo'; + +async function waitForRender(): Promise { + await new Promise((resolve) => setTimeout(resolve)); +} + +describe('React attribute integration', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('renders td-code created through innerHTML', async () => { + const container = document.createElement('div'); + container.innerHTML = ''; + document.body.appendChild(container); + + await waitForRender(); + + const code = container.querySelector('td-code'); + expect(code?.shadowRoot?.textContent).toContain('Button'); + }); + + it('renders td-doc-demo code from React-style data attributes', async () => { + const demo = document.createElement('td-doc-demo'); + demo.setAttribute('languages', 'TypeScript,JavaScript'); + demo.setAttribute('data-TypeScript', 'const value: string = "TS";'); + demo.setAttribute('data-JavaScript', 'const value = "JS";'); + document.body.appendChild(demo); + + await waitForRender(); + + const tabs = Array.from(demo.shadowRoot?.querySelectorAll('.TDesign-doc-demo-tabs__item') ?? []); + expect(tabs.map((tab) => tab.textContent?.trim())).toEqual(['TypeScript', 'JavaScript']); + expect(demo.shadowRoot?.querySelector('code')?.textContent).toContain('const value: string = "TS";'); + + demo.setAttribute('data-TypeScript', 'const updated: string = "TS";'); + await waitForRender(); + + expect(demo.shadowRoot?.querySelector('code')?.textContent).toContain('const updated: string = "TS";'); + }); +}); diff --git a/packages/site-components/src/__tests__/td-doc-badge.spec.ts b/packages/site-components/src/__tests__/td-doc-badge.spec.ts new file mode 100644 index 000000000..7db9fe04f --- /dev/null +++ b/packages/site-components/src/__tests__/td-doc-badge.spec.ts @@ -0,0 +1,51 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import '../components/td-doc-badge'; + +async function waitForRender(): Promise { + await new Promise((resolve) => setTimeout(resolve)); +} + +describe('td-doc-badge', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('reads attributes set after element creation', async () => { + const badge = document.createElement('td-doc-badge'); + badge.setAttribute('label', 'statements'); + badge.setAttribute('message', '87.5%'); + document.body.appendChild(badge); + + await waitForRender(); + + expect(badge.shadowRoot?.querySelector('img')?.getAttribute('src')).toBe( + 'https://img.shields.io/badge/statements-87.5%25-yellow', + ); + }); + + it('updates when an attribute changes', async () => { + const badge = document.createElement('td-doc-badge'); + document.body.appendChild(badge); + await waitForRender(); + + badge.setAttribute('message', '95%'); + await waitForRender(); + + expect(badge.shadowRoot?.querySelector('img')?.getAttribute('src')).toBe( + 'https://img.shields.io/badge/coverage-95%25-brightgreen', + ); + }); + + it('preserves properties set before connection', async () => { + const badge = document.createElement('td-doc-badge') as HTMLElement & { message: string }; + badge.message = '72%'; + document.body.appendChild(badge); + + await waitForRender(); + + expect(badge.shadowRoot?.querySelector('img')?.getAttribute('src')).toBe( + 'https://img.shields.io/badge/coverage-72%25-yellow', + ); + }); +}); diff --git a/packages/site-components/src/__tests__/td-doc-popup.spec.ts b/packages/site-components/src/__tests__/td-doc-popup.spec.ts index ca5eeeef5..1520562f2 100644 --- a/packages/site-components/src/__tests__/td-doc-popup.spec.ts +++ b/packages/site-components/src/__tests__/td-doc-popup.spec.ts @@ -28,7 +28,9 @@ vi.mock('hybrids', () => ({ })); vi.mock('@popperjs/core', () => ({ createPopper: mockCreatePopper })); -vi.mock('@utils', () => ({ parseBoolean: vi.fn((value: unknown) => Boolean(value)) })); +vi.mock('@utils', () => ({ + parseBoolean: vi.fn((value: unknown) => Boolean(value)), +})); vi.mock('../components/td-doc-popup/style.less?inline', () => ({ default: '' })); interface PortalContainerMock { diff --git a/packages/site-components/src/__tests__/td-select.spec.ts b/packages/site-components/src/__tests__/td-select.spec.ts new file mode 100644 index 000000000..1ffe4a865 --- /dev/null +++ b/packages/site-components/src/__tests__/td-select.spec.ts @@ -0,0 +1,64 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import '../components/td-select'; + +interface SelectElement extends HTMLElement { + borderless: boolean; + value: string | number; + options: Array<{ label: string; value: string | number }>; +} + +async function waitForRender(): Promise { + await new Promise((resolve) => setTimeout(resolve)); +} + +describe('td-select', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('reads attributes set after element creation', async () => { + const select = document.createElement('td-select') as SelectElement; + select.options = [ + { label: 'One', value: '1' }, + { label: 'Two', value: '2' }, + ]; + select.setAttribute('value', '2'); + select.setAttribute('borderless', ''); + document.body.appendChild(select); + + await waitForRender(); + + expect(select.shadowRoot?.querySelector('input')?.value).toBe('Two'); + expect(select.shadowRoot?.querySelector('.TDesign-select-input')?.classList).toContain( + 'TDesign-select-input--borderless', + ); + }); + + it('updates when the value attribute changes', async () => { + const select = document.createElement('td-select') as SelectElement; + select.options = [ + { label: 'One', value: '1' }, + { label: 'Two', value: '2' }, + ]; + document.body.appendChild(select); + await waitForRender(); + + select.setAttribute('value', '1'); + await waitForRender(); + + expect(select.shadowRoot?.querySelector('input')?.value).toBe('One'); + }); + + it('preserves properties set before connection', async () => { + const select = document.createElement('td-select') as SelectElement; + select.options = [{ label: 'Two', value: 2 }]; + select.value = 2; + document.body.appendChild(select); + + await waitForRender(); + + expect(select.value).toBe(2); + expect(select.shadowRoot?.querySelector('input')?.value).toBe('Two'); + }); +}); diff --git a/packages/site-components/src/components/td-aegis/index.ts b/packages/site-components/src/components/td-aegis/index.ts index 40d2998b0..9719dcef9 100644 --- a/packages/site-components/src/components/td-aegis/index.ts +++ b/packages/site-components/src/components/td-aegis/index.ts @@ -1,5 +1,5 @@ /* global Aegis */ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; interface AegisProps { aegisId: string; diff --git a/packages/site-components/src/components/td-ai-button/index.ts b/packages/site-components/src/components/td-ai-button/index.ts index d0689b310..ccd03ca94 100644 --- a/packages/site-components/src/components/td-ai-button/index.ts +++ b/packages/site-components/src/components/td-ai-button/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import { createAISearchSDK } from './utils'; import style from './style.less?inline'; diff --git a/packages/site-components/src/components/td-avatar/index.ts b/packages/site-components/src/components/td-avatar/index.ts index 91ba41a1e..a748690f9 100644 --- a/packages/site-components/src/components/td-avatar/index.ts +++ b/packages/site-components/src/components/td-avatar/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; interface AvatarProps { diff --git a/packages/site-components/src/components/td-backtop/index.ts b/packages/site-components/src/components/td-backtop/index.ts index 8b04b8a24..3fc4f64cf 100644 --- a/packages/site-components/src/components/td-backtop/index.ts +++ b/packages/site-components/src/components/td-backtop/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; import backTopIcon from '@images/backtop.svg?raw'; diff --git a/packages/site-components/src/components/td-code-block/index.ts b/packages/site-components/src/components/td-code-block/index.ts index 36687ad07..eb97c3e56 100644 --- a/packages/site-components/src/components/td-code-block/index.ts +++ b/packages/site-components/src/components/td-code-block/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; import Prism from 'prismjs'; diff --git a/packages/site-components/src/components/td-code/index.ts b/packages/site-components/src/components/td-code/index.ts index be58510b9..03852d159 100644 --- a/packages/site-components/src/components/td-code/index.ts +++ b/packages/site-components/src/components/td-code/index.ts @@ -1,4 +1,4 @@ -import { define, html } from 'hybrids'; +import { define, html } from '@utils/hybrids'; import style from './style.less?inline'; interface CodeProps { diff --git a/packages/site-components/src/components/td-contributors/index.ts b/packages/site-components/src/components/td-contributors/index.ts index bfe85bb66..83ddaab2b 100644 --- a/packages/site-components/src/components/td-contributors/index.ts +++ b/packages/site-components/src/components/td-contributors/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; const apiUrl = import.meta.env.VITE_CONTRIBUTORS_API_URL; diff --git a/packages/site-components/src/components/td-doc-aside/index.ts b/packages/site-components/src/components/td-doc-aside/index.ts index b6ab615b6..501e6f290 100644 --- a/packages/site-components/src/components/td-doc-aside/index.ts +++ b/packages/site-components/src/components/td-doc-aside/index.ts @@ -1,4 +1,4 @@ -import { html, dispatch, define, type Component } from 'hybrids'; +import { html, dispatch, define, type Component } from '@utils/hybrids'; import menuFoldIcon from '@images/menu-fold.svg?raw'; import menuUnfoldIcon from '@images/menu-unfold.svg?raw'; import { patchShadowDomIntoDom } from '@utils'; diff --git a/packages/site-components/src/components/td-doc-badge/index.ts b/packages/site-components/src/components/td-doc-badge/index.ts index e3b6fb66e..2e0069937 100644 --- a/packages/site-components/src/components/td-doc-badge/index.ts +++ b/packages/site-components/src/components/td-doc-badge/index.ts @@ -1,4 +1,4 @@ -import { html, define, type Descriptor } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; interface BadgeHost { @@ -7,24 +7,6 @@ interface BadgeHost { color: string; } -function attribute(defaultValue: string): Descriptor { - return { - value: (_host, value) => value ?? defaultValue, - connect: (host, key) => { - const attributeName = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); - const syncAttribute = () => { - host[key] = host.getAttribute(attributeName) ?? defaultValue; - }; - const observer = new MutationObserver(syncAttribute); - - if (host.hasAttribute(attributeName)) syncAttribute(); - observer.observe(host, { attributes: true, attributeFilter: [attributeName] }); - - return () => observer.disconnect(); - }, - }; -} - function getColor(message: string): string { const percentage = parseFloat(message); if (percentage >= 90) { @@ -38,9 +20,9 @@ function getColor(message: string): string { export default define({ tag: 'td-doc-badge', - label: attribute('coverage'), - message: attribute('0%'), - color: attribute(''), + label: 'coverage', + message: '0%', + color: '', render: (host) => { const { label, message, color: defaultColor } = host; let color = defaultColor; diff --git a/packages/site-components/src/components/td-doc-changelog/index.ts b/packages/site-components/src/components/td-doc-changelog/index.ts index 914765440..2038dd0af 100644 --- a/packages/site-components/src/components/td-doc-changelog/index.ts +++ b/packages/site-components/src/components/td-doc-changelog/index.ts @@ -1,4 +1,4 @@ -import { define, html } from 'hybrids'; +import { define, html } from '@utils/hybrids'; import { getLocale } from '@config/locale'; import closeIcon from '@images/close.svg?raw'; import { convert2PascalCase, isComponentPage, isEn, isGlobalConfigPage, parseBoolean } from '@utils'; diff --git a/packages/site-components/src/components/td-doc-content/index.ts b/packages/site-components/src/components/td-doc-content/index.ts index 667f3c5ca..8ffebb71e 100644 --- a/packages/site-components/src/components/td-doc-content/index.ts +++ b/packages/site-components/src/components/td-doc-content/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import { mobileBodyStyle } from '@utils'; import style from './style.less?inline'; diff --git a/packages/site-components/src/components/td-doc-copy/index.ts b/packages/site-components/src/components/td-doc-copy/index.ts index c664a9a36..6e248f7f8 100644 --- a/packages/site-components/src/components/td-doc-copy/index.ts +++ b/packages/site-components/src/components/td-doc-copy/index.ts @@ -1,4 +1,4 @@ -import { html, dispatch, define } from 'hybrids'; +import { html, dispatch, define } from '@utils/hybrids'; import style from './style.less?inline'; import copyIcon from '@images/copy.svg?raw'; diff --git a/packages/site-components/src/components/td-doc-demo/index.ts b/packages/site-components/src/components/td-doc-demo/index.ts index 59dcc1f2e..335165ae7 100644 --- a/packages/site-components/src/components/td-doc-demo/index.ts +++ b/packages/site-components/src/components/td-doc-demo/index.ts @@ -1,4 +1,4 @@ -import { html, define, dispatch } from 'hybrids'; +import { html, define, dispatch } from '@utils/hybrids'; import Prism from 'prismjs'; import { parseBoolean } from '@utils'; import style from './style.less?inline'; diff --git a/packages/site-components/src/components/td-doc-empty/index.ts b/packages/site-components/src/components/td-doc-empty/index.ts index 964ec3c53..73f0f622e 100644 --- a/packages/site-components/src/components/td-doc-empty/index.ts +++ b/packages/site-components/src/components/td-doc-empty/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; interface EmptyHost { diff --git a/packages/site-components/src/components/td-doc-footer/index.ts b/packages/site-components/src/components/td-doc-footer/index.ts index 40535156d..1b6eabdfd 100644 --- a/packages/site-components/src/components/td-doc-footer/index.ts +++ b/packages/site-components/src/components/td-doc-footer/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import { getFooterConfig } from '@config/footer'; import { getLocale } from '@config/locale'; import { patchShadowDomIntoDom } from '@utils'; diff --git a/packages/site-components/src/components/td-doc-header/index.ts b/packages/site-components/src/components/td-doc-header/index.ts index 098c8d320..0dd0e2ac3 100644 --- a/packages/site-components/src/components/td-doc-header/index.ts +++ b/packages/site-components/src/components/td-doc-header/index.ts @@ -2,7 +2,7 @@ import { getLocale } from '@config/locale'; import splineConfig from '@config/spline'; import historyIcon from '@images/history.svg?raw'; import { isComponentPage, isGlobalConfigPage, mobileBodyStyle, parseBoolean, watchHtmlMode } from '@utils'; -import { define, html } from 'hybrids'; +import { define, html } from '@utils/hybrids'; import style from './style.less?inline'; interface DocInfo { @@ -88,7 +88,7 @@ function iframeOnload(host: HeaderHost & HTMLElement): void { export default define({ tag: 'td-doc-header', spline: { - value: (_host, v) => v || '', + value: (_host, value) => value || '', connect: (host) => { const observer = watchHtmlMode((themeMode) => handleModeChange(themeMode, host)); diff --git a/packages/site-components/src/components/td-doc-header/td-doc-issue/index.ts b/packages/site-components/src/components/td-doc-header/td-doc-issue/index.ts index e2b0835cc..1786736d1 100644 --- a/packages/site-components/src/components/td-doc-header/td-doc-issue/index.ts +++ b/packages/site-components/src/components/td-doc-header/td-doc-issue/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; import infoIcon from '@images/info.svg?raw'; import checkIcon from '@images/check.svg?raw'; diff --git a/packages/site-components/src/components/td-doc-history/index.ts b/packages/site-components/src/components/td-doc-history/index.ts index 2dbe700bf..9cf25173f 100644 --- a/packages/site-components/src/components/td-doc-history/index.ts +++ b/packages/site-components/src/components/td-doc-history/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import style from './style.less?inline'; import historyIcon from '@images/history.svg?raw'; diff --git a/packages/site-components/src/components/td-doc-layout/index.ts b/packages/site-components/src/components/td-doc-layout/index.ts index 1c0dd2a6b..9d6bba8fe 100644 --- a/packages/site-components/src/components/td-doc-layout/index.ts +++ b/packages/site-components/src/components/td-doc-layout/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import NProgress from 'nprogress'; import style from './style.less?inline'; diff --git a/packages/site-components/src/components/td-doc-phone/index.ts b/packages/site-components/src/components/td-doc-phone/index.ts index acc9c64c5..9d9db4d7c 100644 --- a/packages/site-components/src/components/td-doc-phone/index.ts +++ b/packages/site-components/src/components/td-doc-phone/index.ts @@ -1,4 +1,4 @@ -import { html, define } from 'hybrids'; +import { html, define } from '@utils/hybrids'; import QRCode from 'qrcode'; import style from './style.less?inline'; import qrcodeIcon from '@images/qrcode.svg?raw'; diff --git a/packages/site-components/src/components/td-doc-popup/index.ts b/packages/site-components/src/components/td-doc-popup/index.ts index 4bbf28b1e..ab2d5f9bc 100644 --- a/packages/site-components/src/components/td-doc-popup/index.ts +++ b/packages/site-components/src/components/td-doc-popup/index.ts @@ -1,4 +1,4 @@ -import { html, define, dispatch } from 'hybrids'; +import { html, define, dispatch } from '@utils/hybrids'; import { createPopper, type Instance, type Placement } from '@popperjs/core'; import { parseBoolean } from '@utils'; import style from './style.less?inline'; @@ -57,7 +57,7 @@ export default define({ portal: null, popper: null, equalWidth: { - value: (_host, v) => parseBoolean(v, false), + value: (_host, value) => parseBoolean(value, false), }, visible: { value: (host, v) => v || false, diff --git a/packages/site-components/src/components/td-doc-search/index.ts b/packages/site-components/src/components/td-doc-search/index.ts index c6d213f39..f8deec8f9 100644 --- a/packages/site-components/src/components/td-doc-search/index.ts +++ b/packages/site-components/src/components/td-doc-search/index.ts @@ -16,7 +16,7 @@ * → views.js 根据最新 host 输出 html`` */ -import { define, html, type Descriptor } from 'hybrids'; +import { define, html, type Descriptor } from '@utils/hybrids'; import style from './style.less?inline'; import { diff --git a/packages/site-components/src/components/td-doc-search/views.ts b/packages/site-components/src/components/td-doc-search/views.ts index ea462e944..7ee335cc6 100644 --- a/packages/site-components/src/components/td-doc-search/views.ts +++ b/packages/site-components/src/components/td-doc-search/views.ts @@ -21,7 +21,7 @@ * } */ -import { html, type UpdateFunctionWithMethods } from 'hybrids'; +import { html, type UpdateFunctionWithMethods } from '@utils/hybrids'; import SearchIcon from 'tdesign-icons-svg/src/search.svg?raw'; import CloseIcon from 'tdesign-icons-svg/src/close.svg?raw'; diff --git a/packages/site-components/src/components/td-doc-tabs/index.ts b/packages/site-components/src/components/td-doc-tabs/index.ts index 328912282..e1f39e029 100644 --- a/packages/site-components/src/components/td-doc-tabs/index.ts +++ b/packages/site-components/src/components/td-doc-tabs/index.ts @@ -1,4 +1,4 @@ -import { html, dispatch, define } from 'hybrids'; +import { html, dispatch, define } from '@utils/hybrids'; import { isEn } from '@utils'; import style from './style.less?inline'; diff --git a/packages/site-components/src/components/td-doc-usage/index.ts b/packages/site-components/src/components/td-doc-usage/index.ts index 2e31c3bec..5b6ff6a46 100644 --- a/packages/site-components/src/components/td-doc-usage/index.ts +++ b/packages/site-components/src/components/td-doc-usage/index.ts @@ -1,4 +1,4 @@ -import { html, define, dispatch } from 'hybrids'; +import { html, define, dispatch } from '@utils/hybrids'; import { parseBoolean } from '@utils'; import style from './style.less?inline'; import tipsIcon from '@images/tips.svg?raw'; diff --git a/packages/site-components/src/components/td-header/index.ts b/packages/site-components/src/components/td-header/index.ts index 60b3d91ea..b14b08069 100644 --- a/packages/site-components/src/components/td-header/index.ts +++ b/packages/site-components/src/components/td-header/index.ts @@ -1,4 +1,4 @@ -import { define, html, type UpdateFunctionWithMethods } from 'hybrids'; +import { define, html, type UpdateFunctionWithMethods } from '@utils/hybrids'; import { getHeaderConfig } from '@config/header'; import closeIcon from '@images/close.svg?raw'; import fakeArrowIcon from '@images/fake-arrow.svg?raw'; @@ -133,7 +133,7 @@ function renderNotice(host: HeaderHost) { function renderLinksPopup(host: HeaderHost, trigger: UpdateFunctionWithMethods) { return html` - + ${trigger}