Skip to content

Commit b25a458

Browse files
KristinLBradleyalex-jushleewhiteandgen404jorytindall
authoredDec 9, 2024
Time - component (HDS-3945) (#2515)
Co-authored-by: Alex <alex-ju@users.noreply.github.com> Co-authored-by: Lee White <leewhite128@gmail.com> Co-authored-by: Andrew Gendel <124841193+andgen404@users.noreply.github.com> Co-authored-by: Jory Tindall <jory.tindall@hashicorp.com> Co-authored-by: Majed <156002572+majedelass@users.noreply.github.com>
1 parent c173c84 commit b25a458

File tree

38 files changed

+5536
-5386
lines changed

38 files changed

+5536
-5386
lines changed
 

‎.changeset/happy-readers-notice.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@hashicorp/design-system-components": minor
3+
---
4+
5+
`Time` - Added Time component, Time service, and related libraries including:
6+
- luxon (2.x or 3.x)
7+
- ember-concurrency (4.x)

‎packages/components/package.json

+10-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"ember-a11y-refocus": "^4.1.3",
4848
"ember-cli-sass": "^11.0.1",
4949
"ember-composable-helpers": "^5.0.0",
50+
"ember-concurrency": "^4.0.2",
5051
"ember-element-helper": "^0.8.5",
5152
"ember-focus-trap": "^1.1.0",
5253
"ember-get-config": "^2.1.1",
@@ -55,6 +56,7 @@
5556
"ember-stargate": "^0.4.3",
5657
"ember-style-modifier": "^4.4.0",
5758
"ember-truth-helpers": "^4.0.3",
59+
"luxon": "^2.3.2 || ^3.4.2",
5860
"prismjs": "^1.29.0",
5961
"sass": "^1.69.5",
6062
"tippy.js": "^6.3.7"
@@ -78,6 +80,7 @@
7880
"@types/ember-qunit": "^6.1.1",
7981
"@types/ember-resolver": "^9.0.0",
8082
"@types/ember__destroyable": "^4.0.5",
83+
"@types/luxon": "^3.2.0",
8184
"@types/prismjs": "^1.26.4",
8285
"@types/qunit": "^2.19.10",
8386
"@types/rsvp": "^4.0.9",
@@ -86,7 +89,6 @@
8689
"babel-plugin-ember-template-compilation": "^2.2.4",
8790
"concurrently": "^8.2.2",
8891
"ember-basic-dropdown": "^8.1.0",
89-
"ember-concurrency": "^4.0.2",
9092
"ember-source": "~5.9.0",
9193
"ember-template-lint": "^6.0.0",
9294
"ember-template-lint-plugin-prettier": "^5.0.0",
@@ -286,16 +288,22 @@
286288
"./components/hds/text/code.js": "./dist/_app_/components/hds/text/code.js",
287289
"./components/hds/text/display.js": "./dist/_app_/components/hds/text/display.js",
288290
"./components/hds/text/index.js": "./dist/_app_/components/hds/text/index.js",
291+
"./components/hds/time/index.js": "./dist/_app_/components/hds/time/index.js",
292+
"./components/hds/time/range.js": "./dist/_app_/components/hds/time/range.js",
293+
"./components/hds/time/single.js": "./dist/_app_/components/hds/time/single.js",
289294
"./components/hds/toast/index.js": "./dist/_app_/components/hds/toast/index.js",
290295
"./components/hds/tooltip-button/index.js": "./dist/_app_/components/hds/tooltip-button/index.js",
291296
"./components/hds/yield/index.js": "./dist/_app_/components/hds/yield/index.js",
297+
"./helpers/hds-format-date.js": "./dist/_app_/helpers/hds-format-date.js",
298+
"./helpers/hds-format-relative.js": "./dist/_app_/helpers/hds-format-relative.js",
292299
"./helpers/hds-link-to-models.js": "./dist/_app_/helpers/hds-link-to-models.js",
293300
"./helpers/hds-link-to-query.js": "./dist/_app_/helpers/hds-link-to-query.js",
294301
"./instance-initializers/load-sprite.js": "./dist/_app_/instance-initializers/load-sprite.js",
295302
"./modifiers/hds-anchored-position.js": "./dist/_app_/modifiers/hds-anchored-position.js",
296303
"./modifiers/hds-clipboard.js": "./dist/_app_/modifiers/hds-clipboard.js",
297304
"./modifiers/hds-register-event.js": "./dist/_app_/modifiers/hds-register-event.js",
298-
"./modifiers/hds-tooltip.js": "./dist/_app_/modifiers/hds-tooltip.js"
305+
"./modifiers/hds-tooltip.js": "./dist/_app_/modifiers/hds-tooltip.js",
306+
"./services/hds-time.js": "./dist/_app_/services/hds-time.js"
299307
}
300308
},
301309
"exports": {

‎packages/components/rollup.config.mjs

+8-5
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ const plugins = [
2727
// These are the modules that should get reexported into the traditional
2828
// "app" tree. Things in here should also be in publicEntrypoints above, but
2929
// not everything in publicEntrypoints necessarily needs to go here.
30-
addon.appReexports([
31-
'components/**/!(*types).js',
32-
'helpers/**/*.js',
33-
'modifiers/**/*.js',
34-
'instance-initializers/**/*.js'],
30+
addon.appReexports(
31+
[
32+
'components/**/!(*types).js',
33+
'helpers/**/*.js',
34+
'modifiers/**/*.js',
35+
'services/**/!(*types).js',
36+
'instance-initializers/**/*.js',
37+
],
3538
{
3639
exclude: [
3740
'components/**/app-header/**/*.js',

‎packages/components/src/components.ts

+6
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@ export { default as HdsTextCode } from './components/hds/text/code.ts';
270270
export { default as HdsTextDisplay } from './components/hds/text/display.ts';
271271
export * from './components/hds/text/types.ts';
272272

273+
// Time
274+
export { default as HdsTime } from './components/hds/time/index.ts';
275+
export { default as HdsTimeSingle } from './components/hds/time/single.ts';
276+
export { default as HdsTimeRange } from './components/hds/time/range.ts';
277+
export * from './services/hds-time-types.ts';
278+
273279
// Toast
274280
export { default as HdsToast } from './components/hds/toast/index.ts';
275281

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{{!
2+
Copyright (c) HashiCorp, Inc.
3+
SPDX-License-Identifier: MPL-2.0
4+
}}
5+
{{! IMPORTANT: we need to add "squishies" here (~) because otherwise the whitespace added by Ember causes extra space around the time element - See https://handlebarsjs.com/guide/expressions.html#whitespace-control }}
6+
{{~#let this.display as |display|~}}
7+
{{~#if this.isValidDate~}}
8+
{{~#if this.hasTooltip~}}
9+
<Hds::TooltipButton
10+
class="hds-time-wrapper"
11+
@text={{if
12+
display.options.tooltipFormat
13+
(hds-format-date this.date display.options.tooltipFormat)
14+
this.isoUtcString
15+
}}
16+
@placement="bottom"
17+
@extraTippyOptions={{hash showOnCreate=this.isOpen}}
18+
>
19+
<Hds::Time::Single
20+
@date={{this.date}}
21+
@isoUtcString={{this.isoUtcString}}
22+
@display={{this.display}}
23+
@register={{this.didInsertNode}}
24+
@unregister={{this.willDestroyNode}}
25+
...attributes
26+
/>
27+
</Hds::TooltipButton>
28+
{{~else~}}
29+
<Hds::Time::Single
30+
@date={{this.date}}
31+
@isoUtcString={{this.isoUtcString}}
32+
@display={{this.display}}
33+
@register={{this.didInsertNode}}
34+
@unregister={{this.willDestroyNode}}
35+
...attributes
36+
/>
37+
{{~/if~}}
38+
{{~else if this.isValidDateRange~}}
39+
{{~#if this.hasTooltip~}}
40+
<Hds::TooltipButton
41+
class="hds-time-wrapper"
42+
@text={{if
43+
display.options.tooltipFormat
44+
(concat
45+
(hds-format-date this.startDate display.options.tooltipFormat)
46+
(hds-format-date this.endDate display.options.tooltipFormat)
47+
)
48+
this.rangeIsoUtcString
49+
}}
50+
@placement="bottom"
51+
@extraTippyOptions={{hash showOnCreate=this.isOpen}}
52+
>
53+
<Hds::Time::Range @startDate={{this.startDate}} @endDate={{this.endDate}} ...attributes />
54+
</Hds::TooltipButton>
55+
{{~else~}}
56+
<Hds::Time::Range @startDate={{this.startDate}} @endDate={{this.endDate}} ...attributes />
57+
{{~/if~}}
58+
{{~/if~}}
59+
{{~/let~}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import Component from '@glimmer/component';
7+
import { typeOf } from '@ember/utils';
8+
import { inject as service } from '@ember/service';
9+
import { action } from '@ember/object';
10+
import type { DisplayType } from '../../../services/hds-time-types.ts';
11+
12+
import type TimeService from '../../../services/hds-time';
13+
14+
export interface HdsTimeSignature {
15+
Args: {
16+
date?: Date | string;
17+
startDate?: Date | string;
18+
endDate?: Date | string;
19+
display?:
20+
| 'utc'
21+
| 'relative'
22+
| 'friendly-only'
23+
| 'friendly-local'
24+
| 'friendly-relative';
25+
isOpen?: boolean;
26+
hasTooltip?: boolean;
27+
isoUtcString?: string;
28+
};
29+
Element: HTMLElement;
30+
}
31+
32+
const dateIsValid = (date?: Date | string): date is Date =>
33+
date instanceof Date && !isNaN(+date);
34+
35+
export default class HdsTime extends Component<HdsTimeSignature> {
36+
@service declare readonly hdsTime: TimeService;
37+
38+
get date(): Date | undefined {
39+
const { date } = this.args;
40+
41+
// Sometimes an ISO date string might be passed in instead of a JS Date.
42+
if (date) {
43+
if (typeOf(date) === 'string') {
44+
return new Date(date);
45+
} else if (date instanceof Date) {
46+
return date;
47+
}
48+
}
49+
}
50+
51+
get startDate(): Date | undefined {
52+
const { startDate } = this.args;
53+
54+
if (startDate) {
55+
if (typeOf(startDate) === 'string') {
56+
return new Date(startDate);
57+
} else if (startDate instanceof Date) {
58+
return startDate;
59+
}
60+
}
61+
}
62+
63+
get endDate(): Date | undefined {
64+
const { endDate } = this.args;
65+
66+
if (endDate) {
67+
if (typeOf(endDate) === 'string') {
68+
return new Date(endDate);
69+
} else if (endDate instanceof Date) {
70+
return endDate;
71+
}
72+
}
73+
}
74+
75+
get isValidDate(): boolean {
76+
return dateIsValid(this.date);
77+
}
78+
79+
get isValidDateRange(): boolean {
80+
if (dateIsValid(this.startDate) && dateIsValid(this.endDate)) {
81+
return this.startDate <= this.endDate;
82+
}
83+
return false;
84+
}
85+
86+
get hasTooltip(): boolean {
87+
return this.args.hasTooltip ?? true;
88+
}
89+
90+
get isoUtcString(): string | undefined {
91+
const date = this.date;
92+
93+
if (dateIsValid(date)) {
94+
return this.hdsTime.toIsoUtcString(date);
95+
}
96+
97+
return undefined;
98+
}
99+
100+
get rangeIsoUtcString(): string {
101+
const startDate = this.startDate;
102+
const endDate = this.endDate;
103+
104+
if (dateIsValid(startDate) && dateIsValid(endDate)) {
105+
return `${this.hdsTime.toIsoUtcString(startDate)}${this.hdsTime.toIsoUtcString(endDate)}`;
106+
}
107+
return '';
108+
}
109+
110+
get display(): DisplayType {
111+
const date = this.date;
112+
const { display } = this.args;
113+
114+
if (dateIsValid(date)) {
115+
const nextDiff = this.hdsTime.timeDifference(this.hdsTime.now, date);
116+
return this.hdsTime.format(nextDiff, display);
117+
}
118+
return {
119+
options: undefined,
120+
difference: { absValueInMs: 0, valueInMs: 0 },
121+
relative: { value: 0, unit: '' },
122+
};
123+
}
124+
125+
get isOpen(): boolean {
126+
return this.args.isOpen ?? false;
127+
}
128+
129+
@action
130+
didInsertNode(): void {
131+
const date = this.date;
132+
133+
if (dateIsValid(date)) {
134+
this.hdsTime.register(date);
135+
}
136+
}
137+
138+
@action
139+
willDestroyNode(): void {
140+
const date = this.date;
141+
142+
if (dateIsValid(date)) {
143+
this.hdsTime.unregister(date);
144+
}
145+
}
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{!
2+
Copyright (c) HashiCorp, Inc.
3+
SPDX-License-Identifier: MPL-2.0
4+
}}
5+
{{! IMPORTANT: we need to add "squishies" here (~) because otherwise the whitespace added by Ember causes extra space around the time element - See https://handlebarsjs.com/guide/expressions.html#whitespace-control }}
6+
<span class="hds-time hds-time--range" ...attributes>
7+
<time datetime={{this.startDateIsoUtcString}}>
8+
{{~hds-format-date @startDate this.startDateDisplayFormat~}}
9+
</time>
10+
11+
<time datetime={{this.endDateIsoUtcString}}>
12+
{{~hds-format-date @endDate this.endDateDisplayFormat~}}
13+
</time>
14+
</span>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import Component from '@glimmer/component';
7+
import { inject as service } from '@ember/service';
8+
import type TimeService from '../../../services/hds-time';
9+
10+
export interface HdsTimeRangeSignature {
11+
Args: {
12+
startDate?: Date;
13+
endDate?: Date;
14+
};
15+
Element: HTMLElement;
16+
}
17+
18+
export default class HdsTimeRange extends Component<HdsTimeRangeSignature> {
19+
@service declare readonly hdsTime: TimeService;
20+
21+
get startDateIsoUtcString(): string | undefined {
22+
const { startDate } = this.args;
23+
if (startDate) {
24+
return this.hdsTime.toIsoUtcString(startDate);
25+
}
26+
}
27+
28+
get endDateIsoUtcString(): string | undefined {
29+
const { endDate } = this.args;
30+
if (endDate) {
31+
return this.hdsTime.toIsoUtcString(endDate);
32+
}
33+
}
34+
35+
get startDateDisplayFormat(): {
36+
month: Intl.DateTimeFormatOptions['month'];
37+
day: Intl.DateTimeFormatOptions['day'];
38+
year?: Intl.DateTimeFormatOptions['year'];
39+
hour?: Intl.DateTimeFormatOptions['hour'];
40+
minute?: Intl.DateTimeFormatOptions['minute'];
41+
second?: Intl.DateTimeFormatOptions['second'];
42+
} {
43+
const { startDate, endDate } = this.args;
44+
45+
if (startDate?.getFullYear() !== endDate?.getFullYear()) {
46+
return {
47+
month: 'short',
48+
day: 'numeric',
49+
year: 'numeric',
50+
};
51+
} else {
52+
return {
53+
month: 'short',
54+
day: 'numeric',
55+
year: undefined,
56+
};
57+
}
58+
}
59+
60+
get endDateDisplayFormat(): {
61+
month: Intl.DateTimeFormatOptions['month'];
62+
day: Intl.DateTimeFormatOptions['day'];
63+
year?: Intl.DateTimeFormatOptions['year'];
64+
hour?: Intl.DateTimeFormatOptions['hour'];
65+
minute?: Intl.DateTimeFormatOptions['minute'];
66+
second?: Intl.DateTimeFormatOptions['second'];
67+
} {
68+
return {
69+
month: 'short',
70+
day: 'numeric',
71+
year: 'numeric',
72+
};
73+
}
74+
}

0 commit comments

Comments
 (0)