diff --git a/packages/elemental-theme/src/custom-elements/ef-clock.less b/packages/elemental-theme/src/custom-elements/ef-clock.less index 037f624a4b..8c7b4fbb94 100644 --- a/packages/elemental-theme/src/custom-elements/ef-clock.less +++ b/packages/elemental-theme/src/custom-elements/ef-clock.less @@ -34,7 +34,7 @@ } [part='increment-button'], [part='decrement-button'] { - left: 0px; + left: 0; width: 100%; height: 50%; position: absolute; @@ -54,20 +54,20 @@ } &[part='increment-button'] { - top: 0px; + top: 0; &::after { - top: 0px; + top: 0; border-width: 0 0.167em 0.178em 0.167em; border-color: transparent transparent @scheme-color-primary transparent; } } &[part='decrement-button'] { - bottom: 0px; + bottom: 0; &::after { - bottom: 0px; + bottom: 0; border-width: 0.178em 0.167em 0 0.167em; border-color: @scheme-color-primary transparent transparent transparent; } @@ -175,7 +175,7 @@ margin-top: 23%; } } - + [part~='am-pm'] { padding-left: 0; display: flex; diff --git a/packages/elements/src/clock/index.ts b/packages/elements/src/clock/index.ts index ab8d780591..bd075ae529 100644 --- a/packages/elements/src/clock/index.ts +++ b/packages/elements/src/clock/index.ts @@ -13,9 +13,9 @@ import { customElement } from '@refinitiv-ui/core/decorators/custom-element.js'; import { property } from '@refinitiv-ui/core/decorators/property.js'; import { query } from '@refinitiv-ui/core/decorators/query.js'; import { state } from '@refinitiv-ui/core/decorators/state.js'; -import { ifDefined } from '@refinitiv-ui/core/directives/if-defined.js'; import { VERSION } from '../version.js'; import { ref, createRef, Ref } from '@refinitiv-ui/core/directives/ref.js'; +import '@refinitiv-ui/phrasebook/locale/en/clock.js'; import { MILLISECONDS_IN_SECOND, @@ -24,16 +24,21 @@ import { toTimeSegment, TimeFormat, format, - padNumber -} from '@refinitiv-ui/utils/date.js'; - -import { + padNumber, HOURS_IN_DAY, MINUTES_IN_HOUR, + SECONDS_IN_MINUTE, SECONDS_IN_DAY, SECONDS_IN_HOUR, - SECONDS_IN_MINUTE -} from './utils/timestamps.js'; + isAM, + parse +} from '@refinitiv-ui/utils/date.js'; + +import { + translate, + TranslatePromise, + TranslatePropertyKey +} from '@refinitiv-ui/translate'; import { register, @@ -47,7 +52,8 @@ enum Direction { } enum Segment { HOURS = 'hours', - MINUTES = 'minutes' + MINUTES = 'minutes', + SECOND = 'seconds' } /** @@ -68,7 +74,7 @@ export class Clock extends ResponsiveElement { return VERSION; } - protected readonly defaultRole: string | null = 'group'; + protected readonly defaultRole: string | null = 'spinbutton'; /** * A `CSSResultGroup` that will be used @@ -144,6 +150,12 @@ export class Clock extends ResponsiveElement { @state() private activeSegment = Segment.HOURS; + /** + * Clock internal translation strings + */ + @translate({ mode: 'promise', scope: 'ef-clock' }) + protected tPromise!: TranslatePromise; + /** * Get time value in format `hh:mm:ss` * @default 00:00:00 @@ -234,27 +246,35 @@ export class Clock extends ResponsiveElement { /** * Display the digital clock in 12hr format. */ - @property({ type: Boolean, attribute: 'am-pm' }) + @property({ type: Boolean, attribute: 'am-pm', reflect: true }) public amPm = false; /** * Display the seconds segment. */ - @property({ type: Boolean, attribute: 'show-seconds' }) + @property({ type: Boolean, attribute: 'show-seconds', reflect: true }) public showSeconds = false; - /** - * Enabled interactive mode. Allowing the user to offset the value. - */ - @property({ type: Boolean }) - public interactive = false; - /** * Display clock in analogue style. */ @property({ type: Boolean, reflect: true }) public analogue = false; + /** + * Enable interactive mode. Allowing the user to offset the value. + * Set tabIndex >= 0 to make the clock interactive + * Set/remove tabIndex to move clock to non interactive mode + * @param interactive Set interactive mode + */ + @property({ type: Boolean }) + public set interactive (interactive: boolean) { + this.tabIndex = interactive ? 0 : -1; + } + public get interactive (): boolean { + return this.tabIndex >= 0; + } + /** * Getter for hours part. */ @@ -287,7 +307,7 @@ export class Clock extends ResponsiveElement { * Size of the clock. */ @property({ type: String, attribute: 'size', reflect: true }) - private size:null | 'small' = null; + private size: null | 'small' = null; /** * Get the display time in seconds. @@ -362,6 +382,19 @@ export class Clock extends ResponsiveElement { return this.displayTime % SECONDS_IN_MINUTE; } + /** + * Get display value + * @returns display value + */ + private get displayValue (): string { + return format({ + hours: this.displayHours, + minutes: this.displayMinutes, + seconds: this.displaySeconds, + milliseconds: 0 + }, TimeFormat.HHmmss); + } + /** * Get display AM or PM depending on time * @returns `AM` or `PM` @@ -375,7 +408,7 @@ export class Clock extends ResponsiveElement { * @returns Result */ private get isAM (): boolean { - return this.displayHours24 < HOURS_OF_NOON; + return isAM(this.displayValue); } /** @@ -417,39 +450,37 @@ export class Clock extends ResponsiveElement { } /** - * Returns any shift amount assigned to a target. - * @param target target of an event. + * Returns any shift amount assigned to a segment. + * @param segment Segment. * @returns {void} */ - private getShiftAmountFromTarget (target: EventTarget | null): number { - if (target === this.hoursPart) { + private getShiftAmountFromSegment (segment: Segment): number { + if (segment === Segment.HOURS) { return SECONDS_IN_HOUR; } - if (target === this.minutesPart) { + if (segment === Segment.MINUTES) { return SECONDS_IN_MINUTE; } - if (target === this.secondsPart) { + if (segment === Segment.SECOND) { return 1; } - if (target instanceof HTMLElement && target.parentElement) { - return this.getShiftAmountFromTarget(target.parentElement); - } + return 0; } /** * Returns `true` or `false` depends on the offset value's effect on giving segment * - * @param segment segment's name + * @param segment segment * @returns Result */ - private isSegmentShifted (segment: string): boolean { + private isSegmentShifted (segment: Segment): boolean { switch (segment) { - case 'hours': + case Segment.HOURS: return this.hours !== this.displayHours24; - case 'minutes': + case Segment.MINUTES: return this.minutes !== this.displayMinutes; - case 'seconds': + case Segment.SECOND: return this.seconds !== this.displaySeconds; default: return false; @@ -480,10 +511,10 @@ export class Clock extends ResponsiveElement { this.activeSegment = Segment.MINUTES; } if (event.target === this.upButtonRef.value) { - this.shift(Direction.UP, this.getShiftAmountFromTarget(event.target)); + this.shift(Direction.UP, this.getShiftAmountFromSegment(this.activeSegment)); } if (event.target === this.downButtonRef.value) { - this.shift(Direction.DOWN, this.getShiftAmountFromTarget(event.target)); + this.shift(Direction.DOWN, this.getShiftAmountFromSegment(this.activeSegment)); } } @@ -510,9 +541,11 @@ export class Clock extends ResponsiveElement { break; case 'Left': // IE case 'ArrowLeft': + this.activeSegment = Segment.HOURS; + break; case 'Right': // IE case 'ArrowRight': - this.handleLeftRightKey(); + this.activeSegment = Segment.MINUTES; break; default: return; @@ -523,43 +556,31 @@ export class Clock extends ResponsiveElement { /** * Handles UP key press - * @param event Event Object * @returns {void} */ private handleUpKey (): void { - const selection = this.renderRoot.querySelector(`[part~=${this.activeSegment}]`); - this.shift(Direction.UP, this.getShiftAmountFromTarget(selection)); + this.shift(Direction.UP, this.getShiftAmountFromSegment(this.activeSegment)); } /** * Handle DOWN key press - * @param event Event Object * @returns {void} */ private handleDownKey (): void { - const selection = this.renderRoot.querySelector(`[part~=${this.activeSegment}]`); - this.shift(Direction.DOWN, this.getShiftAmountFromTarget(selection)); - } - - /** - * Handles Left key press - * @param event Event Object - * @returns {void} - */ - private handleLeftRightKey (): void { - this.activeSegment = this.activeSegment === Segment.HOURS ? Segment.MINUTES : Segment.HOURS; + this.shift(Direction.DOWN, this.getShiftAmountFromSegment(this.activeSegment)); } /** * Updates aria-value-text to have the same value as screen * @returns {void} */ - private updateAriaValue () { - const value = `Time: ${padNumber(this.displayHours, 2)}` - + `:${padNumber(this.displayMinutes, 2)}` - + `${this.showSeconds ? ':' + padNumber(this.displaySeconds, 2) : ''}` - + `${this.amPm ? ' ' + this.displayAmPm : ''}`; - this.setAttribute('aria-valuenow', this.displayTime.toString()); + private async updateAriaValue () { + const value = await this.tPromise('TIME', { + value: parse(this.displayValue), + amPm: this.amPm, + showSeconds: this.showSeconds + }); + this.setAttribute('aria-valuenow', `${this.displayTime}`); this.setAttribute('aria-valuetext', value); } @@ -569,31 +590,35 @@ export class Clock extends ResponsiveElement { * @param segment active segment * @returns {TemplateResult} template result */ - private generateButtonsTemplate (segment: string): TemplateResult { - const isFocused = segment === this.activeSegment; + private generateButtonsTemplate (segment: Segment): TemplateResult { + const isActive = this.focused && segment === this.activeSegment; return html` - - + + `; } /** * Get template of segment - * @param name segment's name + * @param segment segment * @param value segment's value - * @param shiftAmount amount to shift * @returns {TemplateResult} template */ - private generateSegmentTemplate (name: string, value: number): TemplateResult { - const isFocused = this.interactive && (name === this.activeSegment); + private generateSegmentTemplate (segment: Segment, value: number): TemplateResult { + const isActive = this.interactive && this.focused && (segment === this.activeSegment); return html` -
+
${padNumber(value, 2)} - ${this.interactive ? this.generateButtonsTemplate(name) : undefined} + ${this.interactive ? this.generateButtonsTemplate(segment) : undefined}
`; } @@ -622,7 +647,7 @@ export class Clock extends ResponsiveElement { * @returns {TemplateResult} template */ private get hoursSegmentTemplate (): TemplateResult { - return this.generateSegmentTemplate('hours', this.displayHours); + return this.generateSegmentTemplate(Segment.HOURS, this.displayHours); } /** @@ -630,7 +655,7 @@ export class Clock extends ResponsiveElement { * @returns {TemplateResult} template */ private get minutesSegmentTemplate (): TemplateResult { - return this.generateSegmentTemplate('minutes', this.displayMinutes); + return this.generateSegmentTemplate(Segment.MINUTES, this.displayMinutes); } /** @@ -638,7 +663,7 @@ export class Clock extends ResponsiveElement { * @returns {TemplateResult} template */ private get secondsSegmentTemplate (): TemplateResult { - return this.generateSegmentTemplate('seconds', this.displaySeconds); + return this.generateSegmentTemplate(Segment.SECOND, this.displaySeconds); } /** @@ -688,21 +713,14 @@ export class Clock extends ResponsiveElement { protected willUpdate (changedProperties: PropertyValues): void { super.willUpdate(changedProperties); - if (changedProperties.has('interactive')) { - if (this.interactive) { - this.tabIndex = 0; - this.setAttribute('role', 'spinbutton'); - this.updateAriaValue(); - } - else { - this.tabIndex = -1; - this.setAttribute('role', 'group'); - this.removeAttribute('aria-valuenow'); - this.removeAttribute('aria-valuetext'); - } - } - if (this.interactive && (changedProperties.has('offset') || changedProperties.has('value'))) { - this.updateAriaValue(); + if (!this.hasUpdated + || changedProperties.has('sessionTicks') + || changedProperties.has('offset') + || changedProperties.has('value') + || changedProperties.has('showSeconds') + || changedProperties.has('amPm') + || changedProperties.has(TranslatePropertyKey)) { + void this.updateAriaValue(); } } diff --git a/packages/elements/src/clock/utils/timestamps.ts b/packages/elements/src/clock/utils/timestamps.ts deleted file mode 100644 index 06ac73e32c..0000000000 --- a/packages/elements/src/clock/utils/timestamps.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const SECONDS_IN_MINUTE = 60; -export const SECONDS_IN_HOUR = 3600; -export const SECONDS_IN_DAY = 86400; -export const MINUTES_IN_HOUR = 60; -export const MINUTES_IN_DAY = 1440; -export const HOURS_IN_DAY = 24; diff --git a/packages/phrasebook/package.json b/packages/phrasebook/package.json index b4bbfc2a56..b442dbd97c 100644 --- a/packages/phrasebook/package.json +++ b/packages/phrasebook/package.json @@ -23,6 +23,7 @@ ".": "./lib/index.js", "./locale/de/appstate-bar.js": "./lib/locale/de/appstate-bar.js", "./locale/de/calendar.js": "./lib/locale/de/calendar.js", + "./locale/de/clock.js": "./lib/locale/de/clock.js", "./locale/de/color-dialog.js": "./lib/locale/de/color-dialog.js", "./locale/de/combo-box.js": "./lib/locale/de/combo-box.js", "./locale/de/datetime-field.js": "./lib/locale/de/datetime-field.js", @@ -37,6 +38,7 @@ "./locale/de/card.js": "./lib/locale/de/card.js", "./locale/en/appstate-bar.js": "./lib/locale/en/appstate-bar.js", "./locale/en/calendar.js": "./lib/locale/en/calendar.js", + "./locale/en/clock.js": "./lib/locale/en/clock.js", "./locale/en/color-dialog.js": "./lib/locale/en/color-dialog.js", "./locale/en/combo-box.js": "./lib/locale/en/combo-box.js", "./locale/en/datetime-field.js": "./lib/locale/en/datetime-field.js", @@ -51,6 +53,7 @@ "./locale/en/card.js": "./lib/locale/en/card.js", "./locale/ja/appstate-bar.js": "./lib/locale/ja/appstate-bar.js", "./locale/ja/calendar.js": "./lib/locale/ja/calendar.js", + "./locale/ja/clock.js": "./lib/locale/ja/clock.js", "./locale/ja/color-dialog.js": "./lib/locale/ja/color-dialog.js", "./locale/ja/combo-box.js": "./lib/locale/ja/combo-box.js", "./locale/ja/datetime-field.js": "./lib/locale/ja/datetime-field.js", @@ -65,6 +68,7 @@ "./locale/ja/card.js": "./lib/locale/ja/card.js", "./locale/zh/appstate-bar.js": "./lib/locale/zh/appstate-bar.js", "./locale/zh/calendar.js": "./lib/locale/zh/calendar.js", + "./locale/zh/clock.js": "./lib/locale/zh/clock.js", "./locale/zh/color-dialog.js": "./lib/locale/zh/color-dialog.js", "./locale/zh/combo-box.js": "./lib/locale/zh/combo-box.js", "./locale/zh/datetime-field.js": "./lib/locale/zh/datetime-field.js", @@ -79,6 +83,7 @@ "./locale/zh/card.js": "./lib/locale/zh/card.js", "./locale/zh-hant/appstate-bar.js": "./lib/locale/zh-hant/appstate-bar.js", "./locale/zh-hant/calendar.js": "./lib/locale/zh-hant/calendar.js", + "./locale/zh-hant/clock.js": "./lib/locale/zh-hant/clock.js", "./locale/zh-hant/color-dialog.js": "./lib/locale/zh-hant/color-dialog.js", "./locale/zh-hant/combo-box.js": "./lib/locale/zh-hant/combo-box.js", "./locale/zh-hant/datetime-field.js": "./lib/locale/zh-hant/datetime-field.js", @@ -113,4 +118,4 @@ "dependencies": { "tslib": "^2.3.1" } -} \ No newline at end of file +} diff --git a/packages/phrasebook/src/locale/de/clock.ts b/packages/phrasebook/src/locale/de/clock.ts new file mode 100644 index 0000000000..b4c27fc037 --- /dev/null +++ b/packages/phrasebook/src/locale/de/clock.ts @@ -0,0 +1,9 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + TIME: 'Time: {showSeconds, select, true {{amPm, select, true {{value, time, ::hmsa}} other {{value, time, ::Hms}}}} other {{amPm, select, true {{value, time, ::hma}} other {{value, time, ::Hm}}}}}' +}; + +Phrasebook.define('de', 'ef-clock', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/en/clock.ts b/packages/phrasebook/src/locale/en/clock.ts new file mode 100644 index 0000000000..43243265b9 --- /dev/null +++ b/packages/phrasebook/src/locale/en/clock.ts @@ -0,0 +1,9 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + TIME: 'Time: {showSeconds, select, true {{amPm, select, true {{value, time, ::hmsa}} other {{value, time, ::Hms}}}} other {{amPm, select, true {{value, time, ::hma}} other {{value, time, ::Hm}}}}}' +}; + +Phrasebook.define('en', 'ef-clock', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/ja/clock.ts b/packages/phrasebook/src/locale/ja/clock.ts new file mode 100644 index 0000000000..643589dd96 --- /dev/null +++ b/packages/phrasebook/src/locale/ja/clock.ts @@ -0,0 +1,9 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + TIME: 'Time: {showSeconds, select, true {{amPm, select, true {{value, time, ::hmsa}} other {{value, time, ::Hms}}}} other {{amPm, select, true {{value, time, ::hma}} other {{value, time, ::Hm}}}}}' +}; + +Phrasebook.define('ja', 'ef-clock', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/zh-hant/clock.ts b/packages/phrasebook/src/locale/zh-hant/clock.ts new file mode 100644 index 0000000000..39f349780e --- /dev/null +++ b/packages/phrasebook/src/locale/zh-hant/clock.ts @@ -0,0 +1,9 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + TIME: 'Time: {showSeconds, select, true {{amPm, select, true {{value, time, ::hmsa}} other {{value, time, ::Hms}}}} other {{amPm, select, true {{value, time, ::hma}} other {{value, time, ::Hm}}}}}' +}; + +Phrasebook.define('zh-Hant', 'ef-clock', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/zh/clock.ts b/packages/phrasebook/src/locale/zh/clock.ts new file mode 100644 index 0000000000..d2c1fc5b91 --- /dev/null +++ b/packages/phrasebook/src/locale/zh/clock.ts @@ -0,0 +1,9 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + TIME: 'Time: {showSeconds, select, true {{amPm, select, true {{value, time, ::hmsa}} other {{value, time, ::Hms}}}} other {{amPm, select, true {{value, time, ::hma}} other {{value, time, ::Hm}}}}}' +}; + +Phrasebook.define('zh', 'ef-clock', translations); + +export default translations; diff --git a/packages/utils/src/date/timestamps.ts b/packages/utils/src/date/timestamps.ts index ace8720e32..049b6685bf 100644 --- a/packages/utils/src/date/timestamps.ts +++ b/packages/utils/src/date/timestamps.ts @@ -4,6 +4,8 @@ export const MONTHS_IN_YEAR = 12; export const HOURS_IN_DAY = 24; export const MINUTES_IN_HOUR = 60; export const SECONDS_IN_MINUTE = 60; +export const SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR; +export const SECONDS_IN_DAY = SECONDS_IN_HOUR * HOURS_IN_DAY; export const MILLISECONDS_IN_SECOND = 1000; export const MILLISECONDS_IN_MINUTE = MILLISECONDS_IN_SECOND * SECONDS_IN_MINUTE; export const MILLISECONDS_IN_HOUR = MILLISECONDS_IN_MINUTE * MINUTES_IN_HOUR;