Skip to content

Commit 32b398b

Browse files
authored
Merge pull request #514 from webstackdev/feature/relative-time-component
Feature/relative time component
2 parents fdb6926 + 9d3220f commit 32b398b

9 files changed

Lines changed: 190 additions & 6 deletions

File tree

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
test/e2e/__screenshots__/** filter=lfs diff=lfs merge=lfs -text
2+
.github/* linguist-vendored=false
3+
.astro/** linguist-vendored

.github/codeql/codeql-config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
name: CodeQL config
22

3-
# Unavoidable checkoutin untrusted environment to use
3+
# Unavoidable checkout in untrusted environment to use
44
# "astro db verify" and "astro db push --remote" commands
5+
# @see PR https://github.com/withastro/astro/pull/15069
56
paths-ignore:
67
- .github/workflows/deployment-preview.yml
78
- .github/workflows/deployment-production.yml

_TODO.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,6 @@ Google Calendar, Apple Calendar, Yahoo Calender, Microsoft 365, Outlook, and T
229229

230230
[Relative Time](https://github.com/BryceRussell/astro-github-elements/tree/main/packages/time#readme)
231231

232-
### Display text in a circular layout
233-
234-
[TextCircle](https://github.com/LoStisWorld/astro-textcircle#astro-textcircle)
235-
236232
## Markdown
237233

238234
## Code Block and Highlighting

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"@astrojs/sitemap": "^3.6.0",
6868
"@astrojs/vercel": "^9.0.2",
6969
"@axe-core/playwright": "^4.11.0",
70+
"@github/relative-time-element": "^5.0.0",
7071
"@glidejs/glide": "^3.7.1",
7172
"@googlemaps/extended-component-library": "^0.6.14",
7273
"@nanostores/lit": "^0.2.3",
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// @vitest-environment node
2+
3+
import { beforeEach, describe, expect, test, vi } from 'vitest'
4+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
5+
import { withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
6+
7+
describe('Time (Astro)', () => {
8+
let container: AstroContainer
9+
10+
beforeEach(async () => {
11+
container = await AstroContainer.create()
12+
})
13+
14+
test('renders a <relative-time> element and keeps slot fallback content', async () => {
15+
const Time = (await import('@components/Time/index.astro')).default
16+
17+
const renderedHtml = await container.renderToString(Time, {
18+
props: {
19+
format: 'relative',
20+
datetime: '2020-01-01T00:00:00Z',
21+
},
22+
slots: {
23+
default: 'Fallback text',
24+
},
25+
})
26+
27+
await withJsdomEnvironment(async ({ window }) => {
28+
window.document.body.innerHTML = renderedHtml
29+
30+
const element = window.document.querySelector('relative-time')
31+
expect(element).toBeTruthy()
32+
expect(element?.getAttribute('datetime')).toBe('2020-01-01T00:00:00Z')
33+
expect(element?.textContent).toContain('Fallback text')
34+
})
35+
})
36+
37+
test('serializes Date datetime to ISO string', async () => {
38+
const Time = (await import('@components/Time/index.astro')).default
39+
const date = new Date('2024-02-29T12:34:56.000Z')
40+
41+
const renderedHtml = await container.renderToString(Time, {
42+
props: {
43+
format: 'datetime',
44+
datetime: date,
45+
},
46+
})
47+
48+
await withJsdomEnvironment(async ({ window }) => {
49+
window.document.body.innerHTML = renderedHtml
50+
51+
const element = window.document.querySelector('relative-time')
52+
expect(element?.getAttribute('datetime')).toBe('2024-02-29T12:34:56.000Z')
53+
})
54+
})
55+
56+
test('uses current time when now=true and datetime is omitted', async () => {
57+
vi.useFakeTimers()
58+
vi.setSystemTime(new Date('2030-01-02T03:04:05.000Z'))
59+
60+
try {
61+
const Time = (await import('@components/Time/index.astro')).default
62+
63+
const renderedHtml = await container.renderToString(Time, {
64+
props: {
65+
format: 'relative',
66+
now: true,
67+
},
68+
})
69+
70+
await withJsdomEnvironment(async ({ window }) => {
71+
window.document.body.innerHTML = renderedHtml
72+
73+
const element = window.document.querySelector('relative-time')
74+
expect(element?.getAttribute('datetime')).toBe('2030-01-02T03:04:05.000Z')
75+
})
76+
} finally {
77+
vi.useRealTimers()
78+
}
79+
})
80+
81+
test('maps formatStyle and timeZoneName to kebab-case attributes', async () => {
82+
const Time = (await import('@components/Time/index.astro')).default
83+
84+
const renderedHtml = await container.renderToString(Time, {
85+
props: {
86+
format: 'datetime',
87+
datetime: '2020-01-01T00:00:00Z',
88+
formatStyle: 'short',
89+
timeZoneName: 'shortOffset',
90+
},
91+
})
92+
93+
await withJsdomEnvironment(async ({ window }) => {
94+
window.document.body.innerHTML = renderedHtml
95+
96+
const element = window.document.querySelector('relative-time')
97+
expect(element?.getAttribute('format-style')).toBe('short')
98+
expect(element?.getAttribute('time-zone-name')).toBe('shortOffset')
99+
})
100+
})
101+
102+
test('passes through supported attributes to <relative-time>', async () => {
103+
const Time = (await import('@components/Time/index.astro')).default
104+
105+
const renderedHtml = await container.renderToString(Time, {
106+
props: {
107+
format: 'relative',
108+
datetime: '2020-01-01T00:00:00Z',
109+
tense: 'past',
110+
precision: 'day',
111+
threshold: 'P1D',
112+
prefix: 'about',
113+
},
114+
})
115+
116+
await withJsdomEnvironment(async ({ window }) => {
117+
window.document.body.innerHTML = renderedHtml
118+
119+
const element = window.document.querySelector('relative-time')
120+
expect(element?.getAttribute('format')).toBe('relative')
121+
expect(element?.getAttribute('tense')).toBe('past')
122+
expect(element?.getAttribute('precision')).toBe('day')
123+
expect(element?.getAttribute('threshold')).toBe('P1D')
124+
expect(element?.getAttribute('prefix')).toBe('about')
125+
})
126+
})
127+
})

src/components/Time/index.astro

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
type Common = {
3+
format: 'datetime'|'relative'|'duration';
4+
tense?: 'auto'|'past'|'future';
5+
precision?: 'year'|'month'|'day'|'hour'|'minute'|'second';
6+
threshold?: string;
7+
prefix?: string;
8+
formatStyle?: 'long'|'short'|'narrow';
9+
second?: 'numeric'|'2-digit';
10+
minute?: 'numeric'|'2-digit';
11+
hour?: 'numeric'|'2-digit';
12+
weekday?: 'short'|'long'|'narrow';
13+
day?: 'numeric'|'2-digit';
14+
month?: 'numeric'|'2-digit'|'short'|'long'|'narrow';
15+
year?: 'numeric'|'2-digit';
16+
timeZoneName?: 'long'|'short'|'shortOffset'|'longOffset' |'shortGeneric'|'longGeneric';
17+
}
18+
19+
export interface Props extends Common {
20+
datetime?: Date | string
21+
now?: boolean
22+
}
23+
24+
const { now, datetime, formatStyle, timeZoneName, ...attrs } = Astro.props as Props
25+
const resolvedDatetime = datetime ?? (now ? new Date() : undefined)
26+
---
27+
28+
<script>
29+
import '@github/relative-time-element'
30+
</script>
31+
32+
<relative-time
33+
datetime={resolvedDatetime instanceof Date ? resolvedDatetime.toISOString() : resolvedDatetime}
34+
{...{
35+
...attrs,
36+
'format-style':formatStyle,
37+
'time-zone-name':timeZoneName,
38+
}}
39+
>
40+
<slot />
41+
</relative-time>

src/content/articles/demo/index.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ Display customer feedback and testimonials:
225225

226226
<Testimonials count={3} />
227227

228+
### Time Component
229+
230+
```markdown
231+
Let's go to the disco <Time datetime="2026-01-01T00:00:00.000Z" />.
232+
```
233+
234+
Let's go to the disco <Time datetime="2026-01-01T00:00:00.000Z" />.
235+
228236
## GFM
229237

230238
### Footnote

src/layouts/MarkdownLayout.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ import Icon from '@components/Icon/index.astro'
2424
import MastodonModal from '@components/Social/Mastodon/index.astro'
2525
import Newsletter from '@components/CallToAction/Newsletter/index.astro'
2626
import Shares from '@components/Social/Shares/index.astro'
27+
import Time from '@components/Time/index.astro'
2728
import Testimonials from '@components/Testimonials/index.astro'
2829
2930
/** Export components for use in MDX */
30-
const Components = { Avatar, Callout, Carousel, Contact, Embed, Featured, Highlighter, Icon, Image, MastodonModal, Newsletter, Picture, Shares, Testimonials }
31+
const Components = { Avatar, Callout, Carousel, Contact, Embed, Featured, Highlighter, Icon, Image, MastodonModal, Newsletter, Picture, Shares, Testimonials, Time }
3132
3233
export interface Props {
3334
/** Author name */

0 commit comments

Comments
 (0)