Skip to content

Commit

Permalink
HDS-3945 Implement initial Time component based on Cloud-ui and add S…
Browse files Browse the repository at this point in the history
…howcase
  • Loading branch information
KristinLBradley committed Oct 24, 2024
1 parent 6fe63c5 commit 880c9b4
Show file tree
Hide file tree
Showing 13 changed files with 979 additions and 4 deletions.
5 changes: 5 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@
"ember-element-helper": "^0.8.5",
"ember-focus-trap": "^1.1.0",
"ember-get-config": "^2.1.1",
"ember-intl": "^6.5.3",
"ember-modifier": "^4.1.0",
"ember-power-select": "^8.2.0",
"ember-stargate": "^0.4.3",
"ember-style-modifier": "^4.4.0",
"ember-truth-helpers": "^4.0.3",
"luxon": "^3.4.2",
"prismjs": "^1.29.0",
"sass": "^1.69.5",
"tippy.js": "^6.3.7"
Expand All @@ -77,6 +79,7 @@
"@types/ember-qunit": "^6.1.1",
"@types/ember-resolver": "^9.0.0",
"@types/ember__destroyable": "^4.0.5",
"@types/luxon": "^3.2.0",
"@types/prismjs": "^1.26.4",
"@types/qunit": "^2.19.10",
"@types/rsvp": "^4.0.9",
Expand Down Expand Up @@ -286,6 +289,8 @@
"./components/hds/text/code.js": "./dist/_app_/components/hds/text/code.js",
"./components/hds/text/display.js": "./dist/_app_/components/hds/text/display.js",
"./components/hds/text/index.js": "./dist/_app_/components/hds/text/index.js",
"./components/hds/time/index.js": "./dist/_app_/components/hds/time/index.js",
"./components/hds/time/inner.js": "./dist/_app_/components/hds/time/inner.js",
"./components/hds/toast/index.js": "./dist/_app_/components/hds/toast/index.js",
"./components/hds/tooltip-button/index.js": "./dist/_app_/components/hds/tooltip-button/index.js",
"./components/hds/yield/index.js": "./dist/_app_/components/hds/yield/index.js",
Expand Down
44 changes: 44 additions & 0 deletions packages/components/src/components/hds/time/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

{{! @glint-nocheck }}
{{! TODO: format-date & display have type errors, fix }}
{{#let this.display as |display|}}
{{#if (and this.hasTooltip this.isValid)}}
<Hds::TooltipButton
@text={{if
display.options.tooltipFormat
(format-date
this.date
month=display.options.tooltipFormat.month
day=display.options.tooltipFormat.day
year=display.options.tooltipFormat.year
hour=display.options.tooltipFormat.hour
minute=display.options.tooltipFormat.minute
second=display.options.tooltipFormat.second
)
this.isoUtcString
}}
@placement="bottom"
@extraTippyOptions={{hash showOnCreate=this.isOpen}}
>
<Hds::Time::Inner
@date={{this.date}}
@isoUtcString={{this.isoUtcString}}
@display={{this.display}}
@isValid={{this.isValid}}
...attributes
/>
</Hds::TooltipButton>
{{else}}
<Hds::Time::Inner
@date={{this.date}}
@isoUtcString={{this.isoUtcString}}
@display={{this.display}}
@isValid={{this.isValid}}
...attributes
/>
{{/if}}
{{/let}}
279 changes: 279 additions & 0 deletions packages/components/src/components/hds/time/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';
import { typeOf } from '@ember/utils';
import { DateTime } from 'luxon';

const MILLISECOND_IN_MS = 1;
const SECOND_IN_MS = 1000 * MILLISECOND_IN_MS;
const MINUTE_IN_MS = 60 * SECOND_IN_MS;
const HOUR_IN_MS = 60 * MINUTE_IN_MS;
const DAY_IN_MS = 24 * HOUR_IN_MS;
const WEEK_IN_MS = 7 * DAY_IN_MS;

const THRESHOLD_RELATIVE_TIME_IN_MS = WEEK_IN_MS;

const RELATIVE_UNIT_SECOND = 'second';
const RELATIVE_UNIT_HOUR = 'hour';
const RELATIVE_UNIT_MINUTE = 'minute';
const RELATIVE_UNIT_DAY = 'day';
const RELATIVE_UNIT_WEEK = 'week';

const DEFAULT_RELATIVE_THRESHOLDS = {
[RELATIVE_UNIT_SECOND]: 1 * MINUTE_IN_MS,
[RELATIVE_UNIT_MINUTE]: 1 * HOUR_IN_MS,
[RELATIVE_UNIT_HOUR]: 1 * DAY_IN_MS,
[RELATIVE_UNIT_DAY]: 100 * WEEK_IN_MS,
};

// returns 'Sep 5, 2018 (30 minutes ago)'
const DISPLAY_KEY_FRIENDLY_RELATIVE = 'friendly-relative';

// returns 'Sep 5, 2018, 4:07:32 pm'
const DISPLAY_KEY_FRIENDLY_LOCAL = 'friendly-local';

// returns 'Sep 5, 2018'
const DISPLAY_KEY_FRIENDLY_ONLY = 'friendly-only';

// returns 'about 2 hours ago'
const DISPLAY_KEY_RELATIVE = 'relative';

// returns '2018-09-05T23:15:17345Z'
const DISPLAY_KEY_UTC = 'utc';

const FORMAT_PRECISION_SHORT_DATE = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const FORMAT_PRECISION_MINUTE = {
...FORMAT_PRECISION_SHORT_DATE,
hour: 'numeric',
minute: 'numeric',
};
const FORMAT_PRECISION_SECOND = {
...FORMAT_PRECISION_SHORT_DATE,
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
};
const DATE_DISPLAY_FORMATS = {
[DISPLAY_KEY_FRIENDLY_LOCAL]: FORMAT_PRECISION_SECOND,
[DISPLAY_KEY_FRIENDLY_ONLY]: FORMAT_PRECISION_SHORT_DATE,
};

const DEFAULT_DISPLAY = '';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const DEFAULT_DISPLAY_MAPPING: any = {
[DISPLAY_KEY_FRIENDLY_RELATIVE]: {
displayFormat: FORMAT_PRECISION_SHORT_DATE,
showFriendly: true,
showRelative: true,
tooltipFormat: FORMAT_PRECISION_SECOND,
},
[DISPLAY_KEY_FRIENDLY_LOCAL]: {
displayFormat: DATE_DISPLAY_FORMATS[DISPLAY_KEY_FRIENDLY_LOCAL],
showFriendly: true,
showRelative: false,
tooltipFormat: null,
},
[DISPLAY_KEY_FRIENDLY_ONLY]: {
displayFormat: DATE_DISPLAY_FORMATS[DISPLAY_KEY_FRIENDLY_ONLY],
showFriendly: true,
showRelative: false,
tooltipFormat: null,
},
[DISPLAY_KEY_RELATIVE]: {
displayFormat: null,
showFriendly: false,
showRelative: true,
tooltipFormat: FORMAT_PRECISION_MINUTE,
},
[DISPLAY_KEY_UTC]: {
displayFormat: null,
showFriendly: true,
showRelative: false,
tooltipFormat: null,
},
};
const DISPLAY_SCALE = Object.keys(DEFAULT_DISPLAY_MAPPING);

export const DISPLAYS: string[] = [
DISPLAY_KEY_FRIENDLY_RELATIVE,
DISPLAY_KEY_FRIENDLY_LOCAL,
DISPLAY_KEY_FRIENDLY_ONLY,
DISPLAY_KEY_RELATIVE,
DISPLAY_KEY_UTC,
];

export interface HdsTimeSignature {
Args: {
date: Date | string | undefined;
display?: string;
isOpen?: boolean;
hasTooltip?: boolean;
};
Element: HTMLElement;
}

const dateIsValid = (date?: Date | string): date is Date =>
date instanceof Date && !isNaN(+date);

export default class HdsTime extends Component<HdsTimeSignature> {
now = Date.now();

format(
difference: { absValueInMs: number; valueInMs: number },
display: string = DEFAULT_DISPLAY
): {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any;
difference: { absValueInMs: number; valueInMs: number };
relative: { value: number; unit: string };
} {
let displayKey: string;

// If the scale display is defined and valid then set that display.
if (display && DISPLAY_SCALE.includes(display)) {
displayKey = display;
} else {
// If there's no defined display then we will execute the design system's
// prefered algorithm.

// By default, we assume we will display just a relative display only.
displayKey = DISPLAY_KEY_RELATIVE;

// If the difference in date is greater than the threshold for showing the
// relative time then switch the display key.
if (difference.absValueInMs > THRESHOLD_RELATIVE_TIME_IN_MS) {
displayKey = DISPLAY_KEY_FRIENDLY_LOCAL;
}
}

// TODO: Not sure how to determine type (defined as "any" for now)
const options = DEFAULT_DISPLAY_MAPPING[displayKey];

return {
options,
difference,
relative: this.selectTimeRelativeUnit(difference),
};
}

// Formats the value of a relative unit.
formatTimeRelativeUnit(
value: number,
unit: string
): { value: number; unit: string } {
return {
value: Math.trunc(value),
unit,
};
}

// Selects an appropriate display format for the difference.
selectTimeRelativeUnit(
{ absValueInMs, valueInMs }: { absValueInMs: number; valueInMs: number },
thresholds = DEFAULT_RELATIVE_THRESHOLDS
): { value: number; unit: string } {
if (absValueInMs < thresholds[RELATIVE_UNIT_SECOND]) {
return this.formatTimeRelativeUnit(
valueInMs / SECOND_IN_MS,
RELATIVE_UNIT_SECOND
);
}

if (absValueInMs < thresholds[RELATIVE_UNIT_MINUTE]) {
return this.formatTimeRelativeUnit(
valueInMs / MINUTE_IN_MS,
RELATIVE_UNIT_MINUTE
);
}

if (absValueInMs < thresholds[RELATIVE_UNIT_HOUR]) {
return this.formatTimeRelativeUnit(
valueInMs / HOUR_IN_MS,
RELATIVE_UNIT_HOUR
);
}

if (absValueInMs < thresholds[RELATIVE_UNIT_DAY]) {
return this.formatTimeRelativeUnit(
valueInMs / DAY_IN_MS,
RELATIVE_UNIT_DAY
);
}

return this.formatTimeRelativeUnit(
valueInMs / WEEK_IN_MS,
RELATIVE_UNIT_WEEK
);
}

// Gets the currently subscribed listeners.
timeDifference(
startDate: Date | number,
endDate: Date | number
): { absValueInMs: number; valueInMs: number } {
const valueInMs = Number(endDate) - Number(startDate);
return {
absValueInMs: Math.abs(valueInMs),
valueInMs,
};
}

get date(): string | Date | undefined {
const { date } = this.args;

// Sometimes an ISO date string might be passed in instead of a JS Date.
if (typeOf(date) === 'string') {
return new Date(date as string);
}
return date;
}

get isValid(): boolean {
return dateIsValid(this.date);
}

get hasTooltip(): boolean {
return this.args.hasTooltip ?? true;
}

get isoUtcString(): string {
const date = this.date;

// if (dateIsValid(date)) return this.time.toIsoUtcString(date);
if (dateIsValid(date)) {
return DateTime.fromJSDate(date).toUTC().toJSDate().toISOString();
}
return '';
}

get display(): {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any;
difference: { absValueInMs: number; valueInMs: number };
relative: { value: number; unit: string };
} {
const date = this.date;
const { display } = this.args;
if (dateIsValid(date)) {
const nextDiff = this.timeDifference(this.now, date);
return this.format(nextDiff, display);
}
return {
options: {},
difference: { absValueInMs: 0, valueInMs: 0 },
relative: { value: 0, unit: '' },
};
}

get isOpen(): boolean {
return this.args.isOpen ?? false;
}
}
41 changes: 41 additions & 0 deletions packages/components/src/components/hds/time/inner.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

{{! @glint-nocheck: not typesafe yet }}
{{! TODO: format-date & format-relative have type errors, fix }}
{{#if @isValid}}
<time class="hds-time" datetime={{@isoUtcString}} ...attributes>
{{#if @display.options.showFriendly}}
<span data-test-time-friendly>
{{#if @display.options.displayFormat}}
{{format-date
@date
month=@display.options.displayFormat.month
day=@display.options.displayFormat.day
year=@display.options.displayFormat.year
hour=@display.options.displayFormat.hour
minute=@display.options.displayFormat.minute
second=@display.options.displayFormat.second
}}
{{else}}
{{@isoUtcString}}
{{/if}}
</span>
{{#if @display.options.showRelative}}
<span data-test-time-relative>
({{format-relative @display.relative.value unit=@display.relative.unit}})
</span>
{{/if}}
{{else}}
{{#if @display.options.showRelative}}
<span data-test-time-relative>
{{format-relative @display.relative.value unit=@display.relative.unit}}
</span>
{{/if}}
{{/if}}
</time>
{{else}}
<span data-test-time-invalid>--</span>
{{/if}}
Loading

0 comments on commit 880c9b4

Please sign in to comment.