diff --git a/.gitattributes b/.gitattributes index 177ef83c2..419f04c70 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ test/e2e/__screenshots__/** filter=lfs diff=lfs merge=lfs -text +.github/* linguist-vendored=false +.astro/** linguist-vendored diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 71047b321..6c4f3dfbd 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -1,7 +1,8 @@ name: CodeQL config -# Unavoidable checkoutin untrusted environment to use +# Unavoidable checkout in untrusted environment to use # "astro db verify" and "astro db push --remote" commands +# @see PR https://github.com/withastro/astro/pull/15069 paths-ignore: - .github/workflows/deployment-preview.yml - .github/workflows/deployment-production.yml diff --git a/_TODO.md b/_TODO.md index 17fba6b94..23cbe1e1c 100644 --- a/_TODO.md +++ b/_TODO.md @@ -229,10 +229,6 @@ Google Calendar, Apple Calendar, Yahoo Calender, Microsoft 365, Outlook, and T [Relative Time](https://github.com/BryceRussell/astro-github-elements/tree/main/packages/time#readme) -### Display text in a circular layout - -[TextCircle](https://github.com/LoStisWorld/astro-textcircle#astro-textcircle) - ## Markdown ## Code Block and Highlighting diff --git a/package-lock.json b/package-lock.json index 98bd28975..8ba171933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@astrojs/sitemap": "^3.6.0", "@astrojs/vercel": "^9.0.2", "@axe-core/playwright": "^4.11.0", + "@github/relative-time-element": "^5.0.0", "@glidejs/glide": "^3.7.1", "@googlemaps/extended-component-library": "^0.6.14", "@nanostores/lit": "^0.2.3", @@ -4031,6 +4032,12 @@ "node": ">=6" } }, + "node_modules/@github/relative-time-element": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-5.0.0.tgz", + "integrity": "sha512-L/2r0DNR/rMbmHWcsdmhtOiy2gESoGOhItNFD4zJ3nZfHl79Dx3N18Vfx/pYr2lruMOdk1cJZb4wEumm+Dxm1w==", + "license": "MIT" + }, "node_modules/@glidejs/glide": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/@glidejs/glide/-/glide-3.7.1.tgz", diff --git a/package.json b/package.json index 4a2ee7773..618c42410 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@astrojs/sitemap": "^3.6.0", "@astrojs/vercel": "^9.0.2", "@axe-core/playwright": "^4.11.0", + "@github/relative-time-element": "^5.0.0", "@glidejs/glide": "^3.7.1", "@googlemaps/extended-component-library": "^0.6.14", "@nanostores/lit": "^0.2.3", diff --git a/src/components/Time/__tests__/index.spec.ts b/src/components/Time/__tests__/index.spec.ts new file mode 100644 index 000000000..199ce4f56 --- /dev/null +++ b/src/components/Time/__tests__/index.spec.ts @@ -0,0 +1,127 @@ +// @vitest-environment node + +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { experimental_AstroContainer as AstroContainer } from 'astro/container' +import { withJsdomEnvironment } from '@test/unit/helpers/litRuntime' + +describe('Time (Astro)', () => { + let container: AstroContainer + + beforeEach(async () => { + container = await AstroContainer.create() + }) + + test('renders a element and keeps slot fallback content', async () => { + const Time = (await import('@components/Time/index.astro')).default + + const renderedHtml = await container.renderToString(Time, { + props: { + format: 'relative', + datetime: '2020-01-01T00:00:00Z', + }, + slots: { + default: 'Fallback text', + }, + }) + + await withJsdomEnvironment(async ({ window }) => { + window.document.body.innerHTML = renderedHtml + + const element = window.document.querySelector('relative-time') + expect(element).toBeTruthy() + expect(element?.getAttribute('datetime')).toBe('2020-01-01T00:00:00Z') + expect(element?.textContent).toContain('Fallback text') + }) + }) + + test('serializes Date datetime to ISO string', async () => { + const Time = (await import('@components/Time/index.astro')).default + const date = new Date('2024-02-29T12:34:56.000Z') + + const renderedHtml = await container.renderToString(Time, { + props: { + format: 'datetime', + datetime: date, + }, + }) + + await withJsdomEnvironment(async ({ window }) => { + window.document.body.innerHTML = renderedHtml + + const element = window.document.querySelector('relative-time') + expect(element?.getAttribute('datetime')).toBe('2024-02-29T12:34:56.000Z') + }) + }) + + test('uses current time when now=true and datetime is omitted', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2030-01-02T03:04:05.000Z')) + + try { + const Time = (await import('@components/Time/index.astro')).default + + const renderedHtml = await container.renderToString(Time, { + props: { + format: 'relative', + now: true, + }, + }) + + await withJsdomEnvironment(async ({ window }) => { + window.document.body.innerHTML = renderedHtml + + const element = window.document.querySelector('relative-time') + expect(element?.getAttribute('datetime')).toBe('2030-01-02T03:04:05.000Z') + }) + } finally { + vi.useRealTimers() + } + }) + + test('maps formatStyle and timeZoneName to kebab-case attributes', async () => { + const Time = (await import('@components/Time/index.astro')).default + + const renderedHtml = await container.renderToString(Time, { + props: { + format: 'datetime', + datetime: '2020-01-01T00:00:00Z', + formatStyle: 'short', + timeZoneName: 'shortOffset', + }, + }) + + await withJsdomEnvironment(async ({ window }) => { + window.document.body.innerHTML = renderedHtml + + const element = window.document.querySelector('relative-time') + expect(element?.getAttribute('format-style')).toBe('short') + expect(element?.getAttribute('time-zone-name')).toBe('shortOffset') + }) + }) + + test('passes through supported attributes to ', async () => { + const Time = (await import('@components/Time/index.astro')).default + + const renderedHtml = await container.renderToString(Time, { + props: { + format: 'relative', + datetime: '2020-01-01T00:00:00Z', + tense: 'past', + precision: 'day', + threshold: 'P1D', + prefix: 'about', + }, + }) + + await withJsdomEnvironment(async ({ window }) => { + window.document.body.innerHTML = renderedHtml + + const element = window.document.querySelector('relative-time') + expect(element?.getAttribute('format')).toBe('relative') + expect(element?.getAttribute('tense')).toBe('past') + expect(element?.getAttribute('precision')).toBe('day') + expect(element?.getAttribute('threshold')).toBe('P1D') + expect(element?.getAttribute('prefix')).toBe('about') + }) + }) +}) diff --git a/src/components/Time/index.astro b/src/components/Time/index.astro new file mode 100644 index 000000000..a95774d23 --- /dev/null +++ b/src/components/Time/index.astro @@ -0,0 +1,41 @@ +--- +type Common = { + format: 'datetime'|'relative'|'duration'; + tense?: 'auto'|'past'|'future'; + precision?: 'year'|'month'|'day'|'hour'|'minute'|'second'; + threshold?: string; + prefix?: string; + formatStyle?: 'long'|'short'|'narrow'; + second?: 'numeric'|'2-digit'; + minute?: 'numeric'|'2-digit'; + hour?: 'numeric'|'2-digit'; + weekday?: 'short'|'long'|'narrow'; + day?: 'numeric'|'2-digit'; + month?: 'numeric'|'2-digit'|'short'|'long'|'narrow'; + year?: 'numeric'|'2-digit'; + timeZoneName?: 'long'|'short'|'shortOffset'|'longOffset' |'shortGeneric'|'longGeneric'; +} + +export interface Props extends Common { + datetime?: Date | string + now?: boolean +} + +const { now, datetime, formatStyle, timeZoneName, ...attrs } = Astro.props as Props +const resolvedDatetime = datetime ?? (now ? new Date() : undefined) +--- + + + + + + \ No newline at end of file diff --git a/src/content/articles/demo/index.mdx b/src/content/articles/demo/index.mdx index 2525a1fcb..c535ffbe2 100644 --- a/src/content/articles/demo/index.mdx +++ b/src/content/articles/demo/index.mdx @@ -225,6 +225,14 @@ Display customer feedback and testimonials: +### Time Component + +```markdown +Let's go to the disco