From 5744cd96493b90a31fc9bcb36991df751f7f93f2 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 18 Jul 2023 14:29:41 +0100 Subject: [PATCH 01/37] feat: add input segments --- packages/input-segments/.npmignore | 2 + packages/input-segments/README.md | 28 + packages/input-segments/exports.json | 3 + packages/input-segments/package.json | 74 + packages/input-segments/src/InputSegments.ts | 978 +++++++++++++ packages/input-segments/src/index.ts | 12 + .../input-segments/src/input-segments.css | 13 + .../input-segments/src/spectrum-config.js | 32 + .../src/spectrum-input-segments.css | 1212 +++++++++++++++++ packages/input-segments/src/types.ts | 67 + .../test/benchmark/basic-test.ts | 18 + .../test/input-segments.test.ts | 38 + packages/input-segments/tsconfig.json | 14 + tools/bundle/package.json | 1 + tsconfig-all.json | 1 + yarn.lock | 96 +- 16 files changed, 2581 insertions(+), 8 deletions(-) create mode 100644 packages/input-segments/.npmignore create mode 100644 packages/input-segments/README.md create mode 100644 packages/input-segments/exports.json create mode 100644 packages/input-segments/package.json create mode 100644 packages/input-segments/src/InputSegments.ts create mode 100644 packages/input-segments/src/index.ts create mode 100644 packages/input-segments/src/input-segments.css create mode 100644 packages/input-segments/src/spectrum-config.js create mode 100644 packages/input-segments/src/spectrum-input-segments.css create mode 100644 packages/input-segments/src/types.ts create mode 100644 packages/input-segments/test/benchmark/basic-test.ts create mode 100644 packages/input-segments/test/input-segments.test.ts create mode 100644 packages/input-segments/tsconfig.json diff --git a/packages/input-segments/.npmignore b/packages/input-segments/.npmignore new file mode 100644 index 00000000000..c50cbe188c0 --- /dev/null +++ b/packages/input-segments/.npmignore @@ -0,0 +1,2 @@ +stories +test \ No newline at end of file diff --git a/packages/input-segments/README.md b/packages/input-segments/README.md new file mode 100644 index 00000000000..8fb933e1597 --- /dev/null +++ b/packages/input-segments/README.md @@ -0,0 +1,28 @@ +## Description + +### Usage + +[![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/input-segments?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/input-segments) +[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/input-segments?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/input-segments) + +``` +yarn add @spectrum-web-components/input-segments +``` + +Import the side effectful registration of `` via: + +``` +import '@spectrum-web-components/input-segments/sp-input-segments.js'; +``` + +When looking to leverage the `InputSegments` base class as a type and/or for extension purposes, do so via: + +``` +import { InputSegments } from '@spectrum-web-components/input-segments'; +``` + +## Example + +```html + +``` diff --git a/packages/input-segments/exports.json b/packages/input-segments/exports.json new file mode 100644 index 00000000000..8128c96f462 --- /dev/null +++ b/packages/input-segments/exports.json @@ -0,0 +1,3 @@ +{ + "./src/*": "./src/*.js" +} diff --git a/packages/input-segments/package.json b/packages/input-segments/package.json new file mode 100644 index 00000000000..197a6f7f5e6 --- /dev/null +++ b/packages/input-segments/package.json @@ -0,0 +1,74 @@ +{ + "name": "@spectrum-web-components/input-segments", + "version": "0.0.1", + "publishConfig": { + "access": "public" + }, + "description": "Web component implementation of a Spectrum design InputSegments", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/adobe/spectrum-web-components.git", + "directory": "packages/input-segments" + }, + "author": "", + "homepage": "https://adobe.github.io/spectrum-web-components/components/input-segments", + "bugs": { + "url": "https://github.com/adobe/spectrum-web-components/issues" + }, + "main": "./src/index.js", + "module": "./src/index.js", + "type": "module", + "exports": { + ".": { + "development": "./src/index.dev.js", + "default": "./src/index.js" + }, + "./package.json": "./package.json", + "./src/InputSegments.js": { + "development": "./src/InputSegments.dev.js", + "default": "./src/InputSegments.js" + }, + "./src/index.js": { + "development": "./src/index.dev.js", + "default": "./src/index.js" + }, + "./src/input-segments.css.js": "./src/input-segments.css.js", + "./src/types.js": { + "development": "./src/types.dev.js", + "default": "./src/types.js" + } + }, + "scripts": { + "test": "echo \"Error: run tests from mono-repo root.\" && exit 1" + }, + "files": [ + "**/*.d.ts", + "**/*.js", + "**/*.js.map", + "custom-elements.json", + "!stories/", + "!test/" + ], + "keywords": [ + "spectrum css", + "web components", + "lit-element", + "lit-html" + ], + "dependencies": { + "@internationalized/date": "^3.2.0", + "@spectrum-web-components/base": "^0.34.0", + "@spectrum-web-components/reactive-controllers": "^0.34.0", + "@spectrum-web-components/textfield": "^0.34.0" + }, + "devDependencies": { + "@spectrum-css/textfield": "^6.0.7" + }, + "types": "./src/index.d.ts", + "customElements": "custom-elements.json", + "sideEffects": [ + "./sp-*.js", + "./**/*.dev.js" + ] +} diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts new file mode 100644 index 00000000000..865139e663f --- /dev/null +++ b/packages/input-segments/src/InputSegments.ts @@ -0,0 +1,978 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { + CalendarDateTime, + DateFormatter, + getLocalTimeZone, + getMinimumDayInMonth, + getMinimumMonthInYear, + now, + toCalendarDateTime, +} from '@internationalized/date'; +import { + CSSResultArray, + html, + PropertyValueMap, + TemplateResult, +} from '@spectrum-web-components/base'; +import { LanguageResolutionController } from '@spectrum-web-components/reactive-controllers/src/LanguageResolution.js'; +import { TextfieldBase } from '@spectrum-web-components/textfield'; + +import { property, query, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { when } from 'lit/directives/when.js'; + +import { + AM, + dateSegmentTypes, + maxHourAM, + maxHourPM, + minHourAM, + minHourPM, + PM, + Segment, + SegmentValueAndLimits, + TimeGranularity, + timeSegmentTypes, +} from './types.js'; + +import styles from './input-segments.css.js'; + +/** + * @element sp-input-segments + */ +export class InputSegments extends TextfieldBase { + public static override get styles(): CSSResultArray { + return [...super.styles, styles]; + } + + @query('.editable-segment') + firstEditableSegment!: HTMLDivElement; + + @property({ reflect: true, attribute: false }) + selectedDateTime?: Date; + + @property({ attribute: false }) + includeDate = false; + + @property({ attribute: false }) + includeTime = false; + + @property({ attribute: false }) + timeGranularity: TimeGranularity = 'minute'; + + @state() + private _locale!: string; + + @state() + private _previousLocale?: string; + + @state() + private _currentDateTime!: CalendarDateTime; + + @state() + private _newDateTime?: CalendarDateTime; + + @state() + private _segments: Segment[] = []; + + @state() + private _createSegments = true; + + private _languageResolver = new LanguageResolutionController(this); + private _timeZone!: string; + private _formatter!: DateFormatter; + + private get _daySegment(): Segment | undefined { + return this._segments.find((segment) => segment.type === 'day'); + } + + private get _monthSegment(): Segment | undefined { + return this._segments.find((segment) => segment.type === 'month'); + } + + private get _yearSegment(): Segment | undefined { + return this._segments.find((segment) => segment.type === 'year'); + } + + private get _hourSegment(): Segment | undefined { + return this._segments.find((segment) => segment.type === 'hour'); + } + + private get _minuteSegment(): Segment | undefined { + return this._segments.find((segment) => segment.type === 'minute'); + } + + private get _secondSegment(): Segment | undefined { + return this._segments.find((segment) => segment.type === 'second'); + } + + private get _dayPeriodSegment(): Segment | undefined { + return this._segments.find((segment) => segment.type === 'dayPeriod'); + } + + private get _is12HourClock(): boolean { + return Boolean(this._formatter.resolvedOptions().hour12); + } + + /** + * The `TextfieldBase` class requires this getter to return an element of type `HTMLInputElement` or + * `HTMLTextAreaElement`, but since the segments are DIVs with the `contenteditable` attribute, we need to cast as + * an input only to be able to use autofocus. + * + * Note that `focusElement` is only used for that, so converting as an input will have no side effect as all + * functions and attributes used exist in both types, `HTMLInputElement` and `HTMLDivElement`. + */ + public override get focusElement(): HTMLInputElement { + return this.firstEditableSegment as HTMLInputElement; + } + + constructor() { + super(); + + this._setTimeZone(); + this._setLocale(); + this._setFormatter(); + this._setInitialDateTime(); + } + + protected override willUpdate( + changedProperties: PropertyValueMap + ): void { + this._setLocale(); + this._setFormatter(); + + if (changedProperties.has('selectedDateTime')) { + this._setCurrentDateTime(); + this._createSegments = true; + } + + if ( + changedProperties.has('includeDate') || + changedProperties.has('includeTime') || + changedProperties.has('timeGranularity') + ) { + this._createSegments = true; + } + + if (this._locale !== this._previousLocale) { + this._previousLocale = this._locale; + this._createSegments = true; + } + + if (this._createSegments) { + this._setSegments(); + } + } + + protected override renderField(): TemplateResult { + return html` + ${this.renderStateIcons()} + +
+ +
+ `; + } + + public renderLiteralSegment(segment: Segment): TemplateResult { + return html` + + `; + } + + public renderEditableSegment(segment: Segment): TemplateResult { + const isActive = !this.disabled && !this.readonly; + + const isPlaceholderVisible = Boolean(segment.value === undefined); + + const segmentClasses = { + 'is-placeholder': isPlaceholderVisible, + }; + + const segmentStyles = { + minWidth: + segment.maxValue !== undefined + ? `${String(segment.maxValue).length}ch` + : undefined, + }; + + return html` +
{ + this.handleKeydown(segment, event); + }} + > + ${when( + isPlaceholderVisible, + () => html` + + `, + () => segment.formatted + )} +
+ `; + } + + public handleFocusIn(): void { + super.onFocus(); + } + + public handleFocusOut(): void { + super.onBlur(); + } + + /** + * Detects the pressed key and performs the correct action accordingly + * + * @param segment - Segment on which the event was fired + * @param event - Event details + */ + public handleKeydown(segment: Segment, event: KeyboardEvent): void { + switch (event.code) { + case 'ArrowUp': { + this._incrementValue(segment); + break; + } + case 'ArrowRight': { + this._focusNextSegment(event); + break; + } + case 'ArrowDown': { + this._decrementValue(segment); + break; + } + case 'ArrowLeft': { + this._focusPreviousSegment(event); + break; + } + default: { + // TODO: Use @input/@beforeinput events to handle data input/content cleanup + const key = event.key; + const numberKey = /^[\d]+$/.test(key); + const clearKey = ['Backspace', 'Delete'].includes(key); + const allowedKey = ['Tab'].includes(key); + + if (numberKey) { + this.handleTypedValue(segment, event); + } + + if (clearKey) { + this.handleClear(segment); + } + + if (numberKey || clearKey || !allowedKey) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + + public handleTypedValue(segment: Segment, event: KeyboardEvent): void { + const min = segment.minValue; + const max = segment.maxValue; + + if (min !== undefined && max !== undefined) { + const typedValue = Number(event.key); + const isHourAmPm = this._is12HourClock && segment.type === 'hour'; + const maxLength = String(max).length; + + let previousValue = segment.value; + let newValue: number; + + if ( + isHourAmPm && + previousValue !== undefined && + this._isPM(previousValue) + ) { + previousValue -= PM; + } + + newValue = + previousValue !== undefined + ? Number(`${previousValue}${typedValue}`) + : typedValue; + + if (String(newValue).length > maxLength) { + newValue = isHourAmPm + ? typedValue + : Number(String(newValue).slice(1)); + } + + // Defines the value that should be used if the new defined value is less than the minimum allowed + const useTypedValueOrMin = typedValue >= min ? typedValue : min; + + // Defines the value that should be used if the new defined value is greater than the maximum allowed + const useTypedValueOrMax = typedValue <= max ? typedValue : max; + + if (isHourAmPm) { + const isPM = this._isPM(min); + + if (isPM && newValue !== min && newValue > maxHourAM) { + newValue = Number(String(newValue).slice(1)); + } else if (newValue > max) { + const useMinHourAM = !isPM && newValue === PM; + newValue = useMinHourAM ? minHourAM : useTypedValueOrMax; + } + + if (isPM && newValue !== min) { + newValue += PM; + } + } else { + if (String(newValue).length > maxLength) { + newValue = Number(String(newValue).slice(1)); + } + + if (newValue < min) { + newValue = useTypedValueOrMin; + } else if (newValue > max) { + newValue = useTypedValueOrMax; + } + } + + segment.value = newValue; + + this._valueChanged(segment); + } + } + + public handleClear(segment: Segment): void { + let newValue: string | undefined; + let previousValue = segment.value; + + if (previousValue !== undefined) { + if (this._is12HourClock && segment.type === 'hour') { + const isPM = + segment.minValue !== undefined && + this._isPM(segment.minValue); + + if (isPM) { + previousValue -= PM; + } + + newValue = + previousValue === minHourAM + ? String(minHourAM + 1) + : String(previousValue).slice(0, -1); + + if (isPM && newValue !== '') { + newValue = String(Number(newValue) + PM); + } + } else { + newValue = + segment.type === 'dayPeriod' + ? undefined + : String(previousValue).slice(0, -1); + } + + segment.value = (newValue && Number(newValue)) || undefined; + + this._valueChanged(segment); + } + } + + private _setTimeZone(): void { + this._timeZone = getLocalTimeZone(); + } + + private _setLocale(): void { + this._locale = this._languageResolver.language; + } + + private _setFormatter(): void { + let dateOptions: Intl.DateTimeFormatOptions = {}; + let timeOptions: Intl.DateTimeFormatOptions = {}; + + if (this.includeDate) { + dateOptions = { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }; + } + + if (this.includeTime) { + const useMinutes = ( + ['minute', 'second'] as TimeGranularity[] + ).includes(this.timeGranularity); + + timeOptions = { + hour: '2-digit', + ...(useMinutes && { minute: '2-digit' }), + ...(this.timeGranularity === 'second' && { second: '2-digit' }), + }; + } + + this._formatter = new DateFormatter(this._locale, { + ...dateOptions, + ...timeOptions, + }); + } + + private _setInitialDateTime(): void { + this._currentDateTime = toCalendarDateTime(now(this._timeZone)); + } + + private _setCurrentDateTime(): void { + if (this.selectedDateTime) { + this.selectedDateTime = new Date(this.selectedDateTime); + + if (!this._isValidTime(this.selectedDateTime)) { + this.selectedDateTime = undefined; + } else { + this._currentDateTime = this._dateToCalendarDateTime( + this.selectedDateTime + ); + } + } + } + + private _setNewDateTime(): void { + this._newDateTime = undefined; + + let year: number | undefined = undefined; + let month: number | undefined = undefined; + let day: number | undefined = undefined; + + let hour: number | undefined = undefined; + let minute: number | undefined = undefined; + let second: number | undefined = undefined; + + const isHour = this.timeGranularity === 'hour'; + const isMinute = this.timeGranularity === 'minute'; + const isSecond = this.timeGranularity === 'second'; + + if (this.includeDate) { + if (this._yearSegment?.value !== undefined) { + year = this._yearSegment.value; + } + + if (this._monthSegment?.value !== undefined) { + month = this._monthSegment.value; + } + + if (this._daySegment?.value !== undefined) { + day = this._daySegment.value; + } + } + + if (this.includeTime) { + const hasHourValue = this._hourSegment?.value !== undefined; + const hasMinuteValue = this._minuteSegment?.value !== undefined; + const hasSecondValue = this._secondSegment?.value !== undefined; + + if (isHour && hasHourValue) { + hour = this._hourSegment?.value; + } + + if (isMinute && hasHourValue && hasMinuteValue) { + minute = this._minuteSegment?.value; + } + + if (isSecond && hasHourValue && hasMinuteValue && hasSecondValue) { + second = this._secondSegment?.value; + } + + if (!this.includeDate) { + year = this._currentDateTime.year; + month = this._currentDateTime.month; + day = this._currentDateTime.day; + } + } + + // To create a new CalendarDateTime the only mandatory values are those referring to the date + if (year !== undefined && month !== undefined && day !== undefined) { + this._newDateTime = new CalendarDateTime( + year, + month, + day, + hour, + minute, + second + ); + } + } + + /** + * Checks if the date is valid by parsing the time. Invalid dates return `NaN` for times of invalid dates + * + * @param date - `Date` object to validate + */ + private _isValidTime(date: Date): boolean { + return !isNaN(date.getTime()); + } + + /** + * Converts an object of type `Date` to `Calendar DateTime` + * + * @param date - `Date` object to "convert" + */ + private _dateToCalendarDateTime(date: Date): CalendarDateTime { + return new CalendarDateTime( + date.getFullYear(), + date.getMonth() + 1, // The month to create a new `CalendarDate` cannot be a zero-based index, unlike `Date` + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds() + ); + } + + /** + * Determines which segments will be used by the input (hour, minute, second, day period for 12-hour clock). The + * segment referring to the hour will always be displayed, the other segments vary according to the defined locale + * and granularity + */ + private _setSegments(): void { + const { hour, minute, second } = this._currentDateTime; + + const dateTime = new Date(); + dateTime.setHours(hour, minute, second); + + const segmentTypes = [ + ...(this.includeDate ? dateSegmentTypes : []), + ...(this.includeTime ? timeSegmentTypes : []), + ]; + + this._segments = this._formatter + .formatToParts(dateTime) + .map((part) => this._mapToTimeSegment(part)) + .filter((part) => segmentTypes.includes(part.type)); + + this._createSegments = false; + } + + /** + * The parts returned by the `formatToParts()` function have only two properties, `type` and `value`, but we need + * more information for each segment, so we convert it to the type we need + * + * @param part - Part/segment to be "translated" (mapped) + */ + private _mapToTimeSegment(part: Intl.DateTimeFormatPart): Segment { + const { value, minValue, maxValue } = this._getSegmentDetails( + part.type + ); + + const segment: Segment = { + type: part.type, + placeholder: this._getPlaceholder(part.type, part.value), + formatted: part.value, + value, + minValue, + maxValue, + }; + + this._formatValues(segment); + + return segment; + } + + /** + * If the segment has a `value`, it defines the text used in the UI formatted according to the locale + * + * @param segment - Segment to be updated + */ + private _formatValues(segment: Segment): void { + if (segment.value !== undefined) { + const options: Intl.DateTimeFormatOptions = {}; + + let year = this._currentDateTime.year; + let month = this._currentDateTime.month; + let day = this._currentDateTime.day; + + let hour = this._currentDateTime.hour; + let minute = this._currentDateTime.minute; + let second = this._currentDateTime.second; + + let padMaxLength = 2; + + switch (segment.type) { + case 'year': { + year = segment.value; + options.year = 'numeric'; + break; + } + case 'month': { + month = segment.value; + options.month = '2-digit'; + break; + } + case 'day': { + day = segment.value; + options.day = '2-digit'; + break; + } + case 'hour': { + if (this._is12HourClock) { + padMaxLength = 1; + } + + hour = segment.value; + options.hour = 'numeric'; + break; + } + case 'minute': { + minute = segment.value; + options.minute = '2-digit'; + break; + } + case 'second': { + second = segment.value; + options.second = '2-digit'; + break; + } + case 'dayPeriod': { + hour = (segment.value || 0) + 1; + options.hour = 'numeric'; + break; + } + } + + const date = new Date(year, month, day, hour, minute, second); + const formatted = new DateFormatter(this._locale, options) + .formatToParts(date) + .find((part) => part.type === segment.type)?.value; + + segment.formatted = formatted?.padStart(padMaxLength, '0'); + } + } + + /** + * Returns the placeholder that will be used. If it is the day period segment, use the actual value. For the rest of + * the segments, use two dashes as a placeholder + * + * @param type - Type of segment + * @param value - The value of the segment + */ + private _getPlaceholder( + type: Intl.DateTimeFormatPartTypes, + value: string + ): string { + return type === 'dayPeriod' ? value : '––'; + } + + /** + * Indicates whether the hour entered is PM or not + * + * @param hour - The hour to check + */ + private _isPM(hour: number): boolean { + return hour >= PM; + } + + /** + * Returns the corresponding "modifier" (0 for "AM" and 12 for "PM") for the given hour + * + * @param hour - The hour to identify the modifier + */ + private _getAmPmModifier(hour: number): typeof AM | typeof PM { + return this._isPM(hour) ? PM : AM; + } + + /** + * Returns the minimum and maximum values for each segment that will be used, in addition to defining if there is a + * current value to be used. If segments are being recreated, we try to recover the value that was previously set + * for each segment, if possible + * + * @param type - Segment type + */ + private _getSegmentDetails( + type: Intl.DateTimeFormatPartTypes + ): SegmentValueAndLimits { + switch (type) { + case 'year': + return { + minValue: 1, + maxValue: this._currentDateTime.calendar.getYearsInEra( + this._currentDateTime + ), + value: + this._newDateTime?.year ?? + (this.selectedDateTime && this._currentDateTime.year) ?? + undefined, + }; + + case 'month': + return { + minValue: getMinimumMonthInYear(this._currentDateTime), + maxValue: this._currentDateTime.calendar.getMonthsInYear( + this._currentDateTime + ), + value: + this._newDateTime?.month ?? + (this.selectedDateTime && + this._currentDateTime.month) ?? + undefined, + }; + + case 'day': + return { + minValue: getMinimumDayInMonth(this._currentDateTime), + maxValue: this._currentDateTime.calendar.getDaysInMonth( + this._currentDateTime + ), + value: + this._newDateTime?.day ?? + (this.selectedDateTime && this._currentDateTime.day) ?? + undefined, + }; + + case 'hour': + let min = 0; + let max = 23; + + if (this._is12HourClock) { + const isPM = this._isPM( + this._newDateTime?.hour ?? this._currentDateTime.hour + ); + + min = isPM ? minHourPM : minHourAM; + max = isPM ? maxHourPM : maxHourAM; + } + + return { + minValue: min, + maxValue: max, + value: + this._newDateTime?.hour ?? + (this.selectedDateTime && this._currentDateTime.hour) ?? + undefined, + }; + + case 'minute': + case 'second': + const minutes = + this._newDateTime?.minute ?? + (this.selectedDateTime && this._currentDateTime.minute) ?? + undefined; + + const seconds = + this._newDateTime?.second ?? + (this.selectedDateTime && this._currentDateTime.second) ?? + undefined; + + return { + minValue: 0, + maxValue: 59, + value: type === 'minute' ? minutes : seconds, + }; + + case 'dayPeriod': + return { + minValue: AM, + maxValue: PM, + value: + (this._newDateTime?.hour && + this._getAmPmModifier(this._newDateTime.hour)) ?? + (this.selectedDateTime && + this._getAmPmModifier( + this._currentDateTime.hour + )) ?? + undefined, + }; + + default: + return {}; + } + } + + private _incrementValue(segment: Segment): void { + const min = segment.minValue; + const max = segment.maxValue; + + if (min !== undefined && max !== undefined) { + if (segment.value === undefined) { + segment.value = min; + } else if (segment.type === 'dayPeriod') { + segment.value = segment.value === AM ? PM : AM; + } else { + segment.value++; + + if (segment.value > max) { + segment.value = min; + } + } + } + + this._valueChanged(segment); + } + + private _decrementValue(segment: Segment): void { + const min = segment.minValue; + const max = segment.maxValue; + + if (min !== undefined && max !== undefined) { + if (segment.value === undefined) { + segment.value = max; + } else if (segment.type === 'dayPeriod') { + segment.value = segment.value === AM ? PM : AM; + } else { + segment.value--; + + if (segment.value < min) { + segment.value = max; + } + } + } + + this._valueChanged(segment); + } + + /** + * When the day period is changed, it automatically adjusts the hour if it has already been informed previously to + * match the new period (AM or PM). In addition, the minimum and maximum values of the hour are also changed + */ + private _updateHour(): void { + if (this._hourSegment && this._dayPeriodSegment) { + if (this._dayPeriodSegment.value !== undefined) { + const isAM = this._dayPeriodSegment.value === AM; + const isPM = this._dayPeriodSegment.value === PM; + + this._hourSegment.minValue = isPM ? minHourPM : minHourAM; + this._hourSegment.maxValue = isPM ? maxHourPM : maxHourAM; + + if (this._hourSegment.value !== undefined) { + if (isAM && this._isPM(this._hourSegment.value)) { + this._hourSegment.value -= PM; + } else if (isPM && !this._isPM(this._hourSegment.value)) { + this._hourSegment.value += PM; + } + } + } else { + this._resetHourAndDayPeriod(); + } + } + } + + /** + * When the day period is cleared, we need to reset the min and max values of the day period and hour segments to + * their initial values + */ + private _resetHourAndDayPeriod(): void { + const dayPeriod = this._getSegmentDetails('dayPeriod'); + + if (this._dayPeriodSegment) { + this._dayPeriodSegment.value = dayPeriod.value; + this._dayPeriodSegment.minValue = dayPeriod.minValue; + this._dayPeriodSegment.maxValue = dayPeriod.maxValue; + + if (this._dayPeriodSegment.value === undefined) { + this._dayPeriodSegment.formatted = + this._dayPeriodSegment.placeholder; + } + } + + const hour = this._getSegmentDetails('hour'); + + if (this._hourSegment) { + this._hourSegment.minValue = hour.minValue; + this._hourSegment.maxValue = hour.maxValue; + + if (this._hourSegment.value !== undefined) { + this._hourSegment.value += this._getAmPmModifier( + this._currentDateTime.hour + ); + } else { + this._hourSegment.value = hour.value; + } + } + } + + private _valueChanged(segment: Segment): void { + if (this._is12HourClock && segment.type === 'dayPeriod') { + this._updateHour(); + } + + this._formatValues(segment); + this._setNewDateTime(); + + this.requestUpdate(); + + if (this._newDateTime) { + this.dispatchEvent( + new CustomEvent('change', { + bubbles: true, + cancelable: true, + composed: true, + detail: this._newDateTime.toDate(this._timeZone), + }) + ); + } + } + + private _focusNextSegment(event: KeyboardEvent): void { + this._focusSegment(event.target as HTMLDivElement, 'next'); + } + + private _focusPreviousSegment(event: KeyboardEvent): void { + this._focusSegment(event.target as HTMLDivElement, 'previous'); + } + + private _focusSegment( + segment: HTMLDivElement, + elementToFocus: 'previous' | 'next' + ): void { + let segmentFound = false; + let currentSegment = segment; + + while (!segmentFound) { + const siblingSegment = ( + elementToFocus === 'previous' + ? currentSegment.previousElementSibling + : currentSegment.nextElementSibling + ) as HTMLDivElement; + + // No more segments to focus on + if (!siblingSegment) { + break; + } + + if (siblingSegment.getAttribute('contenteditable')) { + segmentFound = true; + siblingSegment.focus(); + } else { + currentSegment = siblingSegment; + } + } + } +} diff --git a/packages/input-segments/src/index.ts b/packages/input-segments/src/index.ts new file mode 100644 index 00000000000..e75d429f1a3 --- /dev/null +++ b/packages/input-segments/src/index.ts @@ -0,0 +1,12 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +export * from './InputSegments.js'; diff --git a/packages/input-segments/src/input-segments.css b/packages/input-segments/src/input-segments.css new file mode 100644 index 00000000000..fce20f42a8f --- /dev/null +++ b/packages/input-segments/src/input-segments.css @@ -0,0 +1,13 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +@import './spectrum-input-segments.css'; diff --git a/packages/input-segments/src/spectrum-config.js b/packages/input-segments/src/spectrum-config.js new file mode 100644 index 00000000000..40b22668061 --- /dev/null +++ b/packages/input-segments/src/spectrum-config.js @@ -0,0 +1,32 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +// @ts-check + +import { converterFor } from '../../../tasks/process-spectrum-utils.js'; + +const converter = converterFor('spectrum-InputSegments'); + +/** + * @type { import('../../../tasks/spectrum-css-converter').SpectrumCSSConverter } + */ +const config = { + conversions: [ + { + inPackage: '@spectrum-css/textfield', + outPackage: 'input-segments', + fileName: 'input-segments', + components: [converter.classToHost()], + }, + ], +}; + +export default config; diff --git a/packages/input-segments/src/spectrum-input-segments.css b/packages/input-segments/src/spectrum-input-segments.css new file mode 100644 index 00000000000..6dc80edead8 --- /dev/null +++ b/packages/input-segments/src/spectrum-input-segments.css @@ -0,0 +1,1212 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +/* THIS FILE IS MACHINE GENERATED. DO NOT EDIT */ +.spectrum-Textfield { + --spectrum-texfield-animation-duration: var( + --spectrum-animation-duration-100 + ); + --spectrum-textfield-width: 240px; + --spectrum-textfield-min-width: var( + --spectrum-text-field-minimum-width-multiplier + ); + --spectrum-textfield-corner-radius: var(--spectrum-corner-radius-100); + --spectrum-textfield-height: var(--spectrum-component-height-100); + --spectrum-textfield-spacing-inline: var( + --spectrum-component-edge-to-text-100 + ); + --spectrum-textfield-spacing-inline-quiet: var( + --spectrum-field-edge-to-text-quiet + ); + --spectrum-textfield-spacing-block-start: var( + --spectrum-component-top-to-text-100 + ); + --spectrum-textfield-spacing-block-end: var( + --spectrum-component-bottom-to-text-100 + ); + --spectrum-textfield-spacing-block-quiet: var( + --spectrum-field-edge-to-border-quiet + ); + --spectrum-textfield-label-spacing-block: var( + --spectrum-field-label-to-component + ); + --spectrum-textfield-label-spacing-block-quiet: var( + --spectrum-field-label-to-component-quiet-medium + ); + --spectrum-textfield-label-spacing-inline-side-label: var( + --spectrum-spacing-100 + ); + --spectrum-textfield-helptext-spacing-block: var( + --spectrum-help-text-to-component + ); + --spectrum-textfield-icon-size-invalid: var( + --spectrum-workflow-icon-size-100 + ); + --spectrum-textfield-icon-spacing-inline-start-invalid: var( + --spectrum-field-text-to-alert-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-end-invalid: var( + --spectrum-field-edge-to-alert-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-end-quiet-invalid: var( + --spectrum-field-edge-to-alert-icon-quiet + ); + --spectrum-textfield-icon-spacing-block-invalid: var( + --spectrum-field-top-to-alert-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-start-valid: var( + --spectrum-field-text-to-validation-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-end-valid: var( + --spectrum-field-edge-to-validation-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-end-quiet-valid: var( + --spectrum-field-edge-to-validation-icon-quiet + ); + --spectrum-textfield-icon-spacing-block-valid: var( + --spectrum-field-top-to-validation-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-end-override: 32px; + --spectrum-Textfield-workflow-icon-width: 18px; + --spectrum-Textfield-workflow-icon-gap: 6px; + --spectrum-textfield-font-family: var(--spectrum-sans-font-family-stack); + --spectrum-textfield-font-weight: var(--spectrum-regular-font-weight); + --spectrum-textfield-placeholder-font-size: var(--spectrum-font-size-100); + --spectrum-textfield-character-count-font-family: var( + --spectrum-sans-font-family-stack + ); + --spectrum-textfield-character-count-font-weight: var( + --spectrum-regular-font-weight + ); + --spectrum-textfield-character-count-font-size: var( + --spectrum-font-size-75 + ); + --spectrum-textfield-character-count-spacing-inline: var( + --spectrum-spacing-200 + ); + --spectrum-textfield-character-count-spacing-block: var( + --spectrum-component-bottom-to-text-75 + ); + --spectrum-textfield-character-count-spacing-inline-side: var( + --spectrum-side-label-character-count-to-field + ); + --spectrum-textfield-character-count-spacing-block-side: var( + --spectrum-side-label-character-count-top-margin-medium + ); + --spectrum-textfield-focus-indicator-width: var( + --spectrum-focus-indicator-thickness + ); + --spectrum-textfield-focus-indicator-gap: var( + --spectrum-focus-indicator-gap + ); + --spectrum-textfield-background-color: var(--spectrum-gray-50); + --spectrum-textfield-text-color-default: var( + --spectrum-neutral-content-color-default + ); + --spectrum-textfield-text-color-hover: var( + --spectrum-neutral-content-color-hover + ); + --spectrum-textfield-text-color-focus: var( + --spectrum-neutral-content-color-focus + ); + --spectrum-textfield-text-color-focus-hover: var( + --spectrum-neutral-content-color-focus-hover + ); + --spectrum-textfield-text-color-keyboard-focus: var( + --spectrum-neutral-content-color-key-focus + ); + --spectrum-textfield-text-color-readonly: var( + --spectrum-neutral-content-color-default + ); + --spectrum-textfield-background-color-disabled: var( + --spectrum-disabled-background-color + ); + --spectrum-textfield-border-color-disabled: var( + --spectrum-disabled-border-color + ); + --spectrum-textfield-text-color-disabled: var( + --spectrum-disabled-content-color + ); + --spectrum-textfield-border-color-invalid-default: var( + --spectrum-negative-border-color-default + ); + --spectrum-textfield-border-color-invalid-hover: var( + --spectrum-negative-border-color-hover + ); + --spectrum-textfield-border-color-invalid-focus: var( + --spectrum-negative-border-color-focus + ); + --spectrum-textfield-border-color-invalid-focus-hover: var( + --spectrum-negative-border-color-focus-hover + ); + --spectrum-textfield-border-color-invalid-keyboard-focus: var( + --spectrum-negative-border-color-key-focus + ); + --spectrum-textfield-icon-color-invalid: var( + --spectrum-negative-visual-color + ); + --spectrum-textfield-text-color-invalid: var( + --spectrum-neutral-content-color-default + ); + --spectrum-textfield-text-color-valid: var( + --spectrum-neutral-content-color-default + ); + --spectrum-textfield-icon-color-valid: var( + --spectrum-positive-visual-color + ); + --spectrum-textfield-focus-indicator-color: var( + --spectrum-focus-indicator-color + ); + --spectrum-text-area-min-inline-size: var( + --spectrum-text-area-minimum-width + ); + --spectrum-text-area-min-block-size: var( + --spectrum-text-area-minimum-height + ); + --spectrum-text-area-min-block-size-quiet: var( + --spectrum-component-height-100 + ); +} +.spectrum-Textfield--sizeS { + --spectrum-textfield-height: var(--spectrum-component-height-75); + --spectrum-textfield-label-spacing-block-quiet: var( + --spectrum-field-label-to-component-quiet-small + ); + --spectrum-textfield-label-spacing-inline-side-label: var( + --spectrum-spacing-100 + ); + --spectrum-textfield-placeholder-font-size: var(--spectrum-font-size-75); + --spectrum-textfield-spacing-inline: var( + --spectrum-component-edge-to-text-75 + ); + --spectrum-textfield-icon-size-invalid: var( + --spectrum-workflow-icon-size-75 + ); + --spectrum-textfield-icon-spacing-inline-end-invalid: var( + --spectrum-field-edge-to-alert-icon-small + ); + --spectrum-textfield-icon-spacing-inline-end-valid: var( + --spectrum-field-edge-to-validation-icon-small + ); + --spectrum-textfield-icon-spacing-block-invalid: var( + --spectrum-field-top-to-alert-icon-small + ); + --spectrum-textfield-icon-spacing-block-valid: var( + --spectrum-field-top-to-validation-icon-small + ); + --spectrum-textfield-icon-spacing-inline-start-invalid: var( + --spectrum-field-text-to-alert-icon-small + ); + --spectrum-textfield-icon-spacing-inline-start-valid: var( + --spectrum-field-text-to-validation-icon-small + ); + --spectrum-textfield-character-count-font-size: var( + --spectrum-font-size-75 + ); + --spectrum-textfield-character-count-spacing-block: var( + --spectrum-component-bottom-to-text-75 + ); + --spectrum-textfield-character-count-spacing-block-quiet: var( + --spectrum-character-count-to-field-quiet-small + ); + --spectrum-textfield-character-count-spacing-block-side: var( + --spectrum-side-label-character-count-top-margin-small + ); + --spectrum-text-area-min-block-size-quiet: var( + --spectrum-component-height-75 + ); +} +.spectrum-Textfield--sizeM { + --spectrum-textfield-height: var(--spectrum-component-height-100); + --spectrum-textfield-label-spacing-block-quiet: var( + --spectrum-field-label-to-component-quiet-medium + ); + --spectrum-textfield-label-spacing-inline-side-label: var( + --spectrum-spacing-200 + ); + --spectrum-textfield-placeholder-font-size: var(--spectrum-font-size-100); + --spectrum-textfield-spacing-inline: var( + --spectrum-component-edge-to-text-100 + ); + --spectrum-textfield-icon-size-invalid: var( + --spectrum-workflow-icon-size-100 + ); + --spectrum-textfield-icon-spacing-inline-end-invalid: var( + --spectrum-field-edge-to-alert-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-end-valid: var( + --spectrum-field-edge-to-validation-icon-medium + ); + --spectrum-textfield-icon-spacing-block-invalid: var( + --spectrum-field-top-to-alert-icon-medium + ); + --spectrum-textfield-icon-spacing-block-valid: var( + --spectrum-field-top-to-validation-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-start-invalid: var( + --spectrum-field-text-to-alert-icon-medium + ); + --spectrum-textfield-icon-spacing-inline-start-valid: var( + --spectrum-field-text-to-validation-icon-medium + ); + --spectrum-textfield-character-count-font-size: var( + --spectrum-font-size-75 + ); + --spectrum-textfield-character-count-spacing-block: var( + --spectrum-component-bottom-to-text-75 + ); + --spectrum-textfield-character-count-spacing-block-quiet: var( + --spectrum-character-count-to-field-quiet-medium + ); + --spectrum-textfield-character-count-spacing-block-side: var( + --spectrum-side-label-character-count-top-margin-medium + ); + --spectrum-text-area-min-block-size-quiet: var( + --spectrum-component-height-100 + ); +} +.spectrum-Textfield--sizeL { + --spectrum-textfield-height: var(--spectrum-component-height-200); + --spectrum-textfield-label-spacing-block-quiet: var( + --spectrum-field-label-to-component-quiet-large + ); + --spectrum-textfield-label-spacing-inline-side-label: var( + --spectrum-spacing-200 + ); + --spectrum-textfield-placeholder-font-size: var(--spectrum-font-size-200); + --spectrum-textfield-spacing-inline: var( + --spectrum-component-edge-to-text-200 + ); + --spectrum-textfield-icon-size-invalid: var( + --spectrum-workflow-icon-size-200 + ); + --spectrum-textfield-icon-spacing-inline-end-invalid: var( + --spectrum-field-edge-to-alert-icon-large + ); + --spectrum-textfield-icon-spacing-inline-end-valid: var( + --spectrum-field-edge-to-validation-icon-large + ); + --spectrum-textfield-icon-spacing-block-invalid: var( + --spectrum-field-top-to-alert-icon-large + ); + --spectrum-textfield-icon-spacing-block-valid: var( + --spectrum-field-top-to-validation-icon-large + ); + --spectrum-textfield-icon-spacing-inline-start-invalid: var( + --spectrum-field-text-to-alert-icon-large + ); + --spectrum-textfield-icon-spacing-inline-start-valid: var( + --spectrum-field-text-to-validation-icon-large + ); + --spectrum-textfield-character-count-font-size: var( + --spectrum-font-size-100 + ); + --spectrum-textfield-character-count-spacing-block: var( + --spectrum-component-bottom-to-text-100 + ); + --spectrum-textfield-character-count-spacing-block-quiet: var( + --spectrum-character-count-to-field-quiet-large + ); + --spectrum-textfield-character-count-spacing-block-side: var( + --spectrum-side-label-character-count-top-margin-large + ); + --spectrum-text-area-min-block-size-quiet: var( + --spectrum-component-height-200 + ); +} +.spectrum-Textfield--sizeXL { + --spectrum-textfield-height: var(--spectrum-component-height-300); + --spectrum-textfield-label-spacing-block-quiet: var( + --spectrum-field-label-to-component-quiet-extra-large + ); + --spectrum-textfield-label-spacing-inline-side-label: var( + --spectrum-spacing-200 + ); + --spectrum-textfield-placeholder-font-size: var(--spectrum-font-size-300); + --spectrum-textfield-spacing-inline: var( + --spectrum-component-edge-to-text-200 + ); + --spectrum-textfield-icon-size-invalid: var( + --spectrum-workflow-icon-size-300 + ); + --spectrum-textfield-icon-spacing-inline-end-invalid: var( + --spectrum-field-edge-to-alert-icon-extra-large + ); + --spectrum-textfield-icon-spacing-inline-end-valid: var( + --spectrum-field-edge-to-validation-icon-extra-large + ); + --spectrum-textfield-icon-spacing-block-invalid: var( + --spectrum-field-top-to-alert-icon-extra-large + ); + --spectrum-textfield-icon-spacing-block-valid: var( + --spectrum-field-top-to-validation-icon-extra-large + ); + --spectrum-textfield-icon-spacing-inline-start-invalid: var( + --spectrum-field-text-to-alert-icon-extra-large + ); + --spectrum-textfield-icon-spacing-inline-start-valid: var( + --spectrum-field-text-to-validation-icon-extra-large + ); + --spectrum-textfield-character-count-font-size: var( + --spectrum-font-size-200 + ); + --spectrum-textfield-character-count-spacing-block: var( + --spectrum-component-bottom-to-text-200 + ); + --spectrum-textfield-character-count-spacing-block-quiet: var( + --spectrum-character-count-to-field-quiet-extra-large + ); + --spectrum-textfield-character-count-spacing-block-side: var( + --spectrum-side-label-character-count-top-margin-extra-large + ); + --spectrum-text-area-min-block-size-quiet: var( + --spectrum-component-height-300 + ); +} +.spectrum-Textfield { + -moz-appearance: textfield; + display: inline-grid; + grid-template-columns: auto auto; + grid-template-rows: auto auto auto; + inline-size: var(--mod-textfield-width, var(--spectrum-textfield-width)); + margin: 0; + overflow: visible; + position: relative; + text-indent: 0; + text-overflow: ellipsis; +} +.spectrum-Textfield.spectrum-Textfield--quiet:after { + block-size: var( + --mod-textfield-focus-indicator-width, + var(--spectrum-textfield-focus-indicator-width) + ); + bottom: calc( + ( + var( + --mod-textfield-focus-indicator-gap, + var(--spectrum-textfield-focus-indicator-gap) + ) + + var( + --mod-textfield-focus-indicator-width, + var(--spectrum-textfield-focus-indicator-width) + ) + ) * -1 + ); + content: ''; + inline-size: 100%; + left: 0; + position: absolute; +} +.spectrum-Textfield.spectrum-Textfield--quiet.focus-visible:after, +.spectrum-Textfield.spectrum-Textfield--quiet.is-keyboardFocused:after, +.spectrum-Textfield.spectrum-Textfield--quiet:focus-within:after { + background-color: var( + --highcontrast-textfield-focus-indicator-color, + var( + --mod-textfield-focus-indicator-color, + var(--spectrum-textfield-focus-indicator-color) + ) + ); +} +.spectrum-Textfield.spectrum-Textfield--quiet.is-keyboardFocused:after, +.spectrum-Textfield.spectrum-Textfield--quiet:focus-visible:after, +.spectrum-Textfield.spectrum-Textfield--quiet:focus-within:after { + background-color: var( + --highcontrast-textfield-focus-indicator-color, + var( + --mod-textfield-focus-indicator-color, + var(--spectrum-textfield-focus-indicator-color) + ) + ); +} +.spectrum-Textfield.is-invalid .spectrum-Textfield-validationIcon, +.spectrum-Textfield.is-valid .spectrum-Textfield-validationIcon { + grid-area: 2/2; + margin-inline-start: auto; + pointer-events: all; + position: absolute; + top: 0; +} +.spectrum-Textfield.is-valid .spectrum-Textfield-validationIcon { + color: var( + --highcontrast-textfield-icon-color-valid, + var( + --mod-textfield-icon-color-valid, + var(--spectrum-textfield-icon-color-valid) + ) + ); + inset-block-end: var( + --mod-textfield-icon-spacing-block-valid, + var(--spectrum-textfield-icon-spacing-block-valid) + ); + inset-block-start: var( + --mod-textfield-icon-spacing-block-valid, + var(--spectrum-textfield-icon-spacing-block-valid) + ); + inset-inline-end: var( + --mod-textfield-icon-spacing-inline-end-valid, + var(--spectrum-textfield-icon-spacing-inline-end-valid) + ); + inset-inline-start: var( + --mod-textfield-icon-spacing-inline-start-valid, + var(--spectrum-textfield-icon-spacing-inline-start-valid) + ); +} +.spectrum-Textfield.is-invalid .spectrum-Textfield-validationIcon { + block-size: var( + --mod-textfield-icon-size-invalid, + var(--spectrum-textfield-icon-size-invalid) + ); + color: var( + --highcontrast-textfield-icon-color-invalid, + var( + --mod-textfield-icon-color-invalid, + var(--spectrum-textfield-icon-color-invalid) + ) + ); + inline-size: var( + --mod-textfield-icon-size-invalid, + var(--spectrum-textfield-icon-size-invalid) + ); + inset-block-end: var( + --mod-textfield-icon-spacing-block-invalid, + var(--spectrum-textfield-icon-spacing-block-invalid) + ); + inset-block-start: var( + --mod-textfield-icon-spacing-block-invalid, + var(--spectrum-textfield-icon-spacing-block-invalid) + ); + inset-inline-end: var( + --mod-textfield-icon-spacing-inline-end-invalid, + var(--spectrum-textfield-icon-spacing-inline-end-invalid) + ); + inset-inline-start: var( + --mod-textfield-icon-spacing-inline-start-invalid, + var(--spectrum-textfield-icon-spacing-inline-start-invalid) + ); +} +.spectrum-Textfield.is-disabled .spectrum-Textfield-validationIcon, +.spectrum-Textfield.is-readOnly .spectrum-Textfield-validationIcon { + color: #0000; +} +.spectrum-Textfield--quiet .spectrum-Textfield-validationIcon { + padding-inline-end: 0; +} +.spectrum-Textfield--quiet.is-valid .spectrum-Textfield-validationIcon { + inset-inline-end: var( + --mod-textfield-icon-spacing-inline-end-quiet-valid, + var(--spectrum-textfield-icon-spacing-inline-end-quiet-valid) + ); +} +.spectrum-Textfield--quiet.is-invalid .spectrum-Textfield-validationIcon { + inset-inline-end: var( + --mod-textfield-icon-spacing-inline-end-quiet-invalid, + var(--spectrum-textfield-icon-spacing-inline-end-quiet-invalid) + ); +} +.spectrum-InputGroup .spectrum-Textfield-validationIcon { + margin-inline-end: var( + --spectrum-textfield-icon-spacing-inline-end-override + ); +} +.spectrum-Textfield .spectrum-FieldLabel { + grid-area: 1/1 / auto/span 1; + margin-block-end: var( + --mod-textfield-label-spacing-block, + var(--spectrum-textfield-label-spacing-block) + ); + padding-left: calc( + var( + --mod-textfield-corner-radius, + var(--spectrum-textfield-corner-radius) + ) / 2 + ); +} +.spectrum-Textfield--quiet .spectrum-FieldLabel { + margin-block-end: var( + --mod-textfield-label-spacing-block-quiet, + var(--spectrum-textfield-label-spacing-block-quiet) + ); +} +.is-disabled .spectrum-FieldLabel { + color: var(--spectrum-textfield-text-color-disabled); +} +.spectrum-Textfield .spectrum-HelpText { + grid-area: 3/1 / auto/span 2; + margin-block-start: var( + --mod-textfield-helptext-spacing-block, + var(--spectrum-textfield-helptext-spacing-block) + ); + padding-left: calc( + var( + --mod-textfield-corner-radius, + var(--spectrum-textfield-corner-radius) + ) / 2 + ); +} +.spectrum-Textfield-characterCount { + align-items: flex-end; + display: inline-flex; + font-family: var( + --mod-textfield-character-count-font-family, + var(--spectrum-textfield-character-count-font-family) + ); + font-size: var( + --mod-textfield-character-count-font-size, + var(--spectrum-textfield-character-count-font-size) + ); + font-weight: var( + --mod-textfield-character-count-font-weight, + var(--spectrum-textfield-character-count-font-weight) + ); + grid-area: 1/2 / auto/span 1; + justify-content: flex-end; + margin-block-end: var( + --mod-textfield-character-count-spacing-block, + var(--spectrum-textfield-character-count-spacing-block) + ); + margin-inline-end: 0; + margin-inline-start: var( + --mod-textfield-character-count-spacing-inline, + var(--spectrum-textfield-character-count-spacing-inline) + ); + padding-right: calc( + var( + --mod-textfield-corner-radius, + var(--spectrum-textfield-corner-radius) + ) / 2 + ); + width: auto; +} +.spectrum-Textfield--quiet .spectrum-Textfield-characterCount { + margin-block-end: var( + --mod-textfield-character-count-spacing-block-quiet, + var(--spectrum-textfield-character-count-spacing-block-quiet) + ); +} +.spectrum-Textfield-input { + -webkit-appearance: none; + -moz-appearance: textfield; + background-color: var( + --mod-textfield-background-color, + var(--spectrum-textfield-background-color) + ); + block-size: var(--mod-textfield-height, var(--spectrum-textfield-height)); + border: var( + --mod-textfield-border-width, + var(--spectrum-textfield-border-width) + ) + solid + var( + --highcontrast-textfield-border-color, + var( + --mod-textfield-border-color, + var(--spectrum-textfield-border-color) + ) + ); + border-radius: var( + --mod-textfield-corner-radius, + var(--spectrum-textfield-corner-radius) + ); + box-sizing: border-box; + color: var( + --highcontrast-textfield-text-color-default, + var( + --mod-textfield-text-color-default, + var(--spectrum-textfield-text-color-default) + ) + ); + font-family: var( + --mod-textfield-font-family, + var(--spectrum-textfield-font-family) + ); + font-size: var( + --mod-textfield-placeholder-font-size, + var(--spectrum-textfield-placeholder-font-size) + ); + font-weight: var( + --mod-textfield-font-weight, + var(--spectrum-textfield-font-weight) + ); + grid-area: 2/1 / auto/span 2; + inline-size: 100%; + margin: 0; + min-inline-size: var( + --mod-textfield-min-width, + var(--spectrum-textfield-min-width) + ); + outline: none; + overflow: visible; + padding-block-end: calc( + var( + --mod-textfield-spacing-block-end, + var(--spectrum-textfield-spacing-block-end) + ) - + var( + --mod-textfield-border-width, + var(--spectrum-textfield-border-width) + ) + ); + padding-block-start: calc( + var( + --mod-textfield-spacing-block-start, + var(--spectrum-textfield-spacing-block-start) + ) - + var( + --mod-textfield-border-width, + var(--spectrum-textfield-border-width) + ) + ); + padding-inline: calc( + var( + --mod-textfield-spacing-inline, + var(--spectrum-textfield-spacing-inline) + ) - + var( + --mod-textfield-border-width, + var(--spectrum-textfield-border-width) + ) + ); + text-indent: 0; + text-overflow: ellipsis; + transition: border-color + var( + --mod-texfield-animation-duration, + var(--spectrum-texfield-animation-duration) + ) + ease-in-out; + vertical-align: top; +} +.spectrum-Textfield--quiet + .spectrum-Textfield-icon + ~ .spectrum-Textfield-input { + padding-inline-start: calc( + var( + --mod--Textfield-workflow-icon-gap, + var(--spectrum-Textfield-workflow-icon-gap) + ) + + var( + --mod-Textfield-workflow-icon-width, + var(--spectrum-Textfield-workflow-icon-width) + ) + ); +} +.spectrum-Textfield-input::-ms-clear { + block-size: 0; + inline-size: 0; +} +.spectrum-Textfield-input::-webkit-inner-spin-button, +.spectrum-Textfield-input::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +.spectrum-Textfield-input:-moz-ui-invalid { + box-shadow: none; +} +.spectrum-Textfield-input::placeholder { + color: var( + --highcontrast-textfield-text-color-default, + var( + --mod-textfield-text-color-default, + var(--spectrum-textfield-text-color-default) + ) + ); + font-family: var( + --mod-textfield-font-family, + var(--spectrum-textfield-font-family) + ); + font-size: var( + --mod-textfield-placeholder-font-size, + var(--spectrum-textfield-placeholder-font-size) + ); + font-weight: var( + --mod-textfield-font-weight, + var(--spectrum-textfield-font-weight) + ); + opacity: 1; + transition: color + var( + --mod-texfield-animation-duration, + var(--spectrum-texfield-animation-duration) + ) + ease-in-out; +} +.spectrum-Textfield-input:lang(ja)::placeholder, +.spectrum-Textfield-input:lang(ko)::placeholder, +.spectrum-Textfield-input:lang(zh)::placeholder { + font-style: normal; +} +.spectrum-Textfield-input:lang(ja)::-moz-placeholder, +.spectrum-Textfield-input:lang(ko)::-moz-placeholder, +.spectrum-Textfield-input:lang(zh)::-moz-placeholder { + font-style: normal; +} +.spectrum-Textfield-input:hover, +.spectrum-Textfield:hover .spectrum-Textfield-input { + border-color: var( + --highcontrast-textfield-border-color-hover, + var( + --mod-textfield-border-color-hover, + var(--spectrum-textfield-border-color-hover) + ) + ); + color: var( + --highcontrast-textfield-text-color-hover, + var( + --mod-textfield-text-color-hover, + var(--spectrum-textfield-text-color-hover) + ) + ); +} +.spectrum-Textfield-input:hover::placeholder, +.spectrum-Textfield:hover .spectrum-Textfield-input::placeholder { + color: var( + --highcontrast-textfield-text-color-hover, + var( + --mod-textfield-text-color-hover, + var(--spectrum-textfield-text-color-hover) + ) + ); +} +.is-focused .spectrum-Textfield-input, +.spectrum-Textfield-input:focus { + border-color: var( + --highcontrast-textfield-border-color-focus, + var( + --mod-textfield-border-color-focus, + var(--spectrum-textfield-border-color-focus) + ) + ); + color: var( + --highcontrast-textfield-text-color-focus, + var( + --mod-textfield-text-color-focus, + var(--spectrum-textfield-text-color-focus) + ) + ); +} +.is-focused .spectrum-Textfield-input::placeholder, +.spectrum-Textfield-input:focus::placeholder { + color: var( + --highcontrast-textfield-text-color-focus, + var( + --mod-textfield-text-color-focus, + var(--spectrum-textfield-text-color-focus) + ) + ); +} +.is-focused .spectrum-Textfield-input:hover, +.spectrum-Textfield-input:focus:hover { + border-color: var( + --highcontrast-textfield-border-color-focus-hover, + var( + --mod-textfield-border-color-focus-hover, + var(--spectrum-textfield-border-color-focus-hover) + ) + ); + color: var( + --highcontrast-textfield-text-color-focus-hover, + var( + --mod-textfield-text-color-focus-hover, + var(--spectrum-textfield-text-color-focus-hover) + ) + ); +} +.is-focused .spectrum-Textfield-input:hover::placeholder, +.spectrum-Textfield-input:focus:hover::placeholder { + color: var( + --highcontrast-textfield-text-color-focus-hover, + var( + --mod-textfield-text-color-focus-hover, + var(--spectrum-textfield-text-color-focus-hover) + ) + ); +} +.is-keyboardFocused .spectrum-Textfield-input, +.spectrum-Textfield-input.focus-visible { + border-color: var( + --highcontrast-textfield-border-color-keyboard-focus, + var( + --mod-textfield-border-color-keyboard-focus, + var(--spectrum-textfield-border-color-keyboard-focus) + ) + ); + color: var( + --highcontrast-textfield-text-color-keyboard-focus, + var( + --mod-textfield-text-color-keyboard-focus, + var(--spectrum-textfield-text-color-keyboard-focus) + ) + ); + outline: var( + --mod-textfield-focus-indicator-width, + var(--spectrum-textfield-focus-indicator-width) + ) + solid; + outline-color: var( + --highcontrast-textfield-focus-indicator-color, + var( + --mod-textfield-focus-indicator-color, + var(--spectrum-textfield-focus-indicator-color) + ) + ); + outline-offset: var( + --mod-textfield-focus-indicator-gap, + var(--spectrum-textfield-focus-indicator-gap) + ); +} +.is-keyboardFocused .spectrum-Textfield-input, +.spectrum-Textfield-input:focus-visible { + border-color: var( + --highcontrast-textfield-border-color-keyboard-focus, + var( + --mod-textfield-border-color-keyboard-focus, + var(--spectrum-textfield-border-color-keyboard-focus) + ) + ); + color: var( + --highcontrast-textfield-text-color-keyboard-focus, + var( + --mod-textfield-text-color-keyboard-focus, + var(--spectrum-textfield-text-color-keyboard-focus) + ) + ); + outline: var( + --mod-textfield-focus-indicator-width, + var(--spectrum-textfield-focus-indicator-width) + ) + solid; + outline-color: var( + --highcontrast-textfield-focus-indicator-color, + var( + --mod-textfield-focus-indicator-color, + var(--spectrum-textfield-focus-indicator-color) + ) + ); + outline-offset: var( + --mod-textfield-focus-indicator-gap, + var(--spectrum-textfield-focus-indicator-gap) + ); +} +.is-keyboardFocused .spectrum-Textfield-input::placeholder, +.spectrum-Textfield-input.focus-visible::placeholder { + color: var( + --highcontrast-textfield-text-color-keyboard-focus, + var( + --mod-textfield-text-color-keyboard-focus, + var(--spectrum-textfield-text-color-keyboard-focus) + ) + ); +} +.is-keyboardFocused .spectrum-Textfield-input::placeholder, +.spectrum-Textfield-input:focus-visible::placeholder { + color: var( + --highcontrast-textfield-text-color-keyboard-focus, + var( + --mod-textfield-text-color-keyboard-focus, + var(--spectrum-textfield-text-color-keyboard-focus) + ) + ); +} +.is-valid .spectrum-Textfield-input { + color: var( + --highcontrast-textfield-text-color-valid, + var( + --mod-textfield-text-color-valid, + var(--spectrum-textfield-text-color-valid) + ) + ); +} +.is-invalid .spectrum-Textfield-input { + border-color: var( + --highcontrast-textfield-border-color-invalid-default, + var( + --mod-textfield-border-color-invalid-default, + var(--spectrum-textfield-border-color-invalid-default) + ) + ); + color: var( + --highcontrast-textfield-text-color-invalid, + var( + --mod-textfield-text-color-invalid, + var(--spectrum-textfield-text-color-invalid) + ) + ); +} +.is-invalid .spectrum-Textfield-input:hover, +.is-invalid:hover .spectrum-Textfield-input { + border-color: var( + --highcontrast-textfield-border-color-invalid-hover, + var( + --mod-textfield-border-color-invalid-hover, + var(--spectrum-textfield-border-color-invalid-hover) + ) + ); +} +.is-invalid .spectrum-Textfield-input:focus, +.is-invalid.is-focused .spectrum-Textfield-input, +.is-invalid:focus .spectrum-Textfield-input { + border-color: var( + --highcontrast-textfield-border-color-invalid-focus, + var( + --mod-textfield-border-color-invalid-focus, + var(--spectrum-textfield-border-color-invalid-focus) + ) + ); +} +.is-invalid .spectrum-Textfield-input:focus:hover, +.is-invalid.is-focused .spectrum-Textfield-input:hover, +.is-invalid:focus .spectrum-Textfield-input:hover { + border-color: var( + --highcontrast-textfield-border-color-invalid-focus-hover, + var( + --mod-textfield-border-color-invalid-focus-hover, + var(--spectrum-textfield-border-color-invalid-focus-hover) + ) + ); +} +.is-invalid .spectrum-Textfield-input.focus-visible, +.is-invalid.is-keyboardFocused .spectrum-Textfield-input { + border-color: var( + --highcontrast-textfield-border-color-invalid-keyboard-focus, + var( + --mod-textfield-border-color-invalid-keyboard-focus, + var(--spectrum-textfield-border-color-invalid-keyboard-focus) + ) + ); +} +.is-invalid .spectrum-Textfield-input:focus-visible, +.is-invalid.is-keyboardFocused .spectrum-Textfield-input { + border-color: var( + --highcontrast-textfield-border-color-invalid-keyboard-focus, + var( + --mod-textfield-border-color-invalid-keyboard-focus, + var(--spectrum-textfield-border-color-invalid-keyboard-focus) + ) + ); +} +.spectrum-Textfield-input:disabled, +.spectrum-Textfield.is-disabled .spectrum-Textfield-input, +.spectrum-Textfield.is-disabled:hover .spectrum-Textfield-input { + -webkit-text-fill-color: var( + --highcontrast-textfield-text-color-disabled, + var( + --mod-textfield-text-color-disabled, + var(--spectrum-textfield-text-color-disabled) + ) + ); + background-color: var( + --mod-textfield-background-color-disabled, + var(--spectrum-textfield-background-color-disabled) + ); + border-color: #0000; + color: var( + --highcontrast-textfield-text-color-disabled, + var( + --mod-textfield-text-color-disabled, + var(--spectrum-textfield-text-color-disabled) + ) + ); + opacity: 1; + resize: none; +} +.spectrum-Textfield-input:disabled::placeholder, +.spectrum-Textfield.is-disabled .spectrum-Textfield-input::placeholder, +.spectrum-Textfield.is-disabled:hover .spectrum-Textfield-input::placeholder { + color: var( + --highcontrast-textfield-text-color-disabled, + var( + --mod-textfield-text-color-disabled, + var(--spectrum-textfield-text-color-disabled) + ) + ); +} +.spectrum-Textfield--quiet .spectrum-Textfield-input { + background-color: #0000; + border-block-start-width: 0; + border-inline-width: 0; + border-radius: 0; + margin-block-end: var( + --mod-textfield-spacing-block-quiet, + var(--spectrum-textfield-spacing-block-quiet) + ); + outline: none; + overflow-y: hidden; + padding-block-start: var( + --mod-textfield-spacing-block-start, + var(--spectrum-textfield-spacing-block-start) + ); + padding-inline: var( + --mod-textfield-spacing-inline-quiet, + var(--spectrum-textfield-spacing-inline-quiet) + ); + resize: none; +} +.spectrum-Textfield--quiet.is-disabled .spectrum-Textfield-input, +.spectrum-Textfield--quiet.is-disabled:hover .spectrum-Textfield-input, +.spectrum-Textfield-input:disabled { + background-color: #0000; + border-color: var( + --mod-textfield-border-color-disabled, + var(--spectrum-textfield-border-color-disabled) + ); + color: var( + --highcontrast-textfield-text-color-disabled, + var( + --mod-textfield-text-color-disabled, + var(--spectrum-textfield-text-color-disabled) + ) + ); +} +.spectrum-Textfield--quiet.is-disabled .spectrum-Textfield-input::placeholder, +.spectrum-Textfield--quiet.is-disabled:hover + .spectrum-Textfield-input::placeholder, +.spectrum-Textfield-input:disabled::placeholder { + color: var( + --highcontrast-textfield-text-color-disabled, + var( + --mod-textfield-text-color-disabled, + var(--spectrum-textfield-text-color-disabled) + ) + ); +} +.spectrum-Textfield-input:read-only, +.spectrum-Textfield.is-readOnly .spectrum-Textfield-input, +.spectrum-Textfield.is-readOnly:hover .spectrum-Textfield-input { + background-color: #0000; + border-color: #0000; + color: var( + --highcontrast-textfield-text-color-readonly, + var( + --mod-textfield-text-color-readonly, + var(--spectrum-textfield-text-color-readonly) + ) + ); + outline: none; +} +.spectrum-Textfield-input:read-only::placeholder, +.spectrum-Textfield.is-readOnly .spectrum-Textfield-input::placeholder, +.spectrum-Textfield.is-readOnly:hover .spectrum-Textfield-input::placeholder { + background-color: #0000; + color: var( + --highcontrast-textfield-text-color-readonly, + var( + --mod-textfield-text-color-readonly, + var(--spectrum-textfield-text-color-readonly) + ) + ); +} +.spectrum-Textfield--sideLabel { + grid-template-columns: auto auto auto; + grid-template-rows: auto auto; +} +.spectrum-Textfield--sideLabel:after { + grid-area: 1/2 / span 1 / span 1; +} +.spectrum-Textfield--sideLabel .spectrum-FieldLabel { + grid-area: 1/1 / span 2 / span 1; + margin-inline-end: var( + --mod-textfield-label-spacing-inline-side-label, + var(--spectrum-textfield-label-spacing-inline-side-label) + ); +} +.spectrum-Textfield--sideLabel .spectrum-Textfield-characterCount { + align-items: flex-start; + grid-area: 1/3 / auto/span 1; + margin-block-start: var( + --mod-textfield-character-count-spacing-block-side, + var(--spectrum-textfield-character-count-spacing-block-side) + ); + margin-inline-start: var( + --mod-textfield-character-count-spacing-inline-side, + var(--spectrum-textfield-character-count-spacing-inline-side) + ); +} +.spectrum-Textfield--sideLabel .spectrum-HelpText { + grid-area: 2/2 / auto/span 1; +} +.spectrum-Textfield--sideLabel .spectrum-Textfield-input, +.spectrum-Textfield--sideLabel .spectrum-Textfield-validationIcon { + grid-area: 1/2 / span 1 / span 1; +} +.spectrum-Textfield--multiline .spectrum-Textfield-input { + min-block-size: var( + --mod-text-area-min-block-size, + var(--spectrum-text-area-min-block-size) + ); + min-inline-size: var( + --mod-text-area-min-inline-size, + var(--spectrum-text-area-min-inline-size) + ); + resize: inherit; +} +.spectrum-Textfield--multiline.spectrum-Textfield--grows + .spectrum-Textfield-input { + grid-row: 1; +} +.spectrum-Textfield--multiline.spectrum-Textfield--quiet + .spectrum-Textfield-input { + min-block-size: var( + --mod-text-area-min-block-size-quiet, + var(--spectrum-text-area-min-block-size-quiet) + ); + overflow-y: hidden; + resize: none; +} +@media (forced-colors: active) { + .spectrum-Textfield { + --highcontrast-textfield-border-color-hover: Highlight; + --highcontrast-textfield-border-color-focus: Highlight; + --highcontrast-textfield-border-color-keyboard-focus: CanvasText; + --highcontrast-textfield-focus-indicator-color: Highlight; + --highcontrast-textfield-border-color-invalid-default: Highlight; + --highcontrast-textfield-border-color-invalid-hover: Highlight; + --highcontrast-textfield-border-color-invalid-focus: Highlight; + --highcontrast-textfield-border-color-invalid-keyboard-focus: Highlight; + --highcontrast-textfield-text-color-valid: CanvasText; + --highcontrast-textfield-text-color-invalid: CanvasText; + } + .spectrum-Textfield .spectrum-Textfield-input { + --highcontrast-textfield-text-color-default: CanvasText; + --highcontrast-textfield-text-color-hover: CanvasText; + --highcontrast-textfield-text-color-keyboard-focus: CanvasText; + --highcontrast-textfield-text-color-disabled: GrayText; + --highcontrast-textfield-text-color-readonly: CanvasText; + } + .spectrum-Textfield .spectrum-Textfield-input::placeholder { + --highcontrast-textfield-text-color-default: GrayText; + --highcontrast-textfield-text-color-hover: GrayText; + --highcontrast-textfield-text-color-keyboard-focus: GrayText; + --highcontrast-textfield-text-color-disabled: GrayText; + --highcontrast-textfield-text-color-readonly: CanvasText; + } +} +.spectrum-Textfield { + --spectrum-textfield-border-color: var( + --system-spectrum-textfield-border-color + ); + --spectrum-textfield-border-color-hover: var( + --system-spectrum-textfield-border-color-hover + ); + --spectrum-textfield-border-color-focus: var( + --system-spectrum-textfield-border-color-focus + ); + --spectrum-textfield-border-color-focus-hover: var( + --system-spectrum-textfield-border-color-focus-hover + ); + --spectrum-textfield-border-color-keyboard-focus: var( + --system-spectrum-textfield-border-color-keyboard-focus + ); + --spectrum-textfield-border-width: var( + --system-spectrum-textfield-border-width + ); +} diff --git a/packages/input-segments/src/types.ts b/packages/input-segments/src/types.ts new file mode 100644 index 00000000000..c8aaba38111 --- /dev/null +++ b/packages/input-segments/src/types.ts @@ -0,0 +1,67 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +export const dateSegmentTypes: Intl.DateTimeFormatPartTypes[] = [ + 'day', + 'month', + 'year', + 'literal', +]; + +export const timeSegmentTypes: Intl.DateTimeFormatPartTypes[] = [ + 'hour', + 'minute', + 'second', + 'dayPeriod', + 'literal', +]; + +export interface Segment extends Omit { + /** A placeholder string for the segment */ + placeholder?: string; + + /** The formatted text for the segment */ + formatted?: string; + + /** The numeric value for the segment, if applicable */ + value?: number; + + /** The minimum numeric value for the segment, if applicable */ + minValue?: number; + + /** The maximum numeric value for the segment, if applicable */ + maxValue?: number; +} + +export type SegmentValueAndLimits = Pick< + Segment, + 'value' | 'minValue' | 'maxValue' +>; + +export type TimeGranularity = 'hour' | 'minute' | 'second'; + +/** AM modifier: `0` hours */ +export const AM = 0; + +/** PM modifier: `12` hours */ +export const PM = 12; + +/** Minimum hour value if AM */ +export const minHourAM = AM; + +/** Maximum hour value if AM */ +export const maxHourAM = 11; + +/** Minimum hour value if PM */ +export const minHourPM = PM; + +/** Maximum hour value if PM */ +export const maxHourPM = 23; diff --git a/packages/input-segments/test/benchmark/basic-test.ts b/packages/input-segments/test/benchmark/basic-test.ts new file mode 100644 index 00000000000..6ca464b0d49 --- /dev/null +++ b/packages/input-segments/test/benchmark/basic-test.ts @@ -0,0 +1,18 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import '@spectrum-web-components/input-segments/sp-input-segments.js'; +import { html } from '@spectrum-web-components/base'; +import { measureFixtureCreation } from '../../../../test/benchmark/helpers.js'; + +measureFixtureCreation(html` + +`); diff --git a/packages/input-segments/test/input-segments.test.ts b/packages/input-segments/test/input-segments.test.ts new file mode 100644 index 00000000000..18860bbcd5e --- /dev/null +++ b/packages/input-segments/test/input-segments.test.ts @@ -0,0 +1,38 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; + +import '../sp-input-segments.js'; +import { InputSegments } from '..'; +import { testForLitDevWarnings } from '../../../test/testing-helpers.js'; + +describe('InputSegments', () => { + testForLitDevWarnings( + async () => + await fixture( + html` + + ` + ) + ); + it('loads default input-segments accessibly', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + await expect(el).to.be.accessible(); + }); +}); diff --git a/packages/input-segments/tsconfig.json b/packages/input-segments/tsconfig.json new file mode 100644 index 00000000000..73f3c3581e0 --- /dev/null +++ b/packages/input-segments/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "./" + }, + "include": ["*.ts", "src/*.ts"], + "exclude": ["test/*.ts", "stories/*.ts"], + "references": [ + { "path": "../../tools/base" }, + { "path": "../../tools/reactive-controllers" }, + { "path": "../textfield" } + ] +} diff --git a/tools/bundle/package.json b/tools/bundle/package.json index 2b26ae4bc7b..06f883bad09 100644 --- a/tools/bundle/package.json +++ b/tools/bundle/package.json @@ -96,6 +96,7 @@ "@spectrum-web-components/icons-workflow": "^0.35.0", "@spectrum-web-components/iconset": "^0.35.0", "@spectrum-web-components/illustrated-message": "^0.35.0", + "@spectrum-web-components/input-segments": "^0.0.1", "@spectrum-web-components/link": "^0.35.0", "@spectrum-web-components/menu": "^0.35.0", "@spectrum-web-components/meter": "^0.35.0", diff --git a/tsconfig-all.json b/tsconfig-all.json index a3ad7b0c079..0ba780868c2 100644 --- a/tsconfig-all.json +++ b/tsconfig-all.json @@ -50,6 +50,7 @@ { "path": "packages/icons-workflow" }, { "path": "packages/iconset" }, { "path": "packages/illustrated-message" }, + { "path": "packages/input-segments" }, { "path": "packages/link" }, { "path": "packages/menu" }, { "path": "packages/meter" }, diff --git a/yarn.lock b/yarn.lock index fceae58a961..71b31edf303 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,11 +2500,11 @@ integrity sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA== "@internationalized/date@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.4.0.tgz#e843ac40b04afafe99fe0a41bae7abdd221a9a44" - integrity sha512-QUDSGCsvrEVITVf+kv9VSAraAmCgjQmU5CiXtesUBBhBe374NmnEIIaOFBZ72t29dfGMBP0zF+v6toVnbcc6jg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.2.0.tgz#1d266e5e5543a059cf8cca9b954fa033c3e58a75" + integrity sha512-VDMHN1m33L4eqPs5BaihzgQJXyaORbMoHOtrapFxx179J8ucY5CRIHYsq5RRLKPHZWgjNfa5v6amWWDkkMFywA== dependencies: - "@swc/helpers" "^0.5.0" + "@swc/helpers" "^0.4.14" "@internationalized/number@^3.1.0": version "3.1.2" @@ -5369,9 +5369,9 @@ integrity sha512-N5d0REziOHWL4pBHFaWzxdnJgBjCSAy4IVkJCIaoQXXqBCSisjfzgepUSiPbS8yekn78InUwKNUyvS866R0MqQ== "@spectrum-css/calendar@^3.2.6": - version "3.2.7" - resolved "https://registry.yarnpkg.com/@spectrum-css/calendar/-/calendar-3.2.7.tgz#10fd44176b6afbdf5baf29ce16728baa98b0d844" - integrity sha512-e2BGyuXzP+VOv0q855EIgrR+ne7e/EP8AMMuSAWazgq2fPZ4CoJIeLYP3tnniKnj2dlb3Gr1LH+6MPlUXS74RA== + version "3.2.6" + resolved "https://registry.yarnpkg.com/@spectrum-css/calendar/-/calendar-3.2.6.tgz#edd700b5b6188a67711087c9604c7dda9ac8920a" + integrity sha512-bB5CXl6B4zpJizEXmRGvv+WJHImXi2D4VaRL0Yi7EBmswYfi0eEjN6CiKljoqe+MBc5nHPp4pimI08Al08q2MA== "@spectrum-css/card@^6.0.9": version "6.0.9" @@ -5598,6 +5598,11 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-6.0.11.tgz#a4dbd8c2da1a317cd3c7208317364fe7a1026213" integrity sha512-lbqVZTUpk4UV18/8wRMTi8/0UwTGEKV5ocysNIeRSEhvHfO4ktQzhyn+T92E7TkY/WqKljEM8EnjX/K6hwUBNQ== +"@spectrum-css/textfield@^6.0.7": + version "6.0.7" + resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-6.0.7.tgz#85c337966bf71d211c27c33eb3741d22200cd9b3" + integrity sha512-KCEJYr/bX+LwAZ08IcbQCVD4X5oo+LUWVA69Ndf81B3aNE83Egl0VyrlcdS5U8zbufNk0ltSZkCW0Nq6NdpNaQ== + "@spectrum-css/thumbnail@^3.0.17": version "3.0.17" resolved "https://registry.yarnpkg.com/@spectrum-css/thumbnail/-/thumbnail-3.0.17.tgz#1cd2f00e5b374aa0617ba3061c325f5174598ad3" @@ -5638,9 +5643,84 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-9.0.8.tgz#6af3bcdace903b8461f5fcd4c9aa23e70128a456" integrity sha512-rGfd7jqXOdR69bEjrRP58ynuIeJU0czPfwQvzhtCzg7jKVukV+efNHqrs086sC6xutB3W4TF71K/dZMr3oyTyg== +"@spectrum-web-components/base@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/base/-/base-0.34.0.tgz#d341665a23e6431fd589727241cf2287f102f220" + integrity sha512-/2J7BT0lKkGeFv4SE5vmGxUtBE9NBo7oa8M1nYTgahm6OL/wN6O5/7qZIY+H2iCTy9719bO/BUa+7A7N5wiLFg== + dependencies: + lit "^2.5.0" + "@spectrum-web-components/eslint-plugin@file:./linters/eslint": version "0.35.0" +"@spectrum-web-components/help-text@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/help-text/-/help-text-0.34.0.tgz#9ef71d0a3e71f2d5560a338c74ecb57b583c29b0" + integrity sha512-VsFBgae+6dKDJcdf6UNMr3/aFPsyaUmeBubIV92kS52nHKTi1dKlYWtnAcvxFY6n3iH4iOn943FVkImuXboRkQ== + dependencies: + "@spectrum-web-components/base" "^0.34.0" + "@spectrum-web-components/icons-workflow" "^0.34.0" + +"@spectrum-web-components/icon@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/icon/-/icon-0.34.0.tgz#109c52b9a0c911051507ff467ee44339eb225158" + integrity sha512-gASDbootZDmXYiRaqd1jlUoXapCI0U/s1ZNqaakAWm+vGoZB6T3b5eV2fUO0aHP8/UCAt+F07T1jTPo/ygaQMA== + dependencies: + "@spectrum-web-components/base" "^0.34.0" + "@spectrum-web-components/iconset" "^0.34.0" + +"@spectrum-web-components/icons-ui@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/icons-ui/-/icons-ui-0.34.0.tgz#ab61568145759e2e0d6cc42dcc176f6d3ec8a959" + integrity sha512-zom55CdjSxpff1f5VVbvqFy3KN7PIXw2M+3RjogKF1Yl5TRt5OUGl6cMBT+vmsBJ9ZOs5g4l0POepJ2Zdom/JQ== + dependencies: + "@spectrum-web-components/base" "^0.34.0" + "@spectrum-web-components/icon" "^0.34.0" + "@spectrum-web-components/iconset" "^0.34.0" + +"@spectrum-web-components/icons-workflow@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/icons-workflow/-/icons-workflow-0.34.0.tgz#b575a32b4c8cc01958dbc21290167454ae34f613" + integrity sha512-Vsa9lzzgC86+NMrW/qYCnAHUw5vk0+FfXpFcCCc3lPYVJQD4ByCQYZ2U+uF/n85o1A88xeuMjaZsGIMHxDMPbA== + dependencies: + "@spectrum-web-components/base" "^0.34.0" + "@spectrum-web-components/icon" "^0.34.0" + +"@spectrum-web-components/iconset@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/iconset/-/iconset-0.34.0.tgz#e2b88cc19c702ec6de1b7047c91128cce1256fc2" + integrity sha512-/pvVyBcJnhrfiIAjnXxwVGMrls6/8uvsSFCt0WFAFaiw5p5/njcuDJRNMo65EctiCQs1ApksA6E6s3rXj7S8TQ== + dependencies: + "@spectrum-web-components/base" "^0.34.0" + +"@spectrum-web-components/reactive-controllers@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/reactive-controllers/-/reactive-controllers-0.34.0.tgz#692663db46bbcb9db85ebdf8a8b4c97905e1119b" + integrity sha512-hAv6gbYhFfLY5f6nRL0YT5qjXLG8IzFx9LbNQVzYpVd7afLa0WZN0EuomgGQ+i0t35VTMnstypNtN3q4FUuV2w== + dependencies: + lit "^2.5.0" + +"@spectrum-web-components/shared@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/shared/-/shared-0.34.0.tgz#60605fd85c71f5e3c6dece2b388c3dbb59b2bab1" + integrity sha512-FGCLiOCJC2/lKdcBh6TcYfxOHF2nKca2RaCDZ2jRJ1fKRx9B48F80lSnSxlwtK7N1XZ8svvKGwJEFWNNvBMH2w== + dependencies: + "@lit-labs/observers" "^2.0.0" + "@spectrum-web-components/base" "^0.34.0" + focus-visible "^5.1.0" + +"@spectrum-web-components/textfield@^0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@spectrum-web-components/textfield/-/textfield-0.34.0.tgz#ec3c64dd3a703b20ccff9180c035298e637e96f7" + integrity sha512-RPODpW7zmpSa4otI40Pn7EW5z/TLVkQOZT3eF6LPnk+ffdOmGJvutL5JB9R8U15jLUFNSt05E6XFqnNxPSRllQ== + dependencies: + "@spectrum-web-components/base" "^0.34.0" + "@spectrum-web-components/help-text" "^0.34.0" + "@spectrum-web-components/icon" "^0.34.0" + "@spectrum-web-components/icons-ui" "^0.34.0" + "@spectrum-web-components/icons-workflow" "^0.34.0" + "@spectrum-web-components/shared" "^0.34.0" + "@storybook/csf-tools@^6.4.9": version "6.4.19" resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.19.tgz#28bdea11da17501a8bc4e761b821d7721880eaf6" @@ -5681,7 +5761,7 @@ magic-string "^0.25.0" string.prototype.matchall "^4.0.6" -"@swc/helpers@0.5.1", "@swc/helpers@^0.5.0": +"@swc/helpers@0.5.1": version "0.5.1" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== From 67b445a01c6e0c0dad8c327cecb58148e87101e6 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 18 Jul 2023 16:16:35 +0100 Subject: [PATCH 02/37] refactor: input segments improvements --- packages/input-segments/README.md | 21 +- packages/input-segments/package.json | 6 +- packages/input-segments/src/InputSegments.ts | 55 +- .../input-segments/src/input-segments.css | 73 +- .../input-segments/src/spectrum-config.js | 32 - .../src/spectrum-input-segments.css | 1212 ----------------- yarn.lock | 5 - 7 files changed, 117 insertions(+), 1287 deletions(-) delete mode 100644 packages/input-segments/src/spectrum-config.js delete mode 100644 packages/input-segments/src/spectrum-input-segments.css diff --git a/packages/input-segments/README.md b/packages/input-segments/README.md index 8fb933e1597..b9f057c4a16 100644 --- a/packages/input-segments/README.md +++ b/packages/input-segments/README.md @@ -9,20 +9,17 @@ yarn add @spectrum-web-components/input-segments ``` -Import the side effectful registration of `` via: +`InputSegments` cannot be used directly as a component, as there is no `` component. The only way to use `InputSegments` is by extending a class: -``` -import '@spectrum-web-components/input-segments/sp-input-segments.js'; -``` - -When looking to leverage the `InputSegments` base class as a type and/or for extension purposes, do so via: - -``` +```js import { InputSegments } from '@spectrum-web-components/input-segments'; + +export class MyInput extends InputSegments { + ... +} ``` -## Example +## To-do list -```html - -``` +- Include ARIA attributes for editable segments +- Use `@input`/`@beforeinput` events to handle data input/content cleanup diff --git a/packages/input-segments/package.json b/packages/input-segments/package.json index 197a6f7f5e6..635ef0a064e 100644 --- a/packages/input-segments/package.json +++ b/packages/input-segments/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "description": "Web component implementation of a Spectrum design InputSegments", + "description": "“Abstract” component used to extend date and time form fields classes", "license": "Apache-2.0", "repository": { "type": "git", @@ -62,13 +62,9 @@ "@spectrum-web-components/reactive-controllers": "^0.34.0", "@spectrum-web-components/textfield": "^0.34.0" }, - "devDependencies": { - "@spectrum-css/textfield": "^6.0.7" - }, "types": "./src/index.d.ts", "customElements": "custom-elements.json", "sideEffects": [ - "./sp-*.js", "./**/*.dev.js" ] } diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 865139e663f..4a55f037992 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -24,15 +24,20 @@ import { PropertyValueMap, TemplateResult, } from '@spectrum-web-components/base'; +import { + property, + query, + state, +} from '@spectrum-web-components/base/src/decorators.js'; +import { + classMap, + ifDefined, + styleMap, + when, +} from '@spectrum-web-components/base/src/directives.js'; import { LanguageResolutionController } from '@spectrum-web-components/reactive-controllers/src/LanguageResolution.js'; import { TextfieldBase } from '@spectrum-web-components/textfield'; -import { property, query, state } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { styleMap } from 'lit/directives/style-map.js'; -import { when } from 'lit/directives/when.js'; - import { AM, dateSegmentTypes, @@ -50,7 +55,7 @@ import { import styles from './input-segments.css.js'; /** - * @element sp-input-segments + * @event change - Announces when a new date/time is defined by emitting a `Date` object */ export class InputSegments extends TextfieldBase { public static override get styles(): CSSResultArray { @@ -60,18 +65,31 @@ export class InputSegments extends TextfieldBase { @query('.editable-segment') firstEditableSegment!: HTMLDivElement; - @property({ reflect: true, attribute: false }) - selectedDateTime?: Date; - - @property({ attribute: false }) - includeDate = false; + /** + * Indicates when date segments should be included in the field + */ + @state() + protected includeDate = false; - @property({ attribute: false }) - includeTime = false; + /** + * Indicates when time segments should be included in the field + */ + @state() + protected includeTime = false; - @property({ attribute: false }) + /** + * Indicates which segments that are part of time should be included. In addition to the hour segment, which will + * always be displayed, we can display the minutes (default) and seconds segment + */ + @property() timeGranularity: TimeGranularity = 'minute'; + /** + * Defines whether a date/time should be displayed in the field + */ + @property({ reflect: true, attribute: false }) + selectedDateTime?: Date; + @state() private _locale!: string; @@ -158,11 +176,7 @@ export class InputSegments extends TextfieldBase { this._createSegments = true; } - if ( - changedProperties.has('includeDate') || - changedProperties.has('includeTime') || - changedProperties.has('timeGranularity') - ) { + if (changedProperties.has('timeGranularity')) { this._createSegments = true; } @@ -227,6 +241,7 @@ export class InputSegments extends TextfieldBase { : undefined, }; + // TODO: Include ARIA attributes for editable segments return html`
Date: Tue, 18 Jul 2023 17:05:23 +0100 Subject: [PATCH 03/37] fix(textfield): convert pseudo :read-only to attribute --- packages/textfield/src/spectrum-config.js | 1 + packages/textfield/src/spectrum-textfield.css | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/textfield/src/spectrum-config.js b/packages/textfield/src/spectrum-config.js index 730a3934e39..d404d7e06b7 100644 --- a/packages/textfield/src/spectrum-config.js +++ b/packages/textfield/src/spectrum-config.js @@ -73,6 +73,7 @@ export default { converter.classToAttribute('is-invalid', 'invalid'), converter.classToAttribute('is-disabled', 'disabled'), converter.classToAttribute('is-readOnly', 'readonly'), + converter.pseudoToAttribute('read-only', 'readonly'), ], }, ], diff --git a/packages/textfield/src/spectrum-textfield.css b/packages/textfield/src/spectrum-textfield.css index c17f166620a..468b9138d8a 100644 --- a/packages/textfield/src/spectrum-textfield.css +++ b/packages/textfield/src/spectrum-textfield.css @@ -1076,9 +1076,9 @@ governing permissions and limitations under the License. ) ); } -.input:read-only, :host([readonly]) #textfield .input, -:host([readonly]) #textfield:hover .input { +:host([readonly]) #textfield:hover .input, +:host([readonly]) .input { background-color: #0000; border-color: #0000; color: var( @@ -1090,9 +1090,9 @@ governing permissions and limitations under the License. ); outline: none; } -.input:read-only::placeholder, :host([readonly]) #textfield .input::placeholder, -:host([readonly]) #textfield:hover .input::placeholder { +:host([readonly]) #textfield:hover .input::placeholder, +:host([readonly]) .input::placeholder { background-color: #0000; color: var( --highcontrast-textfield-text-color-readonly, From bcd9d712aad8f39ac296bd483ddec818de43c3ad Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 18 Jul 2023 17:31:08 +0100 Subject: [PATCH 04/37] feat(time-field): add new time field component --- packages/input-segments/src/InputSegments.ts | 3 + packages/time-field/.npmignore | 2 + packages/time-field/README.md | 28 ++ packages/time-field/exports.json | 4 + packages/time-field/package.json | 68 ++++ packages/time-field/sp-time-field.ts | 20 + packages/time-field/src/TimeField.ts | 33 ++ packages/time-field/src/index.ts | 12 + packages/time-field/src/time-field.css | 11 + .../time-field/stories/time-field.stories.ts | 375 ++++++++++++++++++ .../time-field/test/benchmark/basic-test.ts | 18 + packages/time-field/test/time-field.test.ts | 38 ++ packages/time-field/tsconfig.json | 10 + tools/bundle/elements.ts | 1 + tools/bundle/src/index.ts | 1 + tsconfig-all.json | 1 + yarn.lock | 7 + 17 files changed, 632 insertions(+) create mode 100644 packages/time-field/.npmignore create mode 100644 packages/time-field/README.md create mode 100644 packages/time-field/exports.json create mode 100644 packages/time-field/package.json create mode 100644 packages/time-field/sp-time-field.ts create mode 100644 packages/time-field/src/TimeField.ts create mode 100644 packages/time-field/src/index.ts create mode 100644 packages/time-field/src/time-field.css create mode 100644 packages/time-field/stories/time-field.stories.ts create mode 100644 packages/time-field/test/benchmark/basic-test.ts create mode 100644 packages/time-field/test/time-field.test.ts create mode 100644 packages/time-field/tsconfig.json diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 4a55f037992..e85c51ab5f0 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -56,6 +56,9 @@ import styles from './input-segments.css.js'; /** * @event change - Announces when a new date/time is defined by emitting a `Date` object + * + * @slot help-text - Default or non-negative help text to associate to your form element + * @slot negative-help-text - Negative help text to associate to your form element when `invalid` */ export class InputSegments extends TextfieldBase { public static override get styles(): CSSResultArray { diff --git a/packages/time-field/.npmignore b/packages/time-field/.npmignore new file mode 100644 index 00000000000..c50cbe188c0 --- /dev/null +++ b/packages/time-field/.npmignore @@ -0,0 +1,2 @@ +stories +test \ No newline at end of file diff --git a/packages/time-field/README.md b/packages/time-field/README.md new file mode 100644 index 00000000000..b0690625afb --- /dev/null +++ b/packages/time-field/README.md @@ -0,0 +1,28 @@ +## Description + +### Usage + +[![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/time-field?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/time-field) +[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/time-field?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/time-field) + +``` +yarn add @spectrum-web-components/time-field +``` + +Import the side effectful registration of `` via: + +``` +import '@spectrum-web-components/time-field/sp-time-field.js'; +``` + +When looking to leverage the `TimeField` base class as a type and/or for extension purposes, do so via: + +``` +import { TimeField } from '@spectrum-web-components/time-field'; +``` + +## Example + +```html + +``` diff --git a/packages/time-field/exports.json b/packages/time-field/exports.json new file mode 100644 index 00000000000..925298d825a --- /dev/null +++ b/packages/time-field/exports.json @@ -0,0 +1,4 @@ +{ + "./src/*": "./src/*.js", + "./sp-time-field.js": "./sp-time-field.js" +} diff --git a/packages/time-field/package.json b/packages/time-field/package.json new file mode 100644 index 00000000000..9f3b7b6d9da --- /dev/null +++ b/packages/time-field/package.json @@ -0,0 +1,68 @@ +{ + "name": "@spectrum-web-components/time-field", + "version": "0.0.1", + "publishConfig": { + "access": "public" + }, + "description": "Web component implementation of a Spectrum design TimeField", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/adobe/spectrum-web-components.git", + "directory": "packages/time-field" + }, + "author": "", + "homepage": "https://adobe.github.io/spectrum-web-components/components/time-field", + "bugs": { + "url": "https://github.com/adobe/spectrum-web-components/issues" + }, + "main": "./src/index.js", + "module": "./src/index.js", + "type": "module", + "exports": { + ".": { + "development": "./src/index.dev.js", + "default": "./src/index.js" + }, + "./package.json": "./package.json", + "./src/TimeField.js": { + "development": "./src/TimeField.dev.js", + "default": "./src/TimeField.js" + }, + "./src/index.js": { + "development": "./src/index.dev.js", + "default": "./src/index.js" + }, + "./src/time-field.css.js": "./src/time-field.css.js", + "./sp-time-field.js": { + "development": "./sp-time-field.dev.js", + "default": "./sp-time-field.js" + } + }, + "scripts": { + "test": "echo \"Error: run tests from mono-repo root.\" && exit 1" + }, + "files": [ + "**/*.d.ts", + "**/*.js", + "**/*.js.map", + "custom-elements.json", + "!stories/", + "!test/" + ], + "keywords": [ + "spectrum css", + "web components", + "lit-element", + "lit-html" + ], + "dependencies": { + "@spectrum-web-components/input-segments": "^0.0.1" + }, + "types": "./src/index.d.ts", + "customElements": "custom-elements.json", + "sideEffects": [ + "./sp-*.js", + "./**/*.dev.js" + ] +} diff --git a/packages/time-field/sp-time-field.ts b/packages/time-field/sp-time-field.ts new file mode 100644 index 00000000000..5f998e4506e --- /dev/null +++ b/packages/time-field/sp-time-field.ts @@ -0,0 +1,20 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { TimeField } from './src/TimeField.js'; + +customElements.define('sp-time-field', TimeField); + +declare global { + interface HTMLElementTagNameMap { + 'sp-time-field': TimeField; + } +} diff --git a/packages/time-field/src/TimeField.ts b/packages/time-field/src/TimeField.ts new file mode 100644 index 00000000000..6847b2dbc3c --- /dev/null +++ b/packages/time-field/src/TimeField.ts @@ -0,0 +1,33 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { CSSResultArray } from '@spectrum-web-components/base'; +import { state } from '@spectrum-web-components/base/src/decorators.js'; +import { InputSegments } from '@spectrum-web-components/input-segments'; + +import styles from './time-field.css.js'; + +/** + * @element sp-time-field + * + * @event change - Announces when a new time is defined by emitting a `Date` object + * + * @slot help-text - Default or non-negative help text to associate to your form element + * @slot negative-help-text - Negative help text to associate to your form element when `invalid` + */ +export class TimeField extends InputSegments { + public static override get styles(): CSSResultArray { + return [...super.styles, styles]; + } + + @state() + override includeTime = true; +} diff --git a/packages/time-field/src/index.ts b/packages/time-field/src/index.ts new file mode 100644 index 00000000000..2c5490778da --- /dev/null +++ b/packages/time-field/src/index.ts @@ -0,0 +1,12 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +export * from './TimeField.js'; diff --git a/packages/time-field/src/time-field.css b/packages/time-field/src/time-field.css new file mode 100644 index 00000000000..26ef92385f0 --- /dev/null +++ b/packages/time-field/src/time-field.css @@ -0,0 +1,11 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ diff --git a/packages/time-field/stories/time-field.stories.ts b/packages/time-field/stories/time-field.stories.ts new file mode 100644 index 00000000000..7afad6d8af4 --- /dev/null +++ b/packages/time-field/stories/time-field.stories.ts @@ -0,0 +1,375 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { html, nothing, TemplateResult } from '@spectrum-web-components/base'; +import { TimeGranularity } from '@spectrum-web-components/input-segments/src/types.js'; + +import { spreadProps } from '../../../test/lit-helpers.js'; + +import '@spectrum-web-components/help-text/sp-help-text.js'; +import '@spectrum-web-components/time-field/sp-time-field.js'; +import '@spectrum-web-components/theme/sp-theme.js'; + +const locales = [ + 'cs-CZ', + 'cy-GB', + 'da-DK', + 'de-DE', + 'en-GB', + 'en-US', + 'es-ES', + 'fi-FI', + 'fr-FR', + 'hu-HU', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'nb-NO', + 'nl-NL', + 'pl-PL', + 'pt-BR', + 'ru-RU', + 'sv-SE', + 'tr-TR', + 'uk-UA', + 'zh-Hans-CN', + 'zh-Hans-CN-u-nu-hanidec', + 'zh-Hant-TW', + 'zz-ZY', + 'zz-ZZ', +] as const; + +const defaultLocale = 'en-US'; + +const timeGranularities: TimeGranularity[] = ['hour', 'minute', 'second']; + +const hiddenProperty = { + table: { + disable: true, + }, +}; + +export default { + title: 'Time Field', + component: 'sp-time-field', + + argTypes: { + locale: { + options: locales, + control: { + type: 'select', + }, + table: { + defaultValue: { + summary: defaultLocale, + }, + }, + }, + + // Don't render private properties and getters in the Storybook UI + firstEditableSegment: { ...hiddenProperty }, + _locale: { ...hiddenProperty }, + _previousLocale: { ...hiddenProperty }, + _currentDateTime: { ...hiddenProperty }, + _newDateTime: { ...hiddenProperty }, + _segments: { ...hiddenProperty }, + _createSegments: { ...hiddenProperty }, + _languageResolver: { ...hiddenProperty }, + _timeZone: { ...hiddenProperty }, + _formatter: { ...hiddenProperty }, + _is12HourClock: { ...hiddenProperty }, + _daySegment: { ...hiddenProperty }, + _monthSegment: { ...hiddenProperty }, + _yearSegment: { ...hiddenProperty }, + _hourSegment: { ...hiddenProperty }, + _minuteSegment: { ...hiddenProperty }, + _secondSegment: { ...hiddenProperty }, + _dayPeriodSegment: { ...hiddenProperty }, + + // Inherited + _dirParent: { ...hiddenProperty }, + shadowRoot: { ...hiddenProperty }, + dir: { ...hiddenProperty }, + isLTR: { ...hiddenProperty }, + 'allowed-keys': { ...hiddenProperty }, + allowedKeys: { ...hiddenProperty }, + autocomplete: { ...hiddenProperty }, + displayValue: { ...hiddenProperty }, + focused: { ...hiddenProperty }, + focusElement: { ...hiddenProperty }, + grows: { ...hiddenProperty }, + inputElement: { ...hiddenProperty }, + label: { ...hiddenProperty }, + maxlength: { ...hiddenProperty }, + minlength: { ...hiddenProperty }, + multiline: { ...hiddenProperty }, + pattern: { ...hiddenProperty }, + placeholder: { ...hiddenProperty }, + renderInput: { ...hiddenProperty }, + renderMultiline: { ...hiddenProperty }, + type: { ...hiddenProperty }, + value: { ...hiddenProperty }, + _type: { ...hiddenProperty }, + _value: { ...hiddenProperty }, + input: { ...hiddenProperty }, + }, + + args: { + locale: defaultLocale, + }, + + parameters: { + controls: { + // Hide "This story is not configured to handle controls" warning + hideNoControlsWarning: true, + }, + actions: { + handles: ['onChange'], + }, + }, +}; + +interface StoryArgs { + locale?: string; + + selectedDateTime?: Date; + timeGranularity?: TimeGranularity; + quiet?: boolean; + disabled?: boolean; + readonly?: boolean; + valid?: boolean; + invalid?: boolean; + + onChange?: (dateTime: Date) => void; + + [prop: string]: unknown; +} + +const renderTimeField = ( + title: string, + args: StoryArgs = {}, + content: TemplateResult | typeof nothing = nothing +): TemplateResult => { + return html` + + + +

${title}

+

Locale: ${args.locale}

+
+ + ${content} + +
+ `; +}; + +export const Default = (args: StoryArgs = {}): TemplateResult => { + return renderTimeField('Default', args); +}; + +export const selectedDateTime = (args: StoryArgs = {}): TemplateResult[] => { + const formatter = Intl.DateTimeFormat(args.locale || defaultLocale, { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + + return [ + new Date(1995, 1, 28, 9, 31, 7), + new Date(2021, 10, 2, 16, 1, 54), + ].map((dateTime) => { + const formatted = formatter.format(dateTime); + const title = `Selected Date/Time: ${formatted}`; + + args = { + ...args, + selectedDateTime: dateTime, + }; + + return renderTimeField(title, args); + }); +}; + +export const timeGranularity = (args: StoryArgs = {}): TemplateResult => { + args = { + ...args, + timeGranularity: args.timeGranularity, + }; + + return renderTimeField(`Time Granularity: ${args.timeGranularity}`, args); +}; + +timeGranularity.argTypes = { + timeGranularity: { + options: timeGranularities, + control: { + type: 'select', + }, + table: { + defaultValue: { + summary: 'minute', + }, + }, + }, +}; + +timeGranularity.args = { + timeGranularity: 'second', +}; + +export const disabled = (args: StoryArgs = {}): TemplateResult => { + return renderTimeField(`Disabled? ${args.disabled}`, args); +}; + +disabled.argTypes = { + disabled: { + control: 'boolean', + table: { + defaultValue: { + summary: false, + }, + }, + }, +}; + +disabled.args = { + disabled: true, +}; + +export const quiet = (args: StoryArgs = {}): TemplateResult => { + return renderTimeField(`Quiet? ${args.quiet}`, args); +}; + +quiet.argTypes = { + quiet: { + control: 'boolean', + table: { + defaultValue: { + summary: false, + }, + }, + }, +}; + +quiet.args = { + quiet: true, +}; + +export const readonly = (args: StoryArgs = {}): TemplateResult => { + return renderTimeField(`Read only? ${args.readonly}`, args); +}; + +readonly.argTypes = { + readonly: { + control: 'boolean', + table: { + defaultValue: { + summary: false, + }, + }, + }, +}; + +readonly.args = { + readonly: true, +}; + +export const autoFocus = (args: StoryArgs = {}): TemplateResult => { + args = { + ...args, + autofocus: true, + }; + + return renderTimeField('Auto focus', args); +}; + +export const valid = (args: StoryArgs = {}): TemplateResult => { + return renderTimeField(`Is valid? ${args.valid}`, args); +}; + +valid.argTypes = { + valid: { + control: 'boolean', + table: { + defaultValue: { + summary: false, + }, + }, + }, +}; + +valid.args = { + valid: true, +}; + +export const invalid = (args: StoryArgs = {}): TemplateResult => { + return renderTimeField(`Is invalid? ${args.invalid}`, args); +}; + +invalid.argTypes = { + invalid: { + control: 'boolean', + table: { + defaultValue: { + summary: false, + }, + }, + }, +}; + +invalid.args = { + invalid: true, +}; + +export const helpText = (args: StoryArgs = {}): TemplateResult => { + const content = html` + My default help text + `; + + return renderTimeField(`With help text`, args, content); +}; + +export const negativeHelpText = (args: StoryArgs = {}): TemplateResult => { + const content = html` + + Default help text (displayed only when not invalid) + + + This field is required! + + `; + + return renderTimeField(`With negative help text`, args, content); +}; + +negativeHelpText.argTypes = { + invalid: { + control: 'boolean', + table: { + defaultValue: { + summary: false, + }, + }, + }, +}; + +negativeHelpText.args = { + invalid: true, +}; diff --git a/packages/time-field/test/benchmark/basic-test.ts b/packages/time-field/test/benchmark/basic-test.ts new file mode 100644 index 00000000000..5192687fdc7 --- /dev/null +++ b/packages/time-field/test/benchmark/basic-test.ts @@ -0,0 +1,18 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import '@spectrum-web-components/time-field/sp-time-field.js'; +import { html } from '@spectrum-web-components/base'; +import { measureFixtureCreation } from '../../../../test/benchmark/helpers.js'; + +measureFixtureCreation(html` + +`); diff --git a/packages/time-field/test/time-field.test.ts b/packages/time-field/test/time-field.test.ts new file mode 100644 index 00000000000..56127d9e2bb --- /dev/null +++ b/packages/time-field/test/time-field.test.ts @@ -0,0 +1,38 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; + +import '../sp-time-field.js'; +import { TimeField } from '..'; +import { testForLitDevWarnings } from '../../../test/testing-helpers.js'; + +describe('TimeField', () => { + testForLitDevWarnings( + async () => + await fixture( + html` + + ` + ) + ); + it('loads default time-field accessibly', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + await expect(el).to.be.accessible(); + }); +}); diff --git a/packages/time-field/tsconfig.json b/packages/time-field/tsconfig.json new file mode 100644 index 00000000000..c90873db4cf --- /dev/null +++ b/packages/time-field/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "./" + }, + "include": ["*.ts", "src/*.ts"], + "exclude": ["test/*.ts", "stories/*.ts"], + "references": [{ "path": "../../tools/base" }] +} diff --git a/tools/bundle/elements.ts b/tools/bundle/elements.ts index af94b1b3ff1..a82d368be27 100644 --- a/tools/bundle/elements.ts +++ b/tools/bundle/elements.ts @@ -89,6 +89,7 @@ import '@spectrum-web-components/textfield/sp-textfield.js'; import '@spectrum-web-components/theme/sp-theme.js'; import '@spectrum-web-components/theme/src/themes.js'; import '@spectrum-web-components/thumbnail/sp-thumbnail.js'; +import '@spectrum-web-components/time-field/sp-time-field.js'; import '@spectrum-web-components/toast/sp-toast.js'; import '@spectrum-web-components/tooltip/sp-tooltip.js'; import '@spectrum-web-components/top-nav/sp-top-nav.js'; diff --git a/tools/bundle/src/index.ts b/tools/bundle/src/index.ts index 3d7a937e503..4c9bdd0f5c7 100644 --- a/tools/bundle/src/index.ts +++ b/tools/bundle/src/index.ts @@ -63,6 +63,7 @@ export * from '@spectrum-web-components/tags'; export * from '@spectrum-web-components/textfield'; export * from '@spectrum-web-components/theme'; export * from '@spectrum-web-components/thumbnail'; +export * from '@spectrum-web-components/time-field'; export * from '@spectrum-web-components/toast'; export * from '@spectrum-web-components/tooltip'; export * from '@spectrum-web-components/top-nav'; diff --git a/tsconfig-all.json b/tsconfig-all.json index 0ba780868c2..00f52c2c9ed 100644 --- a/tsconfig-all.json +++ b/tsconfig-all.json @@ -77,6 +77,7 @@ { "path": "packages/tags" }, { "path": "packages/textfield" }, { "path": "packages/thumbnail" }, + { "path": "packages/time-field" }, { "path": "packages/toast" }, { "path": "packages/tooltip" }, { "path": "packages/top-nav" }, diff --git a/yarn.lock b/yarn.lock index 7b98f3cbc1a..f815af001d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5598,6 +5598,7 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-6.0.11.tgz#a4dbd8c2da1a317cd3c7208317364fe7a1026213" integrity sha512-lbqVZTUpk4UV18/8wRMTi8/0UwTGEKV5ocysNIeRSEhvHfO4ktQzhyn+T92E7TkY/WqKljEM8EnjX/K6hwUBNQ== +<<<<<<< HEAD "@spectrum-css/thumbnail@^3.0.17": version "3.0.17" resolved "https://registry.yarnpkg.com/@spectrum-css/thumbnail/-/thumbnail-3.0.17.tgz#1cd2f00e5b374aa0617ba3061c325f5174598ad3" @@ -5607,6 +5608,12 @@ version "9.1.0" resolved "https://registry.yarnpkg.com/@spectrum-css/toast/-/toast-9.1.0.tgz#209b2397da9c1304b3b0e538025afcb5c29dc26b" integrity sha512-/DheKxtmvoAZneAs1Dql+1waSqCxTFIEAfzaPCaQvAYg+ctW9NhXMZjGz5cRbHTzCmeuE6reuCLfuFput9Z7bQ== +======= +"@spectrum-css/thumbnail@^3.0.15": + version "3.0.15" + resolved "https://registry.yarnpkg.com/@spectrum-css/thumbnail/-/thumbnail-3.0.15.tgz#6c1a15f85cd309b75e1809813d834e1111b863ae" + integrity sha512-UnQDLrDl28VNVZCmksg5zVu65nWKMDpqDvb5QkOlslJrmouQ8LuB7RCEHUlEwJ/f4adkZ6EOjS09B6OZp02MDg== +>>>>>>> 3725083d4 (feat(time-field): add new time field component) "@spectrum-css/tokens@^11.2.0": version "11.2.0" From 107c0b40c57b9772d99e7f3e33ff76743be749ef Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Wed, 19 Jul 2023 20:21:08 +0100 Subject: [PATCH 05/37] refactor: remove `_` from private properties and methods and convert property `locale` to a getter --- packages/input-segments/src/InputSegments.ts | 371 +++++++++--------- .../time-field/stories/time-field.stories.ts | 48 +-- 2 files changed, 202 insertions(+), 217 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index e85c51ab5f0..a2e3f6dcfbc 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -81,8 +81,7 @@ export class InputSegments extends TextfieldBase { protected includeTime = false; /** - * Indicates which segments that are part of time should be included. In addition to the hour segment, which will - * always be displayed, we can display the minutes (default) and seconds segment + * Indicates which segments that are part of time should be used */ @property() timeGranularity: TimeGranularity = 'minute'; @@ -94,57 +93,58 @@ export class InputSegments extends TextfieldBase { selectedDateTime?: Date; @state() - private _locale!: string; + private previousLocale?: string; @state() - private _previousLocale?: string; + private currentDateTime!: CalendarDateTime; @state() - private _currentDateTime!: CalendarDateTime; + private newDateTime?: CalendarDateTime; @state() - private _newDateTime?: CalendarDateTime; + private segments: Segment[] = []; @state() - private _segments: Segment[] = []; + private createSegments = true; - @state() - private _createSegments = true; + private languageResolver = new LanguageResolutionController(this); + private timeZone = getLocalTimeZone(); + private formatter!: DateFormatter; - private _languageResolver = new LanguageResolutionController(this); - private _timeZone!: string; - private _formatter!: DateFormatter; + private get locale(): string { + return this.languageResolver.language; + } - private get _daySegment(): Segment | undefined { - return this._segments.find((segment) => segment.type === 'day'); + private get daySegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'day'); } - private get _monthSegment(): Segment | undefined { - return this._segments.find((segment) => segment.type === 'month'); + private get monthSegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'month'); } - private get _yearSegment(): Segment | undefined { - return this._segments.find((segment) => segment.type === 'year'); + private get yearSegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'year'); } - private get _hourSegment(): Segment | undefined { - return this._segments.find((segment) => segment.type === 'hour'); + private get hourSegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'hour'); } - private get _minuteSegment(): Segment | undefined { - return this._segments.find((segment) => segment.type === 'minute'); + private get minuteSegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'minute'); } - private get _secondSegment(): Segment | undefined { - return this._segments.find((segment) => segment.type === 'second'); + private get secondSegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'second'); } - private get _dayPeriodSegment(): Segment | undefined { - return this._segments.find((segment) => segment.type === 'dayPeriod'); + private get dayPeriodSegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'dayPeriod'); } - private get _is12HourClock(): boolean { - return Boolean(this._formatter.resolvedOptions().hour12); + private get is12HourClock(): boolean { + return Boolean(this.formatter.resolvedOptions().hour12); } /** @@ -161,35 +161,31 @@ export class InputSegments extends TextfieldBase { constructor() { super(); - - this._setTimeZone(); - this._setLocale(); - this._setFormatter(); - this._setInitialDateTime(); + this.setInitialDateTime(); } protected override willUpdate( changedProperties: PropertyValueMap ): void { - this._setLocale(); - this._setFormatter(); + if (this.locale !== this.previousLocale) { + this.previousLocale = this.locale; + this.createSegments = true; - if (changedProperties.has('selectedDateTime')) { - this._setCurrentDateTime(); - this._createSegments = true; + this.setFormatter(); } - if (changedProperties.has('timeGranularity')) { - this._createSegments = true; + if (changedProperties.has('selectedDateTime')) { + this.createSegments = true; + + this.setCurrentDateTime(); } - if (this._locale !== this._previousLocale) { - this._previousLocale = this._locale; - this._createSegments = true; + if (changedProperties.has('timeGranularity')) { + this.createSegments = true; } - if (this._createSegments) { - this._setSegments(); + if (this.createSegments) { + this.setSegments(); } } @@ -204,7 +200,7 @@ export class InputSegments extends TextfieldBase { @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut} > - ${this._segments.map((segment) => + ${this.segments.map((segment) => when( segment.type === 'literal', () => this.renderLiteralSegment(segment), @@ -288,19 +284,19 @@ export class InputSegments extends TextfieldBase { public handleKeydown(segment: Segment, event: KeyboardEvent): void { switch (event.code) { case 'ArrowUp': { - this._incrementValue(segment); + this.incrementValue(segment); break; } case 'ArrowRight': { - this._focusNextSegment(event); + this.focusNextSegment(event); break; } case 'ArrowDown': { - this._decrementValue(segment); + this.decrementValue(segment); break; } case 'ArrowLeft': { - this._focusPreviousSegment(event); + this.focusPreviousSegment(event); break; } default: { @@ -332,7 +328,7 @@ export class InputSegments extends TextfieldBase { if (min !== undefined && max !== undefined) { const typedValue = Number(event.key); - const isHourAmPm = this._is12HourClock && segment.type === 'hour'; + const isHourAmPm = this.is12HourClock && segment.type === 'hour'; const maxLength = String(max).length; let previousValue = segment.value; @@ -341,7 +337,7 @@ export class InputSegments extends TextfieldBase { if ( isHourAmPm && previousValue !== undefined && - this._isPM(previousValue) + this.isPM(previousValue) ) { previousValue -= PM; } @@ -364,7 +360,7 @@ export class InputSegments extends TextfieldBase { const useTypedValueOrMax = typedValue <= max ? typedValue : max; if (isHourAmPm) { - const isPM = this._isPM(min); + const isPM = this.isPM(min); if (isPM && newValue !== min && newValue > maxHourAM) { newValue = Number(String(newValue).slice(1)); @@ -390,7 +386,7 @@ export class InputSegments extends TextfieldBase { segment.value = newValue; - this._valueChanged(segment); + this.valueChanged(segment); } } @@ -399,10 +395,10 @@ export class InputSegments extends TextfieldBase { let previousValue = segment.value; if (previousValue !== undefined) { - if (this._is12HourClock && segment.type === 'hour') { + if (this.is12HourClock && segment.type === 'hour') { const isPM = segment.minValue !== undefined && - this._isPM(segment.minValue); + this.isPM(segment.minValue); if (isPM) { previousValue -= PM; @@ -425,19 +421,11 @@ export class InputSegments extends TextfieldBase { segment.value = (newValue && Number(newValue)) || undefined; - this._valueChanged(segment); + this.valueChanged(segment); } } - private _setTimeZone(): void { - this._timeZone = getLocalTimeZone(); - } - - private _setLocale(): void { - this._locale = this._languageResolver.language; - } - - private _setFormatter(): void { + private setFormatter(): void { let dateOptions: Intl.DateTimeFormatOptions = {}; let timeOptions: Intl.DateTimeFormatOptions = {}; @@ -461,32 +449,32 @@ export class InputSegments extends TextfieldBase { }; } - this._formatter = new DateFormatter(this._locale, { + this.formatter = new DateFormatter(this.locale, { ...dateOptions, ...timeOptions, }); } - private _setInitialDateTime(): void { - this._currentDateTime = toCalendarDateTime(now(this._timeZone)); + private setInitialDateTime(): void { + this.currentDateTime = toCalendarDateTime(now(this.timeZone)); } - private _setCurrentDateTime(): void { + private setCurrentDateTime(): void { if (this.selectedDateTime) { this.selectedDateTime = new Date(this.selectedDateTime); - if (!this._isValidTime(this.selectedDateTime)) { + if (!this.isValidTime(this.selectedDateTime)) { this.selectedDateTime = undefined; } else { - this._currentDateTime = this._dateToCalendarDateTime( + this.currentDateTime = this.dateToCalendarDateTime( this.selectedDateTime ); } } } - private _setNewDateTime(): void { - this._newDateTime = undefined; + private setNewDateTime(): void { + this.newDateTime = undefined; let year: number | undefined = undefined; let month: number | undefined = undefined; @@ -501,46 +489,46 @@ export class InputSegments extends TextfieldBase { const isSecond = this.timeGranularity === 'second'; if (this.includeDate) { - if (this._yearSegment?.value !== undefined) { - year = this._yearSegment.value; + if (this.yearSegment?.value !== undefined) { + year = this.yearSegment.value; } - if (this._monthSegment?.value !== undefined) { - month = this._monthSegment.value; + if (this.monthSegment?.value !== undefined) { + month = this.monthSegment.value; } - if (this._daySegment?.value !== undefined) { - day = this._daySegment.value; + if (this.daySegment?.value !== undefined) { + day = this.daySegment.value; } } if (this.includeTime) { - const hasHourValue = this._hourSegment?.value !== undefined; - const hasMinuteValue = this._minuteSegment?.value !== undefined; - const hasSecondValue = this._secondSegment?.value !== undefined; + const hasHourValue = this.hourSegment?.value !== undefined; + const hasMinuteValue = this.minuteSegment?.value !== undefined; + const hasSecondValue = this.secondSegment?.value !== undefined; if (isHour && hasHourValue) { - hour = this._hourSegment?.value; + hour = this.hourSegment?.value; } if (isMinute && hasHourValue && hasMinuteValue) { - minute = this._minuteSegment?.value; + minute = this.minuteSegment?.value; } if (isSecond && hasHourValue && hasMinuteValue && hasSecondValue) { - second = this._secondSegment?.value; + second = this.secondSegment?.value; } if (!this.includeDate) { - year = this._currentDateTime.year; - month = this._currentDateTime.month; - day = this._currentDateTime.day; + year = this.currentDateTime.year; + month = this.currentDateTime.month; + day = this.currentDateTime.day; } } // To create a new CalendarDateTime the only mandatory values are those referring to the date if (year !== undefined && month !== undefined && day !== undefined) { - this._newDateTime = new CalendarDateTime( + this.newDateTime = new CalendarDateTime( year, month, day, @@ -556,7 +544,7 @@ export class InputSegments extends TextfieldBase { * * @param date - `Date` object to validate */ - private _isValidTime(date: Date): boolean { + private isValidTime(date: Date): boolean { return !isNaN(date.getTime()); } @@ -565,7 +553,7 @@ export class InputSegments extends TextfieldBase { * * @param date - `Date` object to "convert" */ - private _dateToCalendarDateTime(date: Date): CalendarDateTime { + private dateToCalendarDateTime(date: Date): CalendarDateTime { return new CalendarDateTime( date.getFullYear(), date.getMonth() + 1, // The month to create a new `CalendarDate` cannot be a zero-based index, unlike `Date` @@ -581,8 +569,8 @@ export class InputSegments extends TextfieldBase { * segment referring to the hour will always be displayed, the other segments vary according to the defined locale * and granularity */ - private _setSegments(): void { - const { hour, minute, second } = this._currentDateTime; + private setSegments(): void { + const { hour, minute, second } = this.currentDateTime; const dateTime = new Date(); dateTime.setHours(hour, minute, second); @@ -592,12 +580,12 @@ export class InputSegments extends TextfieldBase { ...(this.includeTime ? timeSegmentTypes : []), ]; - this._segments = this._formatter + this.segments = this.formatter .formatToParts(dateTime) - .map((part) => this._mapToTimeSegment(part)) + .map((part) => this.mapToTimeSegment(part)) .filter((part) => segmentTypes.includes(part.type)); - this._createSegments = false; + this.createSegments = false; } /** @@ -606,21 +594,19 @@ export class InputSegments extends TextfieldBase { * * @param part - Part/segment to be "translated" (mapped) */ - private _mapToTimeSegment(part: Intl.DateTimeFormatPart): Segment { - const { value, minValue, maxValue } = this._getSegmentDetails( - part.type - ); + private mapToTimeSegment(part: Intl.DateTimeFormatPart): Segment { + const { value, minValue, maxValue } = this.getSegmentDetails(part.type); const segment: Segment = { type: part.type, - placeholder: this._getPlaceholder(part.type, part.value), + placeholder: this.getPlaceholder(part.type, part.value), formatted: part.value, value, minValue, maxValue, }; - this._formatValues(segment); + this.formatValues(segment); return segment; } @@ -630,17 +616,17 @@ export class InputSegments extends TextfieldBase { * * @param segment - Segment to be updated */ - private _formatValues(segment: Segment): void { + private formatValues(segment: Segment): void { if (segment.value !== undefined) { const options: Intl.DateTimeFormatOptions = {}; - let year = this._currentDateTime.year; - let month = this._currentDateTime.month; - let day = this._currentDateTime.day; + let year = this.currentDateTime.year; + let month = this.currentDateTime.month; + let day = this.currentDateTime.day; - let hour = this._currentDateTime.hour; - let minute = this._currentDateTime.minute; - let second = this._currentDateTime.second; + let hour = this.currentDateTime.hour; + let minute = this.currentDateTime.minute; + let second = this.currentDateTime.second; let padMaxLength = 2; @@ -661,7 +647,7 @@ export class InputSegments extends TextfieldBase { break; } case 'hour': { - if (this._is12HourClock) { + if (this.is12HourClock) { padMaxLength = 1; } @@ -687,7 +673,7 @@ export class InputSegments extends TextfieldBase { } const date = new Date(year, month, day, hour, minute, second); - const formatted = new DateFormatter(this._locale, options) + const formatted = new DateFormatter(this.locale, options) .formatToParts(date) .find((part) => part.type === segment.type)?.value; @@ -702,7 +688,7 @@ export class InputSegments extends TextfieldBase { * @param type - Type of segment * @param value - The value of the segment */ - private _getPlaceholder( + private getPlaceholder( type: Intl.DateTimeFormatPartTypes, value: string ): string { @@ -714,7 +700,7 @@ export class InputSegments extends TextfieldBase { * * @param hour - The hour to check */ - private _isPM(hour: number): boolean { + private isPM(hour: number): boolean { return hour >= PM; } @@ -723,8 +709,8 @@ export class InputSegments extends TextfieldBase { * * @param hour - The hour to identify the modifier */ - private _getAmPmModifier(hour: number): typeof AM | typeof PM { - return this._isPM(hour) ? PM : AM; + private getAmPmModifier(hour: number): typeof AM | typeof PM { + return this.isPM(hour) ? PM : AM; } /** @@ -734,44 +720,43 @@ export class InputSegments extends TextfieldBase { * * @param type - Segment type */ - private _getSegmentDetails( + private getSegmentDetails( type: Intl.DateTimeFormatPartTypes ): SegmentValueAndLimits { switch (type) { case 'year': return { minValue: 1, - maxValue: this._currentDateTime.calendar.getYearsInEra( - this._currentDateTime + maxValue: this.currentDateTime.calendar.getYearsInEra( + this.currentDateTime ), value: - this._newDateTime?.year ?? - (this.selectedDateTime && this._currentDateTime.year) ?? + this.newDateTime?.year ?? + (this.selectedDateTime && this.currentDateTime.year) ?? undefined, }; case 'month': return { - minValue: getMinimumMonthInYear(this._currentDateTime), - maxValue: this._currentDateTime.calendar.getMonthsInYear( - this._currentDateTime + minValue: getMinimumMonthInYear(this.currentDateTime), + maxValue: this.currentDateTime.calendar.getMonthsInYear( + this.currentDateTime ), value: - this._newDateTime?.month ?? - (this.selectedDateTime && - this._currentDateTime.month) ?? + this.newDateTime?.month ?? + (this.selectedDateTime && this.currentDateTime.month) ?? undefined, }; case 'day': return { - minValue: getMinimumDayInMonth(this._currentDateTime), - maxValue: this._currentDateTime.calendar.getDaysInMonth( - this._currentDateTime + minValue: getMinimumDayInMonth(this.currentDateTime), + maxValue: this.currentDateTime.calendar.getDaysInMonth( + this.currentDateTime ), value: - this._newDateTime?.day ?? - (this.selectedDateTime && this._currentDateTime.day) ?? + this.newDateTime?.day ?? + (this.selectedDateTime && this.currentDateTime.day) ?? undefined, }; @@ -779,9 +764,9 @@ export class InputSegments extends TextfieldBase { let min = 0; let max = 23; - if (this._is12HourClock) { - const isPM = this._isPM( - this._newDateTime?.hour ?? this._currentDateTime.hour + if (this.is12HourClock) { + const isPM = this.isPM( + this.newDateTime?.hour ?? this.currentDateTime.hour ); min = isPM ? minHourPM : minHourAM; @@ -792,21 +777,21 @@ export class InputSegments extends TextfieldBase { minValue: min, maxValue: max, value: - this._newDateTime?.hour ?? - (this.selectedDateTime && this._currentDateTime.hour) ?? + this.newDateTime?.hour ?? + (this.selectedDateTime && this.currentDateTime.hour) ?? undefined, }; case 'minute': case 'second': const minutes = - this._newDateTime?.minute ?? - (this.selectedDateTime && this._currentDateTime.minute) ?? + this.newDateTime?.minute ?? + (this.selectedDateTime && this.currentDateTime.minute) ?? undefined; const seconds = - this._newDateTime?.second ?? - (this.selectedDateTime && this._currentDateTime.second) ?? + this.newDateTime?.second ?? + (this.selectedDateTime && this.currentDateTime.second) ?? undefined; return { @@ -820,12 +805,10 @@ export class InputSegments extends TextfieldBase { minValue: AM, maxValue: PM, value: - (this._newDateTime?.hour && - this._getAmPmModifier(this._newDateTime.hour)) ?? + (this.newDateTime?.hour && + this.getAmPmModifier(this.newDateTime.hour)) ?? (this.selectedDateTime && - this._getAmPmModifier( - this._currentDateTime.hour - )) ?? + this.getAmPmModifier(this.currentDateTime.hour)) ?? undefined, }; @@ -834,7 +817,7 @@ export class InputSegments extends TextfieldBase { } } - private _incrementValue(segment: Segment): void { + private incrementValue(segment: Segment): void { const min = segment.minValue; const max = segment.maxValue; @@ -852,10 +835,10 @@ export class InputSegments extends TextfieldBase { } } - this._valueChanged(segment); + this.valueChanged(segment); } - private _decrementValue(segment: Segment): void { + private decrementValue(segment: Segment): void { const min = segment.minValue; const max = segment.maxValue; @@ -873,31 +856,31 @@ export class InputSegments extends TextfieldBase { } } - this._valueChanged(segment); + this.valueChanged(segment); } /** * When the day period is changed, it automatically adjusts the hour if it has already been informed previously to * match the new period (AM or PM). In addition, the minimum and maximum values of the hour are also changed */ - private _updateHour(): void { - if (this._hourSegment && this._dayPeriodSegment) { - if (this._dayPeriodSegment.value !== undefined) { - const isAM = this._dayPeriodSegment.value === AM; - const isPM = this._dayPeriodSegment.value === PM; - - this._hourSegment.minValue = isPM ? minHourPM : minHourAM; - this._hourSegment.maxValue = isPM ? maxHourPM : maxHourAM; - - if (this._hourSegment.value !== undefined) { - if (isAM && this._isPM(this._hourSegment.value)) { - this._hourSegment.value -= PM; - } else if (isPM && !this._isPM(this._hourSegment.value)) { - this._hourSegment.value += PM; + private updateHour(): void { + if (this.hourSegment && this.dayPeriodSegment) { + if (this.dayPeriodSegment.value !== undefined) { + const isAM = this.dayPeriodSegment.value === AM; + const isPM = this.dayPeriodSegment.value === PM; + + this.hourSegment.minValue = isPM ? minHourPM : minHourAM; + this.hourSegment.maxValue = isPM ? maxHourPM : maxHourAM; + + if (this.hourSegment.value !== undefined) { + if (isAM && this.isPM(this.hourSegment.value)) { + this.hourSegment.value -= PM; + } else if (isPM && !this.isPM(this.hourSegment.value)) { + this.hourSegment.value += PM; } } } else { - this._resetHourAndDayPeriod(); + this.resetHourAndDayPeriod(); } } } @@ -906,67 +889,67 @@ export class InputSegments extends TextfieldBase { * When the day period is cleared, we need to reset the min and max values of the day period and hour segments to * their initial values */ - private _resetHourAndDayPeriod(): void { - const dayPeriod = this._getSegmentDetails('dayPeriod'); + private resetHourAndDayPeriod(): void { + const dayPeriod = this.getSegmentDetails('dayPeriod'); - if (this._dayPeriodSegment) { - this._dayPeriodSegment.value = dayPeriod.value; - this._dayPeriodSegment.minValue = dayPeriod.minValue; - this._dayPeriodSegment.maxValue = dayPeriod.maxValue; + if (this.dayPeriodSegment) { + this.dayPeriodSegment.value = dayPeriod.value; + this.dayPeriodSegment.minValue = dayPeriod.minValue; + this.dayPeriodSegment.maxValue = dayPeriod.maxValue; - if (this._dayPeriodSegment.value === undefined) { - this._dayPeriodSegment.formatted = - this._dayPeriodSegment.placeholder; + if (this.dayPeriodSegment.value === undefined) { + this.dayPeriodSegment.formatted = + this.dayPeriodSegment.placeholder; } } - const hour = this._getSegmentDetails('hour'); + const hour = this.getSegmentDetails('hour'); - if (this._hourSegment) { - this._hourSegment.minValue = hour.minValue; - this._hourSegment.maxValue = hour.maxValue; + if (this.hourSegment) { + this.hourSegment.minValue = hour.minValue; + this.hourSegment.maxValue = hour.maxValue; - if (this._hourSegment.value !== undefined) { - this._hourSegment.value += this._getAmPmModifier( - this._currentDateTime.hour + if (this.hourSegment.value !== undefined) { + this.hourSegment.value += this.getAmPmModifier( + this.currentDateTime.hour ); } else { - this._hourSegment.value = hour.value; + this.hourSegment.value = hour.value; } } } - private _valueChanged(segment: Segment): void { - if (this._is12HourClock && segment.type === 'dayPeriod') { - this._updateHour(); + private valueChanged(segment: Segment): void { + if (this.is12HourClock && segment.type === 'dayPeriod') { + this.updateHour(); } - this._formatValues(segment); - this._setNewDateTime(); + this.formatValues(segment); + this.setNewDateTime(); this.requestUpdate(); - if (this._newDateTime) { + if (this.newDateTime) { this.dispatchEvent( new CustomEvent('change', { bubbles: true, cancelable: true, composed: true, - detail: this._newDateTime.toDate(this._timeZone), + detail: this.newDateTime.toDate(this.timeZone), }) ); } } - private _focusNextSegment(event: KeyboardEvent): void { - this._focusSegment(event.target as HTMLDivElement, 'next'); + private focusNextSegment(event: KeyboardEvent): void { + this.focusSegment(event.target as HTMLDivElement, 'next'); } - private _focusPreviousSegment(event: KeyboardEvent): void { - this._focusSegment(event.target as HTMLDivElement, 'previous'); + private focusPreviousSegment(event: KeyboardEvent): void { + this.focusSegment(event.target as HTMLDivElement, 'previous'); } - private _focusSegment( + private focusSegment( segment: HTMLDivElement, elementToFocus: 'previous' | 'next' ): void { diff --git a/packages/time-field/stories/time-field.stories.ts b/packages/time-field/stories/time-field.stories.ts index 7afad6d8af4..a176b56ec3f 100644 --- a/packages/time-field/stories/time-field.stories.ts +++ b/packages/time-field/stories/time-field.stories.ts @@ -62,7 +62,7 @@ export default { component: 'sp-time-field', argTypes: { - locale: { + lang: { options: locales, control: { type: 'select', @@ -76,23 +76,25 @@ export default { // Don't render private properties and getters in the Storybook UI firstEditableSegment: { ...hiddenProperty }, - _locale: { ...hiddenProperty }, - _previousLocale: { ...hiddenProperty }, - _currentDateTime: { ...hiddenProperty }, - _newDateTime: { ...hiddenProperty }, - _segments: { ...hiddenProperty }, - _createSegments: { ...hiddenProperty }, - _languageResolver: { ...hiddenProperty }, - _timeZone: { ...hiddenProperty }, - _formatter: { ...hiddenProperty }, - _is12HourClock: { ...hiddenProperty }, - _daySegment: { ...hiddenProperty }, - _monthSegment: { ...hiddenProperty }, - _yearSegment: { ...hiddenProperty }, - _hourSegment: { ...hiddenProperty }, - _minuteSegment: { ...hiddenProperty }, - _secondSegment: { ...hiddenProperty }, - _dayPeriodSegment: { ...hiddenProperty }, + includeDate: { ...hiddenProperty }, + includeTime: { ...hiddenProperty }, + previousLocale: { ...hiddenProperty }, + currentDateTime: { ...hiddenProperty }, + newDateTime: { ...hiddenProperty }, + segments: { ...hiddenProperty }, + createSegments: { ...hiddenProperty }, + languageResolver: { ...hiddenProperty }, + timeZone: { ...hiddenProperty }, + formatter: { ...hiddenProperty }, + locale: { ...hiddenProperty }, + daySegment: { ...hiddenProperty }, + monthSegment: { ...hiddenProperty }, + yearSegment: { ...hiddenProperty }, + hourSegment: { ...hiddenProperty }, + minuteSegment: { ...hiddenProperty }, + secondSegment: { ...hiddenProperty }, + dayPeriodSegment: { ...hiddenProperty }, + is12HourClock: { ...hiddenProperty }, // Inherited _dirParent: { ...hiddenProperty }, @@ -123,7 +125,7 @@ export default { }, args: { - locale: defaultLocale, + lang: defaultLocale, }, parameters: { @@ -138,7 +140,7 @@ export default { }; interface StoryArgs { - locale?: string; + lang?: string; selectedDateTime?: Date; timeGranularity?: TimeGranularity; @@ -166,9 +168,9 @@ const renderTimeField = ( } - +

${title}

-

Locale: ${args.locale}

+

Locale: ${args.lang}


${content} @@ -182,7 +184,7 @@ export const Default = (args: StoryArgs = {}): TemplateResult => { }; export const selectedDateTime = (args: StoryArgs = {}): TemplateResult[] => { - const formatter = Intl.DateTimeFormat(args.locale || defaultLocale, { + const formatter = Intl.DateTimeFormat(args.lang ?? defaultLocale, { day: 'numeric', month: 'short', year: 'numeric', From 018b50b437f1b86cbbca579fe8fbdc55aae4bd69 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Thu, 20 Jul 2023 09:28:38 +0100 Subject: [PATCH 06/37] refactor: add new `@internationalized/number` package --- packages/input-segments/package.json | 3 ++- yarn.lock | 31 +++++++++++++++++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/input-segments/package.json b/packages/input-segments/package.json index 635ef0a064e..7c78a145315 100644 --- a/packages/input-segments/package.json +++ b/packages/input-segments/package.json @@ -57,7 +57,8 @@ "lit-html" ], "dependencies": { - "@internationalized/date": "^3.2.0", + "@internationalized/date": "^3.2.1", + "@internationalized/number": "^3.2.1", "@spectrum-web-components/base": "^0.34.0", "@spectrum-web-components/reactive-controllers": "^0.34.0", "@spectrum-web-components/textfield": "^0.34.0" diff --git a/yarn.lock b/yarn.lock index f815af001d2..864d7c44161 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,11 +2500,18 @@ integrity sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA== "@internationalized/date@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.2.0.tgz#1d266e5e5543a059cf8cca9b954fa033c3e58a75" - integrity sha512-VDMHN1m33L4eqPs5BaihzgQJXyaORbMoHOtrapFxx179J8ucY5CRIHYsq5RRLKPHZWgjNfa5v6amWWDkkMFywA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.3.0.tgz#90386d4b4e707f28507d1a1b3cc0e162ad5ee038" + integrity sha512-qfRd7jCIgUjabI8RxeAsxhLDRS1u8eUPX96GB5uBp1Tpm6YY6dVveE7YwsTEV6L4QOp5LKFirFHHGsL/XQwJIA== dependencies: - "@swc/helpers" "^0.4.14" + "@swc/helpers" "^0.5.0" + +"@internationalized/date@^3.2.1": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.4.0.tgz#e843ac40b04afafe99fe0a41bae7abdd221a9a44" + integrity sha512-QUDSGCsvrEVITVf+kv9VSAraAmCgjQmU5CiXtesUBBhBe374NmnEIIaOFBZ72t29dfGMBP0zF+v6toVnbcc6jg== + dependencies: + "@swc/helpers" "^0.5.0" "@internationalized/number@^3.1.0": version "3.1.2" @@ -2513,6 +2520,13 @@ dependencies: "@swc/helpers" "^0.4.14" +"@internationalized/number@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.2.1.tgz#570e4010544a84a8225e65b34a689a36187caaa8" + integrity sha512-hK30sfBlmB1aIe3/OwAPg9Ey0DjjXvHEiGVhNaOiBJl31G0B6wMaX8BN3ibzdlpyRNE9p7X+3EBONmxtJO9Yfg== + dependencies: + "@swc/helpers" "^0.5.0" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -5598,7 +5612,6 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-6.0.11.tgz#a4dbd8c2da1a317cd3c7208317364fe7a1026213" integrity sha512-lbqVZTUpk4UV18/8wRMTi8/0UwTGEKV5ocysNIeRSEhvHfO4ktQzhyn+T92E7TkY/WqKljEM8EnjX/K6hwUBNQ== -<<<<<<< HEAD "@spectrum-css/thumbnail@^3.0.17": version "3.0.17" resolved "https://registry.yarnpkg.com/@spectrum-css/thumbnail/-/thumbnail-3.0.17.tgz#1cd2f00e5b374aa0617ba3061c325f5174598ad3" @@ -5608,12 +5621,6 @@ version "9.1.0" resolved "https://registry.yarnpkg.com/@spectrum-css/toast/-/toast-9.1.0.tgz#209b2397da9c1304b3b0e538025afcb5c29dc26b" integrity sha512-/DheKxtmvoAZneAs1Dql+1waSqCxTFIEAfzaPCaQvAYg+ctW9NhXMZjGz5cRbHTzCmeuE6reuCLfuFput9Z7bQ== -======= -"@spectrum-css/thumbnail@^3.0.15": - version "3.0.15" - resolved "https://registry.yarnpkg.com/@spectrum-css/thumbnail/-/thumbnail-3.0.15.tgz#6c1a15f85cd309b75e1809813d834e1111b863ae" - integrity sha512-UnQDLrDl28VNVZCmksg5zVu65nWKMDpqDvb5QkOlslJrmouQ8LuB7RCEHUlEwJ/f4adkZ6EOjS09B6OZp02MDg== ->>>>>>> 3725083d4 (feat(time-field): add new time field component) "@spectrum-css/tokens@^11.2.0": version "11.2.0" @@ -5763,7 +5770,7 @@ magic-string "^0.25.0" string.prototype.matchall "^4.0.6" -"@swc/helpers@0.5.1": +"@swc/helpers@0.5.1", "@swc/helpers@^0.5.0": version "0.5.1" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== From 03a31dd74c53a2385d2cd63907ed90fcadaf2c43 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Thu, 20 Jul 2023 12:11:09 +0100 Subject: [PATCH 07/37] refactor: reduce cognitive complexity for `handleTypedValue()` method * "Refactor this function to reduce its Cognitive Complexity from 41 to the 15 allowed." (SonarLint) --- packages/input-segments/src/InputSegments.ts | 298 ++++++++++++++----- packages/input-segments/src/types.ts | 12 + 2 files changed, 228 insertions(+), 82 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index a2e3f6dcfbc..3745a09a26b 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -18,6 +18,7 @@ import { now, toCalendarDateTime, } from '@internationalized/date'; +import { NumberParser } from '@internationalized/number'; import { CSSResultArray, html, @@ -47,6 +48,7 @@ import { minHourPM, PM, Segment, + SegmentDetails, SegmentValueAndLimits, TimeGranularity, timeSegmentTypes, @@ -110,6 +112,7 @@ export class InputSegments extends TextfieldBase { private languageResolver = new LanguageResolutionController(this); private timeZone = getLocalTimeZone(); private formatter!: DateFormatter; + private numberParser!: NumberParser; private get locale(): string { return this.languageResolver.language; @@ -172,6 +175,7 @@ export class InputSegments extends TextfieldBase { this.createSegments = true; this.setFormatter(); + this.setNumberParser(); } if (changedProperties.has('selectedDateTime')) { @@ -227,7 +231,7 @@ export class InputSegments extends TextfieldBase { public renderEditableSegment(segment: Segment): TemplateResult { const isActive = !this.disabled && !this.readonly; - const isPlaceholderVisible = Boolean(segment.value === undefined); + const isPlaceholderVisible = segment.value === undefined; const segmentClasses = { 'is-placeholder': isPlaceholderVisible, @@ -302,19 +306,19 @@ export class InputSegments extends TextfieldBase { default: { // TODO: Use @input/@beforeinput events to handle data input/content cleanup const key = event.key; - const numberKey = /^[\d]+$/.test(key); - const clearKey = ['Backspace', 'Delete'].includes(key); - const allowedKey = ['Tab'].includes(key); + const isNumberKey = this.numberParser.isValidPartialNumber(key); + const isClearKey = ['Backspace', 'Delete'].includes(key); + const isAllowedKey = ['Tab'].includes(key); - if (numberKey) { + if (isNumberKey) { this.handleTypedValue(segment, event); } - if (clearKey) { + if (isClearKey) { this.handleClear(segment); } - if (numberKey || clearKey || !allowedKey) { + if (isNumberKey || isClearKey || !isAllowedKey) { event.preventDefault(); event.stopPropagation(); } @@ -322,72 +326,27 @@ export class InputSegments extends TextfieldBase { } } + /** + * Sets new segment value after user types some number + * + * @param segment - The segment being changed + * @param event - Event details + */ public handleTypedValue(segment: Segment, event: KeyboardEvent): void { - const min = segment.minValue; - const max = segment.maxValue; - - if (min !== undefined && max !== undefined) { - const typedValue = Number(event.key); - const isHourAmPm = this.is12HourClock && segment.type === 'hour'; - const maxLength = String(max).length; - - let previousValue = segment.value; - let newValue: number; - - if ( - isHourAmPm && - previousValue !== undefined && - this.isPM(previousValue) - ) { - previousValue -= PM; - } + const details = this.extractDetails(segment); - newValue = - previousValue !== undefined - ? Number(`${previousValue}${typedValue}`) - : typedValue; - - if (String(newValue).length > maxLength) { - newValue = isHourAmPm - ? typedValue - : Number(String(newValue).slice(1)); - } - - // Defines the value that should be used if the new defined value is less than the minimum allowed - const useTypedValueOrMin = typedValue >= min ? typedValue : min; - - // Defines the value that should be used if the new defined value is greater than the maximum allowed - const useTypedValueOrMax = typedValue <= max ? typedValue : max; - - if (isHourAmPm) { - const isPM = this.isPM(min); - - if (isPM && newValue !== min && newValue > maxHourAM) { - newValue = Number(String(newValue).slice(1)); - } else if (newValue > max) { - const useMinHourAM = !isPM && newValue === PM; - newValue = useMinHourAM ? minHourAM : useTypedValueOrMax; - } + if (details === undefined) { + return; + } - if (isPM && newValue !== min) { - newValue += PM; - } - } else { - if (String(newValue).length > maxLength) { - newValue = Number(String(newValue).slice(1)); - } + const typedValue = this.numberParser.parse(event.key); + const isAmPmHour = this.is12HourClock && segment.type === 'hour'; - if (newValue < min) { - newValue = useTypedValueOrMin; - } else if (newValue > max) { - newValue = useTypedValueOrMax; - } - } + segment.value = isAmPmHour + ? this.getNewValueForAmPmHourSegment(details, typedValue) + : this.getNewValueForOtherSegments(details, typedValue); - segment.value = newValue; - - this.valueChanged(segment); - } + this.valueChanged(segment); } public handleClear(segment: Segment): void { @@ -410,7 +369,7 @@ export class InputSegments extends TextfieldBase { : String(previousValue).slice(0, -1); if (isPM && newValue !== '') { - newValue = String(Number(newValue) + PM); + newValue = String(this.numberParser.parse(newValue) + PM); } } else { newValue = @@ -419,7 +378,8 @@ export class InputSegments extends TextfieldBase { : String(previousValue).slice(0, -1); } - segment.value = (newValue && Number(newValue)) || undefined; + segment.value = + (newValue && this.numberParser.parse(newValue)) || undefined; this.valueChanged(segment); } @@ -455,6 +415,12 @@ export class InputSegments extends TextfieldBase { }); } + private setNumberParser(): void { + this.numberParser = new NumberParser(this.locale, { + maximumFractionDigits: 0, + }); + } + private setInitialDateTime(): void { this.currentDateTime = toCalendarDateTime(now(this.timeZone)); } @@ -500,6 +466,15 @@ export class InputSegments extends TextfieldBase { if (this.daySegment?.value !== undefined) { day = this.daySegment.value; } + + if ( + year !== undefined && + month !== undefined && + day !== undefined && + !this.includeTime + ) { + this.newDateTime = new CalendarDateTime(year, month, day); + } } if (this.includeTime) { @@ -524,18 +499,32 @@ export class InputSegments extends TextfieldBase { month = this.currentDateTime.month; day = this.currentDateTime.day; } - } - // To create a new CalendarDateTime the only mandatory values are those referring to the date - if (year !== undefined && month !== undefined && day !== undefined) { - this.newDateTime = new CalendarDateTime( - year, - month, - day, - hour, - minute, - second - ); + const hasTime = + (this.timeGranularity === 'hour' && hasHourValue) || + (this.timeGranularity === 'minute' && + hasHourValue && + hasMinuteValue) || + (this.timeGranularity === 'second' && + hasHourValue && + hasMinuteValue && + hasSecondValue); + + if ( + year !== undefined && + month !== undefined && + day !== undefined && + hasTime + ) { + this.newDateTime = new CalendarDateTime( + year, + month, + day, + hour, + minute, + second + ); + } } } @@ -695,6 +684,27 @@ export class InputSegments extends TextfieldBase { return type === 'dayPeriod' ? value : '––'; } + /** + * Extracts the segment details, validating that the limits have been defined. The value currently assigned to the + * segment remains optional + * + * @param segment - The segment to extract the details + */ + private extractDetails(segment: Segment): SegmentDetails | undefined { + const min = segment.minValue; + const max = segment.maxValue; + + if (min === undefined || max === undefined) { + return undefined; + } + + return { + value: segment.value, + minValue: min, + maxValue: max, + }; + } + /** * Indicates whether the hour entered is PM or not * @@ -933,7 +943,6 @@ export class InputSegments extends TextfieldBase { this.dispatchEvent( new CustomEvent('change', { bubbles: true, - cancelable: true, composed: true, detail: this.newDateTime.toDate(this.timeZone), }) @@ -941,6 +950,131 @@ export class InputSegments extends TextfieldBase { } } + /** + * Returns the value to be used if the typed value is less than the minimum allowed + * + * @param typedValue - The value typed by the user + * @param min - The minimum value allowed for that segment + */ + private useTypedValueOrMin(typedValue: number, min: number): number { + return typedValue < min ? min : typedValue; + } + + /** + * Returns the value to be used if the typed value is greater than the maximum allowed + * + * @param typedValue - The value typed by the user + * @param max - The maximum value allowed for that segment + */ + private useTypedValueOrMax(typedValue: number, max: number): number { + return typedValue > max ? max : typedValue; + } + + /** + * When the user types a value into a segment, we need to temporarily store the current segment's value before it + * changes to include the value the user has just entered. Also, we need to identify if it is the hour segment and + * if the clock format is 12 hours, if so, we have to adjust the previous value to perform some calculations + * + * @param segment - The segment being changed + */ + private mergePreviousValueWithTypedValue( + details: SegmentDetails, + typedValue: number, + isAmPmHour = false + ): number { + let previousValue = details.value ?? 0; + + if (isAmPmHour && this.isPM(previousValue)) { + previousValue -= PM; + } + + let newValue = this.numberParser.parse(`${previousValue}${typedValue}`); + + if (String(newValue).length > String(details.maxValue).length) { + newValue = isAmPmHour + ? typedValue + : this.numberParser.parse(String(newValue).slice(1)); + } + + return newValue; + } + + /** + * For the hour segment whose clock format is 12 hours, we need to perform some checks before defining what will be + * the new value associated with the segment. This is necessary because the time the user sees might not match the + * value we need to store in the segment + * + * For example, if "10" is the value displayed in the field, the actual value could be "22" if it's PM, so we need + * to identify when we have to change the "actual value" + * + * @param details - Segment value and limits + * @param typedValue - The value typed by the user + */ + private getNewValueForAmPmHourSegment( + details: SegmentDetails, + typedValue: number + ): number { + const isAmPmHour = true; + + let newValue = this.mergePreviousValueWithTypedValue( + details, + typedValue, + isAmPmHour + ); + + const min = details.minValue; + const max = details.maxValue; + const isPM = this.isPM(min); + + if (isPM && newValue !== min && newValue > maxHourAM) { + newValue = this.numberParser.parse(String(newValue).slice(1)); + } else if (newValue > max) { + const useMinHourAM = !isPM && newValue === PM; + + newValue = useMinHourAM + ? minHourAM + : this.useTypedValueOrMax(typedValue, max); + } + + if (isPM && newValue !== min) { + newValue += PM; + } + + return newValue; + } + + /** + * Defines the new value that will be associated with the segment, with the exception of the hour segment for + * 12-hour clocks, whose value is defined in another method + * + * @param details - Segment value and limits + * @param typedValue - The value typed by the user + */ + private getNewValueForOtherSegments( + details: SegmentDetails, + typedValue: number + ): number { + let newValue = this.mergePreviousValueWithTypedValue( + details, + typedValue + ); + + const min = details.minValue; + const max = details.maxValue; + + if (String(newValue).length > String(max).length) { + newValue = this.numberParser.parse(String(newValue).slice(1)); + } + + if (newValue < min) { + newValue = this.useTypedValueOrMin(typedValue, min); + } else if (newValue > max) { + newValue = this.useTypedValueOrMax(typedValue, max); + } + + return newValue; + } + private focusNextSegment(event: KeyboardEvent): void { this.focusSegment(event.target as HTMLDivElement, 'next'); } diff --git a/packages/input-segments/src/types.ts b/packages/input-segments/src/types.ts index c8aaba38111..ddd75dfa9fb 100644 --- a/packages/input-segments/src/types.ts +++ b/packages/input-segments/src/types.ts @@ -41,11 +41,23 @@ export interface Segment extends Omit { maxValue?: number; } +/** + * Value and limits of a segment. They are all optional, as literal segments have none of these properties + */ export type SegmentValueAndLimits = Pick< Segment, 'value' | 'minValue' | 'maxValue' >; +/** + * Value and limits of a segment, however the limits are mandatory, as it is known that they have already been defined + */ +export interface SegmentDetails + extends Omit { + minValue: number; + maxValue: number; +} + export type TimeGranularity = 'hour' | 'minute' | 'second'; /** AM modifier: `0` hours */ From 50a3b74468825a1664d115e2e7fcce5f2ed62f2a Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Thu, 20 Jul 2023 13:54:33 +0100 Subject: [PATCH 08/37] refactor: reduce cognitive complexity for `handleClear()` method * "Refactor this function to reduce its Cognitive Complexity from 21 to the 15 allowed." (SonarLint) --- packages/input-segments/src/InputSegments.ts | 59 +++++++++++--------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 3745a09a26b..9346b717afd 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -349,40 +349,49 @@ export class InputSegments extends TextfieldBase { this.valueChanged(segment); } + /** + * Sets the new segment value after the user clears the content + * + * @param segment - The segment being changed + * @param event - Event details + */ public handleClear(segment: Segment): void { - let newValue: string | undefined; - let previousValue = segment.value; + const details = this.extractDetails(segment); - if (previousValue !== undefined) { - if (this.is12HourClock && segment.type === 'hour') { - const isPM = - segment.minValue !== undefined && - this.isPM(segment.minValue); + if (details?.value === undefined) { + return; + } - if (isPM) { - previousValue -= PM; - } + let newValue: string | undefined; + let previousValue = details.value; - newValue = - previousValue === minHourAM - ? String(minHourAM + 1) - : String(previousValue).slice(0, -1); + if (this.is12HourClock && segment.type === 'hour') { + const isPM = this.isPM(details.minValue); - if (isPM && newValue !== '') { - newValue = String(this.numberParser.parse(newValue) + PM); - } - } else { - newValue = - segment.type === 'dayPeriod' - ? undefined - : String(previousValue).slice(0, -1); + if (isPM) { + previousValue -= PM; } - segment.value = - (newValue && this.numberParser.parse(newValue)) || undefined; + newValue = + previousValue === minHourAM + ? String(minHourAM + 1) + : String(previousValue).slice(0, -1); - this.valueChanged(segment); + if (isPM && newValue !== '') { + newValue = String(this.numberParser.parse(newValue) + PM); + } + } else { + newValue = + segment.type === 'dayPeriod' + ? undefined + : String(previousValue).slice(0, -1); } + + segment.value = + (newValue !== undefined && this.numberParser.parse(newValue)) || + undefined; + + this.valueChanged(segment); } private setFormatter(): void { From 69979813c0a77f15fbfdadfeda681d3c76b03d10 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Thu, 20 Jul 2023 16:03:59 +0100 Subject: [PATCH 09/37] refactor: reduce cognitive complexity for `setNewDateTime()` method * "Refactor this function to reduce its Cognitive Complexity from 30 to the 15 allowed." (SonarLint) --- packages/input-segments/src/InputSegments.ts | 120 +++++++------------ 1 file changed, 46 insertions(+), 74 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 9346b717afd..4bd700a7414 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -448,92 +448,64 @@ export class InputSegments extends TextfieldBase { } } + /** + * Sets the new date/time object according to the configuration parameters and if the minimum required values for + * each type (date only, time only or date and time together) were defined + */ private setNewDateTime(): void { - this.newDateTime = undefined; - - let year: number | undefined = undefined; - let month: number | undefined = undefined; - let day: number | undefined = undefined; - - let hour: number | undefined = undefined; - let minute: number | undefined = undefined; - let second: number | undefined = undefined; - - const isHour = this.timeGranularity === 'hour'; - const isMinute = this.timeGranularity === 'minute'; - const isSecond = this.timeGranularity === 'second'; + const defined = (value: number | undefined): value is number => { + return typeof value === 'number'; + }; - if (this.includeDate) { - if (this.yearSegment?.value !== undefined) { - year = this.yearSegment.value; - } + this.newDateTime = undefined; - if (this.monthSegment?.value !== undefined) { - month = this.monthSegment.value; - } + // If none of the date/time segments are being used, there is nothing to do here + if (!this.includeDate && !this.includeTime) { + return; + } - if (this.daySegment?.value !== undefined) { - day = this.daySegment.value; - } + let year = this.yearSegment?.value; + let month = this.monthSegment?.value; + let day = this.daySegment?.value; - if ( - year !== undefined && - month !== undefined && - day !== undefined && - !this.includeTime - ) { + // When only date segments are being used + if (this.includeDate && !this.includeTime) { + if (defined(year) && defined(month) && defined(day)) { this.newDateTime = new CalendarDateTime(year, month, day); } - } - - if (this.includeTime) { - const hasHourValue = this.hourSegment?.value !== undefined; - const hasMinuteValue = this.minuteSegment?.value !== undefined; - const hasSecondValue = this.secondSegment?.value !== undefined; - if (isHour && hasHourValue) { - hour = this.hourSegment?.value; - } + return; + } - if (isMinute && hasHourValue && hasMinuteValue) { - minute = this.minuteSegment?.value; - } + // When only time segments are being used, we need to set the date based on the current date + if (!this.includeDate) { + year = this.currentDateTime.year; + month = this.currentDateTime.month; + day = this.currentDateTime.day; + } - if (isSecond && hasHourValue && hasMinuteValue && hasSecondValue) { - second = this.secondSegment?.value; - } + const hour = this.hourSegment?.value; + const minute = this.minuteSegment?.value; + const second = this.secondSegment?.value; - if (!this.includeDate) { - year = this.currentDateTime.year; - month = this.currentDateTime.month; - day = this.currentDateTime.day; - } + const isHour = this.timeGranularity === 'hour'; + const isMinute = this.timeGranularity === 'minute'; + const isSecond = this.timeGranularity === 'second'; - const hasTime = - (this.timeGranularity === 'hour' && hasHourValue) || - (this.timeGranularity === 'minute' && - hasHourValue && - hasMinuteValue) || - (this.timeGranularity === 'second' && - hasHourValue && - hasMinuteValue && - hasSecondValue); - - if ( - year !== undefined && - month !== undefined && - day !== undefined && - hasTime - ) { - this.newDateTime = new CalendarDateTime( - year, - month, - day, - hour, - minute, - second - ); - } + const hasTime = + (isHour && defined(hour)) || + (isMinute && defined(hour) && defined(minute)) || + (isSecond && defined(hour) && defined(minute) && defined(second)); + + if (defined(year) && defined(month) && defined(day) && hasTime) { + this.newDateTime = new CalendarDateTime( + year, + month, + day, + hour, + minute, + second + ); } } From e3f158754d727e11cf2da05d14e41b8c5f83e2a1 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Thu, 20 Jul 2023 16:07:28 +0100 Subject: [PATCH 10/37] refactor: add correct placeholder for year --- packages/input-segments/src/InputSegments.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 4bd700a7414..96c1ee97a00 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -652,8 +652,7 @@ export class InputSegments extends TextfieldBase { } /** - * Returns the placeholder that will be used. If it is the day period segment, use the actual value. For the rest of - * the segments, use two dashes as a placeholder + * Returns the placeholder that will be used according to the segment type * * @param type - Type of segment * @param value - The value of the segment @@ -662,7 +661,14 @@ export class InputSegments extends TextfieldBase { type: Intl.DateTimeFormatPartTypes, value: string ): string { - return type === 'dayPeriod' ? value : '––'; + switch (type) { + case 'dayPeriod': + return value; + case 'year': + return '––––'; + default: + return '––'; + } } /** From a23f7782652f6d419636cdf810bed9719e65b52a Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Fri, 21 Jul 2023 10:48:47 +0100 Subject: [PATCH 11/37] refactor: reduce cognitive complexity for `getSegmentValueAndLimits()` method * "Refactor this function to reduce its Cognitive Complexity from 34 to the 15 allowed." (SonarLint) --- packages/input-segments/src/InputSegments.ts | 254 +++++++++++-------- 1 file changed, 149 insertions(+), 105 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 96c1ee97a00..ed4969936d0 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -535,15 +535,10 @@ export class InputSegments extends TextfieldBase { } /** - * Determines which segments will be used by the input (hour, minute, second, day period for 12-hour clock). The - * segment referring to the hour will always be displayed, the other segments vary according to the defined locale - * and granularity + * Determines which segments will be used by the input (date only, time only or both types) */ private setSegments(): void { - const { hour, minute, second } = this.currentDateTime; - - const dateTime = new Date(); - dateTime.setHours(hour, minute, second); + const dateTime = this.currentDateTime.toDate(this.timeZone); const segmentTypes = [ ...(this.includeDate ? dateSegmentTypes : []), @@ -565,7 +560,9 @@ export class InputSegments extends TextfieldBase { * @param part - Part/segment to be "translated" (mapped) */ private mapToTimeSegment(part: Intl.DateTimeFormatPart): Segment { - const { value, minValue, maxValue } = this.getSegmentDetails(part.type); + const { value, minValue, maxValue } = this.getSegmentValueAndLimits( + part.type + ); const segment: Segment = { type: part.type, @@ -587,68 +584,76 @@ export class InputSegments extends TextfieldBase { * @param segment - Segment to be updated */ private formatValues(segment: Segment): void { - if (segment.value !== undefined) { - const options: Intl.DateTimeFormatOptions = {}; + if (segment.value === undefined) { + return; + } - let year = this.currentDateTime.year; - let month = this.currentDateTime.month; - let day = this.currentDateTime.day; + const options: Intl.DateTimeFormatOptions = {}; - let hour = this.currentDateTime.hour; - let minute = this.currentDateTime.minute; - let second = this.currentDateTime.second; + let year = this.currentDateTime.year; + let month = this.currentDateTime.month; + let day = this.currentDateTime.day; - let padMaxLength = 2; + let hour = this.currentDateTime.hour; + let minute = this.currentDateTime.minute; + let second = this.currentDateTime.second; - switch (segment.type) { - case 'year': { - year = segment.value; - options.year = 'numeric'; - break; - } - case 'month': { - month = segment.value; - options.month = '2-digit'; - break; - } - case 'day': { - day = segment.value; - options.day = '2-digit'; - break; - } - case 'hour': { - if (this.is12HourClock) { - padMaxLength = 1; - } + let padMaxLength = 2; - hour = segment.value; - options.hour = 'numeric'; - break; - } - case 'minute': { - minute = segment.value; - options.minute = '2-digit'; - break; - } - case 'second': { - second = segment.value; - options.second = '2-digit'; - break; - } - case 'dayPeriod': { - hour = (segment.value || 0) + 1; - options.hour = 'numeric'; - break; + switch (segment.type) { + case 'year': { + year = segment.value; + options.year = 'numeric'; + padMaxLength = 0; + break; + } + case 'month': { + month = segment.value; + options.month = '2-digit'; + break; + } + case 'day': { + day = segment.value; + options.day = '2-digit'; + break; + } + case 'hour': { + if (this.is12HourClock) { + padMaxLength = 1; } + + hour = segment.value; + options.hour = 'numeric'; + break; } + case 'minute': { + minute = segment.value; + options.minute = '2-digit'; + break; + } + case 'second': { + second = segment.value; + options.second = '2-digit'; + break; + } + case 'dayPeriod': { + hour = (segment.value || 0) + 1; + options.hour = 'numeric'; + padMaxLength = 0; + break; + } + } - const date = new Date(year, month, day, hour, minute, second); - const formatted = new DateFormatter(this.locale, options) - .formatToParts(date) - .find((part) => part.type === segment.type)?.value; + /** + * As we use `CalendarDateTime`, we need to subtract 1 from the month before creating a new `Date` object, + * as this uses zero-based months, and `CalendarDateTime` does not + */ + const date = new Date(year, month - 1, day, hour, minute, second); + const formatted = new DateFormatter(this.locale, options) + .formatToParts(date) + .find((part) => part.type === segment.type)?.value; - segment.formatted = formatted?.padStart(padMaxLength, '0'); - } + segment.formatted = formatted?.padStart(padMaxLength, '0'); } /** @@ -710,16 +715,89 @@ export class InputSegments extends TextfieldBase { return this.isPM(hour) ? PM : AM; } + /** + * Checks whether the segment being created or updated will have a value or not by checking the following order: + * + * 1. Did the segment already have a previously defined value? If so, keep the same value + * + * 2. Since the segment doesn't have a previous value to keep, was the component given a specific date/time when it + * was created? If yes, then use that information + * + * 3. There is no value to use at this point, so it will remain as `undefined` + * + * @param newValue - Current segment value, if any + * @param currentValue - Current date/time value with the same segment type as the current value + */ + private useNewOrCurrentValue( + newValue: number | undefined, + currentValue: number + ): number | undefined { + return newValue ?? (this.selectedDateTime && currentValue) ?? undefined; + } + + /** + * Gets the current value of the segment according to the type + * + * @param type - Type of segment + */ + private getCurrentValue( + type: Intl.DateTimeFormatPartTypes + ): number | undefined { + switch (type) { + case 'year': + return this.useNewOrCurrentValue( + this.yearSegment?.value, + this.currentDateTime.year + ); + case 'month': + return this.useNewOrCurrentValue( + this.monthSegment?.value, + this.currentDateTime.month + ); + case 'day': + return this.useNewOrCurrentValue( + this.daySegment?.value, + this.currentDateTime.day + ); + case 'hour': + return this.useNewOrCurrentValue( + this.hourSegment?.value, + this.currentDateTime.hour + ); + case 'minute': + return this.useNewOrCurrentValue( + this.minuteSegment?.value, + this.currentDateTime.minute + ); + case 'second': + return this.useNewOrCurrentValue( + this.secondSegment?.value, + this.currentDateTime.second + ); + case 'dayPeriod': + // To identify the current value of "AM/PM", we use the value of the hour, not the day period itself + return this.useNewOrCurrentValue( + this.hourSegment?.value && + this.getAmPmModifier(this.hourSegment.value), + this.getAmPmModifier(this.currentDateTime.hour) + ); + default: + return undefined; + } + } + /** * Returns the minimum and maximum values for each segment that will be used, in addition to defining if there is a * current value to be used. If segments are being recreated, we try to recover the value that was previously set * for each segment, if possible * - * @param type - Segment type + * @param type - Type of segment */ - private getSegmentDetails( + private getSegmentValueAndLimits( type: Intl.DateTimeFormatPartTypes ): SegmentValueAndLimits { + const value = this.getCurrentValue(type); + switch (type) { case 'year': return { @@ -727,36 +805,24 @@ export class InputSegments extends TextfieldBase { maxValue: this.currentDateTime.calendar.getYearsInEra( this.currentDateTime ), - value: - this.newDateTime?.year ?? - (this.selectedDateTime && this.currentDateTime.year) ?? - undefined, + value, }; - case 'month': return { minValue: getMinimumMonthInYear(this.currentDateTime), maxValue: this.currentDateTime.calendar.getMonthsInYear( this.currentDateTime ), - value: - this.newDateTime?.month ?? - (this.selectedDateTime && this.currentDateTime.month) ?? - undefined, + value, }; - case 'day': return { minValue: getMinimumDayInMonth(this.currentDateTime), maxValue: this.currentDateTime.calendar.getDaysInMonth( this.currentDateTime ), - value: - this.newDateTime?.day ?? - (this.selectedDateTime && this.currentDateTime.day) ?? - undefined, + value, }; - case 'hour': let min = 0; let max = 23; @@ -773,42 +839,21 @@ export class InputSegments extends TextfieldBase { return { minValue: min, maxValue: max, - value: - this.newDateTime?.hour ?? - (this.selectedDateTime && this.currentDateTime.hour) ?? - undefined, + value, }; - case 'minute': case 'second': - const minutes = - this.newDateTime?.minute ?? - (this.selectedDateTime && this.currentDateTime.minute) ?? - undefined; - - const seconds = - this.newDateTime?.second ?? - (this.selectedDateTime && this.currentDateTime.second) ?? - undefined; - return { minValue: 0, maxValue: 59, - value: type === 'minute' ? minutes : seconds, + value, }; - case 'dayPeriod': return { minValue: AM, maxValue: PM, - value: - (this.newDateTime?.hour && - this.getAmPmModifier(this.newDateTime.hour)) ?? - (this.selectedDateTime && - this.getAmPmModifier(this.currentDateTime.hour)) ?? - undefined, + value, }; - default: return {}; } @@ -887,7 +932,7 @@ export class InputSegments extends TextfieldBase { * their initial values */ private resetHourAndDayPeriod(): void { - const dayPeriod = this.getSegmentDetails('dayPeriod'); + const dayPeriod = this.getSegmentValueAndLimits('dayPeriod'); if (this.dayPeriodSegment) { this.dayPeriodSegment.value = dayPeriod.value; @@ -900,7 +945,7 @@ export class InputSegments extends TextfieldBase { } } - const hour = this.getSegmentDetails('hour'); + const hour = this.getSegmentValueAndLimits('hour'); if (this.hourSegment) { this.hourSegment.minValue = hour.minValue; @@ -923,7 +968,6 @@ export class InputSegments extends TextfieldBase { this.formatValues(segment); this.setNewDateTime(); - this.requestUpdate(); if (this.newDateTime) { From ebe93c3570ef5955e1c31a7263b5c188bada491d Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Fri, 21 Jul 2023 10:54:50 +0100 Subject: [PATCH 12/37] refactor: reduce cognitive complexity for `updateHour()` method * "Refactor this function to reduce its Cognitive Complexity from 21 to the 15 allowed." (SonarLint) --- packages/input-segments/src/InputSegments.ts | 41 +++++++++++--------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index ed4969936d0..9e24d437b5f 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -906,24 +906,29 @@ export class InputSegments extends TextfieldBase { * match the new period (AM or PM). In addition, the minimum and maximum values of the hour are also changed */ private updateHour(): void { - if (this.hourSegment && this.dayPeriodSegment) { - if (this.dayPeriodSegment.value !== undefined) { - const isAM = this.dayPeriodSegment.value === AM; - const isPM = this.dayPeriodSegment.value === PM; - - this.hourSegment.minValue = isPM ? minHourPM : minHourAM; - this.hourSegment.maxValue = isPM ? maxHourPM : maxHourAM; - - if (this.hourSegment.value !== undefined) { - if (isAM && this.isPM(this.hourSegment.value)) { - this.hourSegment.value -= PM; - } else if (isPM && !this.isPM(this.hourSegment.value)) { - this.hourSegment.value += PM; - } - } - } else { - this.resetHourAndDayPeriod(); - } + if (!this.hourSegment || !this.dayPeriodSegment) { + this.resetHourAndDayPeriod(); + return; + } + + if (this.dayPeriodSegment.value === undefined) { + return; + } + + const isAM = this.dayPeriodSegment.value === AM; + const isPM = this.dayPeriodSegment.value === PM; + + this.hourSegment.minValue = isPM ? minHourPM : minHourAM; + this.hourSegment.maxValue = isPM ? maxHourPM : maxHourAM; + + if (this.hourSegment.value === undefined) { + return; + } + + if (isAM && this.isPM(this.hourSegment.value)) { + this.hourSegment.value -= PM; + } else if (isPM && !this.isPM(this.hourSegment.value)) { + this.hourSegment.value += PM; } } From 7dca5fcc0a28a61a1abde955c6b3fd487aede86f Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Fri, 21 Jul 2023 12:14:22 +0100 Subject: [PATCH 13/37] refactor: add missing comments and minor code improvements --- packages/input-segments/src/InputSegments.ts | 157 ++++++++++++++----- 1 file changed, 115 insertions(+), 42 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 9e24d437b5f..c5c4aa53840 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -67,9 +67,6 @@ export class InputSegments extends TextfieldBase { return [...super.styles, styles]; } - @query('.editable-segment') - firstEditableSegment!: HTMLDivElement; - /** * Indicates when date segments should be included in the field */ @@ -109,6 +106,9 @@ export class InputSegments extends TextfieldBase { @state() private createSegments = true; + @query('.editable-segment') + firstEditableSegment!: HTMLDivElement; + private languageResolver = new LanguageResolutionController(this); private timeZone = getLocalTimeZone(); private formatter!: DateFormatter; @@ -271,10 +271,18 @@ export class InputSegments extends TextfieldBase { `; } + /** + * Indicates the parent component when a segment is focused, this way we can apply all styles to the "fake" input + * (wrapper) as if it were a standard input + */ public handleFocusIn(): void { super.onFocus(); } + /** + * Indicates the parent component when a segment is blurred, this way we can remove all styles that were applied to + * the "fake" input (wrapper) while one of the segments was focused + */ public handleFocusOut(): void { super.onBlur(); } @@ -394,6 +402,9 @@ export class InputSegments extends TextfieldBase { this.valueChanged(segment); } + /** + * Defines the formatter that will be used in the creation of segments + */ private setFormatter(): void { let dateOptions: Intl.DateTimeFormatOptions = {}; let timeOptions: Intl.DateTimeFormatOptions = {}; @@ -407,14 +418,15 @@ export class InputSegments extends TextfieldBase { } if (this.includeTime) { - const useMinutes = ( - ['minute', 'second'] as TimeGranularity[] - ).includes(this.timeGranularity); + const useMinutes: TimeGranularity[] = ['minute', 'second']; + + const includeMinutes = useMinutes.includes(this.timeGranularity); + const includeSeconds = this.timeGranularity === 'second'; timeOptions = { hour: '2-digit', - ...(useMinutes && { minute: '2-digit' }), - ...(this.timeGranularity === 'second' && { second: '2-digit' }), + ...(includeMinutes && { minute: '2-digit' }), + ...(includeSeconds && { second: '2-digit' }), }; } @@ -424,28 +436,41 @@ export class InputSegments extends TextfieldBase { }); } + /** + * * Defines the number parser using the defined locale + */ private setNumberParser(): void { this.numberParser = new NumberParser(this.locale, { maximumFractionDigits: 0, }); } + /** + * Defines the initial date and time that will be used to render the input, if no specific date and time is provided + */ private setInitialDateTime(): void { this.currentDateTime = toCalendarDateTime(now(this.timeZone)); } + /** + * If a datetime is received by the component via property, it will use it as the current datetime to render the + * input + */ private setCurrentDateTime(): void { - if (this.selectedDateTime) { - this.selectedDateTime = new Date(this.selectedDateTime); + if (!this.selectedDateTime) { + return; + } - if (!this.isValidTime(this.selectedDateTime)) { - this.selectedDateTime = undefined; - } else { - this.currentDateTime = this.dateToCalendarDateTime( - this.selectedDateTime - ); - } + this.selectedDateTime = new Date(this.selectedDateTime); + + if (!this.isValidTime(this.selectedDateTime)) { + this.selectedDateTime = undefined; + return; } + + this.currentDateTime = this.dateToCalendarDateTime( + this.selectedDateTime + ); } /** @@ -519,14 +544,17 @@ export class InputSegments extends TextfieldBase { } /** - * Converts an object of type `Date` to `Calendar DateTime` + * Converts an object of type `Date` to `CalendarDateTime` * * @param date - `Date` object to "convert" */ private dateToCalendarDateTime(date: Date): CalendarDateTime { return new CalendarDateTime( date.getFullYear(), - date.getMonth() + 1, // The month to create a new `CalendarDate` cannot be a zero-based index, unlike `Date` + + // The month to create a new `CalendarDateTime` cannot be a zero-based index, unlike `Date` + date.getMonth() + 1, + date.getDate(), date.getHours(), date.getMinutes(), @@ -535,7 +563,7 @@ export class InputSegments extends TextfieldBase { } /** - * Determines which segments will be used by the input (date only, time only or both types) + * Creates the segments that will be used by the input */ private setSegments(): void { const dateTime = this.currentDateTime.toDate(this.timeZone); @@ -859,48 +887,71 @@ export class InputSegments extends TextfieldBase { } } + /** + * Increments the segment value respecting the minimum and maximum limits + * + * @param segment - The segment being changed + */ private incrementValue(segment: Segment): void { const min = segment.minValue; const max = segment.maxValue; - if (min !== undefined && max !== undefined) { - if (segment.value === undefined) { - segment.value = min; - } else if (segment.type === 'dayPeriod') { - segment.value = segment.value === AM ? PM : AM; - } else { - segment.value++; + if (min === undefined || max === undefined) { + return; + } - if (segment.value > max) { - segment.value = min; - } + if (segment.value === undefined) { + segment.value = min; + } else if (segment.type === 'dayPeriod') { + segment.value = this.toggleDayPeriod(segment.value); + } else { + segment.value++; + + if (segment.value > max) { + segment.value = min; } } this.valueChanged(segment); } + /** + * Decrements the segment value respecting the minimum and maximum limits + * + * @param segment - The segment being changed + */ private decrementValue(segment: Segment): void { const min = segment.minValue; const max = segment.maxValue; - if (min !== undefined && max !== undefined) { - if (segment.value === undefined) { - segment.value = max; - } else if (segment.type === 'dayPeriod') { - segment.value = segment.value === AM ? PM : AM; - } else { - segment.value--; + if (min === undefined || max === undefined) { + return; + } - if (segment.value < min) { - segment.value = max; - } + if (segment.value === undefined) { + segment.value = max; + } else if (segment.type === 'dayPeriod') { + segment.value = this.toggleDayPeriod(segment.value); + } else { + segment.value--; + + if (segment.value < min) { + segment.value = max; } } this.valueChanged(segment); } + /** + * Switches the value of the `dayPeriod` segment from `AM` to `PM` or vice versa + * + * @param value - Current value of segment `dayPeriod` + */ + private toggleDayPeriod(value: number): typeof AM | typeof PM { + return value === AM ? PM : AM; + } + /** * When the day period is changed, it automatically adjusts the hour if it has already been informed previously to * match the new period (AM or PM). In addition, the minimum and maximum values of the hour are also changed @@ -966,6 +1017,12 @@ export class InputSegments extends TextfieldBase { } } + /** + * After defining the new segment value, it formats the values that will be displayed on the screen and prepares the + * object that will be emitted by the component, if it is ready + * + * @param segment - The segment that was changed + */ private valueChanged(segment: Segment): void { if (this.is12HourClock && segment.type === 'dayPeriod') { this.updateHour(); @@ -1111,14 +1168,30 @@ export class InputSegments extends TextfieldBase { return newValue; } + /** + * Focuses on the next editable segment, if any + * + * @param event - Event details + */ private focusNextSegment(event: KeyboardEvent): void { this.focusSegment(event.target as HTMLDivElement, 'next'); } + /** + * Focuses on the previous editable segment, if any + * + * @param event - Event details + */ private focusPreviousSegment(event: KeyboardEvent): void { this.focusSegment(event.target as HTMLDivElement, 'previous'); } + /** + * Focuses the segment according to the direction, if there is one to focus on + * + * @param segment - Segment that is currently focused + * @param elementToFocus - Defines which element will be focused: is it the previous one or the next one? + */ private focusSegment( segment: HTMLDivElement, elementToFocus: 'previous' | 'next' @@ -1141,9 +1214,9 @@ export class InputSegments extends TextfieldBase { if (siblingSegment.getAttribute('contenteditable')) { segmentFound = true; siblingSegment.focus(); - } else { - currentSegment = siblingSegment; } + + currentSegment = siblingSegment; } } } From d8cfe99f15e3763dd606418e35d9f71088dbe86b Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Fri, 21 Jul 2023 12:35:48 +0100 Subject: [PATCH 14/37] refactor: preserve white spaces inside literal segments --- packages/input-segments/src/InputSegments.ts | 10 +++++++--- packages/input-segments/src/input-segments.css | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index c5c4aa53840..a60a0427b2d 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -217,14 +217,18 @@ export class InputSegments extends TextfieldBase { } public renderLiteralSegment(segment: Segment): TemplateResult { + /** + * We use `.innerText` here to preserve the text exactly as it is, including all spaces. If we use string + * interpolation, the value can be changed because of the code format, in which the contents of the tags are + * moved to a separate line, and this causes problems for the contents of some literal segments + */ return html` + .innerText=${segment.formatted ?? ''} + > `; } diff --git a/packages/input-segments/src/input-segments.css b/packages/input-segments/src/input-segments.css index 82c759bb9cb..24ad3ceb824 100644 --- a/packages/input-segments/src/input-segments.css +++ b/packages/input-segments/src/input-segments.css @@ -29,18 +29,19 @@ governing permissions and limitations under the License. display: inline-block; height: 100%; color: var(--spectrum-textfield-text-color-default); - white-space: nowrap; } .editable-segment { padding: 0 0.15em; font-variant-numeric: tabular-nums; text-align: end; + white-space: nowrap; caret-color: transparent; outline: none; } .literal-segment { + white-space: pre; user-select: none; } From ba56c75d6078c2b939b1e80a11d7b96d8bda636f Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Fri, 21 Jul 2023 14:31:43 +0100 Subject: [PATCH 15/37] refactor: add types to variables --- packages/input-segments/src/InputSegments.ts | 8 +++++--- tools/base/src/directives.ts | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index a60a0427b2d..b4d02aea8f2 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -31,8 +31,10 @@ import { state, } from '@spectrum-web-components/base/src/decorators.js'; import { + ClassInfo, classMap, ifDefined, + StyleInfo, styleMap, when, } from '@spectrum-web-components/base/src/directives.js'; @@ -237,12 +239,12 @@ export class InputSegments extends TextfieldBase { const isPlaceholderVisible = segment.value === undefined; - const segmentClasses = { + const segmentClasses: ClassInfo = { 'is-placeholder': isPlaceholderVisible, }; - const segmentStyles = { - minWidth: + const segmentStyles: StyleInfo = { + 'min-width': segment.maxValue !== undefined ? `${String(segment.maxValue).length}ch` : undefined, diff --git a/tools/base/src/directives.ts b/tools/base/src/directives.ts index 16780ee6e9b..6e16d107d88 100644 --- a/tools/base/src/directives.ts +++ b/tools/base/src/directives.ts @@ -12,6 +12,7 @@ governing permissions and limitations under the License. export { ifDefined } from 'lit/directives/if-defined.js'; export { repeat } from 'lit/directives/repeat.js'; +export type { ClassInfo } from 'lit/directives/class-map.js'; export { classMap } from 'lit/directives/class-map.js'; export type { ClassInfo } from 'lit/directives/class-map.js'; export { styleMap } from 'lit/directives/style-map.js'; From a9303e177b5424224ea8b75e6525599014b4421f Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Fri, 21 Jul 2023 16:24:02 +0100 Subject: [PATCH 16/37] refactor: formatting year --- packages/input-segments/src/InputSegments.ts | 35 ++++++++++---------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index b4d02aea8f2..4c9596651be 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -624,30 +624,34 @@ export class InputSegments extends TextfieldBase { const options: Intl.DateTimeFormatOptions = {}; - let year = this.currentDateTime.year; - let month = this.currentDateTime.month; - let day = this.currentDateTime.day; + const year = this.yearSegment?.value ?? this.currentDateTime.year; + const month = this.monthSegment?.value ?? this.currentDateTime.month; + const day = this.daySegment?.value ?? this.currentDateTime.day; - let hour = this.currentDateTime.hour; - let minute = this.currentDateTime.minute; - let second = this.currentDateTime.second; + // The hour can be changed if we are formatting the "dayPeriod" segment + let hour = this.hourSegment?.value ?? this.currentDateTime.hour; + + const minute = this.minuteSegment?.value ?? this.currentDateTime.minute; + const second = this.secondSegment?.value ?? this.currentDateTime.second; + + /** + * For the year we do not use the value returned by the formatter, to avoid that the typed year is displayed in + * an unexpected way. For example, when typing "2", the year would be formatted as "1902", but we keep it as it + * is being displayed on the screen. If the user wants to enter the year "1902", he will enter number by number + */ + if (segment.type === 'year') { + segment.formatted = String(year); + return; + } let padMaxLength = 2; switch (segment.type) { - case 'year': { - year = segment.value; - options.year = 'numeric'; - padMaxLength = 0; - break; - } case 'month': { - month = segment.value; options.month = '2-digit'; break; } case 'day': { - day = segment.value; options.day = '2-digit'; break; } @@ -656,17 +660,14 @@ export class InputSegments extends TextfieldBase { padMaxLength = 1; } - hour = segment.value; options.hour = 'numeric'; break; } case 'minute': { - minute = segment.value; options.minute = '2-digit'; break; } case 'second': { - second = segment.value; options.second = '2-digit'; break; } From 1dc517b373072c58e714bfcee8d273f79aea4133 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Fri, 21 Jul 2023 16:57:57 +0100 Subject: [PATCH 17/37] refactor: increment/decrement year when is `undefined` --- packages/input-segments/src/InputSegments.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 4c9596651be..274df6b9003 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -908,7 +908,8 @@ export class InputSegments extends TextfieldBase { } if (segment.value === undefined) { - segment.value = min; + segment.value = + segment.type === 'year' ? this.currentDateTime.year : min; } else if (segment.type === 'dayPeriod') { segment.value = this.toggleDayPeriod(segment.value); } else { @@ -936,7 +937,8 @@ export class InputSegments extends TextfieldBase { } if (segment.value === undefined) { - segment.value = max; + segment.value = + segment.type === 'year' ? this.currentDateTime.year : max; } else if (segment.type === 'dayPeriod') { segment.value = this.toggleDayPeriod(segment.value); } else { From f4ad08393d37eab1385f769c2b103e060687b674 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Mon, 24 Jul 2023 09:48:01 +0100 Subject: [PATCH 18/37] feat: add logic to update day if needed --- packages/input-segments/src/InputSegments.ts | 84 ++++++++++++++++---- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 274df6b9003..84b21865786 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -12,6 +12,7 @@ governing permissions and limitations under the License. import { CalendarDateTime, DateFormatter, + endOfMonth, getLocalTimeZone, getMinimumDayInMonth, getMinimumMonthInYear, @@ -58,6 +59,15 @@ import { import styles from './input-segments.css.js'; +/** + * An utility to check if the given value is a number (not `undefined`) + * + * @param value - Number to check + */ +const isNumber = (value: number | undefined): value is number => { + return typeof value === 'number'; +}; + /** * @event change - Announces when a new date/time is defined by emitting a `Date` object * @@ -244,10 +254,9 @@ export class InputSegments extends TextfieldBase { }; const segmentStyles: StyleInfo = { - 'min-width': - segment.maxValue !== undefined - ? `${String(segment.maxValue).length}ch` - : undefined, + 'min-width': isNumber(segment.maxValue) + ? `${String(segment.maxValue).length}ch` + : undefined, }; // TODO: Include ARIA attributes for editable segments @@ -484,10 +493,6 @@ export class InputSegments extends TextfieldBase { * each type (date only, time only or date and time together) were defined */ private setNewDateTime(): void { - const defined = (value: number | undefined): value is number => { - return typeof value === 'number'; - }; - this.newDateTime = undefined; // If none of the date/time segments are being used, there is nothing to do here @@ -501,7 +506,7 @@ export class InputSegments extends TextfieldBase { // When only date segments are being used if (this.includeDate && !this.includeTime) { - if (defined(year) && defined(month) && defined(day)) { + if (isNumber(year) && isNumber(month) && isNumber(day)) { this.newDateTime = new CalendarDateTime(year, month, day); } @@ -524,11 +529,14 @@ export class InputSegments extends TextfieldBase { const isSecond = this.timeGranularity === 'second'; const hasTime = - (isHour && defined(hour)) || - (isMinute && defined(hour) && defined(minute)) || - (isSecond && defined(hour) && defined(minute) && defined(second)); - - if (defined(year) && defined(month) && defined(day) && hasTime) { + (isHour && isNumber(hour)) || + (isMinute && isNumber(hour) && isNumber(minute)) || + (isSecond && + isNumber(hour) && + isNumber(minute) && + isNumber(second)); + + if (isNumber(year) && isNumber(month) && isNumber(day) && hasTime) { this.newDateTime = new CalendarDateTime( year, month, @@ -1016,7 +1024,7 @@ export class InputSegments extends TextfieldBase { this.hourSegment.minValue = hour.minValue; this.hourSegment.maxValue = hour.maxValue; - if (this.hourSegment.value !== undefined) { + if (isNumber(this.hourSegment.value)) { this.hourSegment.value += this.getAmPmModifier( this.currentDateTime.hour ); @@ -1026,9 +1034,38 @@ export class InputSegments extends TextfieldBase { } } + /** + * It validates if the day is valid for the given month and, if it is above the maximum limit, it changes the day to + * correspond to the last day of that month. Also, if the month segment has changed, updates the maximum limit of + * day segment + * + * @param monthChanged - Indicates whether it was the month segment that was changed + */ + private updateDay(monthChanged: boolean): void { + if ( + this.daySegment?.value === undefined || + this.monthSegment?.value === undefined + ) { + return; + } + + const lastDayOfMonth = endOfMonth( + this.currentDateTime.set({ month: this.monthSegment.value }) + ); + + if (this.daySegment.value > lastDayOfMonth.day) { + this.daySegment.value = lastDayOfMonth.day; + this.formatValues(this.daySegment); + } + + if (monthChanged) { + this.daySegment.maxValue = lastDayOfMonth.day; + } + } + /** * After defining the new segment value, it formats the values that will be displayed on the screen and prepares the - * object that will be emitted by the component, if it is ready + * object that will be emitted by the component, if it is ready/defined * * @param segment - The segment that was changed */ @@ -1037,6 +1074,21 @@ export class InputSegments extends TextfieldBase { this.updateHour(); } + const hasDay = isNumber(this.daySegment?.value); + const hasMonth = isNumber(this.monthSegment?.value); + + const dayChanged = segment.type === 'day'; + const monthChanged = segment.type === 'month'; + const yearChanged = segment.type === 'year'; + + if ( + (dayChanged && hasMonth) || + (monthChanged && hasDay) || + (yearChanged && hasDay && hasMonth) + ) { + this.updateDay(monthChanged); + } + this.formatValues(segment); this.setNewDateTime(); this.requestUpdate(); From 5e07468460d70bc8dfc1789ae5c986b282516655 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Mon, 24 Jul 2023 13:59:31 +0100 Subject: [PATCH 19/37] fix: improve logic to update the day --- packages/input-segments/src/InputSegments.ts | 42 ++++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 84b21865786..b01a4569ea2 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -1036,30 +1036,34 @@ export class InputSegments extends TextfieldBase { /** * It validates if the day is valid for the given month and, if it is above the maximum limit, it changes the day to - * correspond to the last day of that month. Also, if the month segment has changed, updates the maximum limit of - * day segment + * correspond to the last day of that month. In addition, updates the maximum limit of day segment * - * @param monthChanged - Indicates whether it was the month segment that was changed + * @param updateDayMaxValue - Indicates when the maximum allowed value for the day should be changed */ - private updateDay(monthChanged: boolean): void { + private updateDay(): void { if ( - this.daySegment?.value === undefined || - this.monthSegment?.value === undefined + this.monthSegment?.value === undefined || + this.daySegment === undefined ) { return; } + const useThisDate = isNumber(this.yearSegment?.value) + ? this.currentDateTime.set({ year: this.yearSegment?.value }) + : this.currentDateTime.copy(); + const lastDayOfMonth = endOfMonth( - this.currentDateTime.set({ month: this.monthSegment.value }) + useThisDate.set({ month: this.monthSegment.value }) ); - if (this.daySegment.value > lastDayOfMonth.day) { - this.daySegment.value = lastDayOfMonth.day; - this.formatValues(this.daySegment); - } + this.daySegment.maxValue = lastDayOfMonth.day; - if (monthChanged) { - this.daySegment.maxValue = lastDayOfMonth.day; + if ( + isNumber(this.daySegment.value) && + this.daySegment.value > this.daySegment.maxValue + ) { + this.daySegment.value = this.daySegment.maxValue; + this.formatValues(this.daySegment); } } @@ -1077,16 +1081,12 @@ export class InputSegments extends TextfieldBase { const hasDay = isNumber(this.daySegment?.value); const hasMonth = isNumber(this.monthSegment?.value); - const dayChanged = segment.type === 'day'; - const monthChanged = segment.type === 'month'; - const yearChanged = segment.type === 'year'; - if ( - (dayChanged && hasMonth) || - (monthChanged && hasDay) || - (yearChanged && hasDay && hasMonth) + segment.type === 'month' || + (segment.type === 'day' && hasMonth) || + (segment.type === 'year' && hasDay && hasMonth) ) { - this.updateDay(monthChanged); + this.updateDay(); } this.formatValues(segment); From 81755e6a3652248e94e64dbabd1db11f5008ccf7 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Mon, 24 Jul 2023 14:05:34 +0100 Subject: [PATCH 20/37] fix: package version --- packages/input-segments/package.json | 2 +- yarn.lock | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/input-segments/package.json b/packages/input-segments/package.json index 7c78a145315..d8f6f498efa 100644 --- a/packages/input-segments/package.json +++ b/packages/input-segments/package.json @@ -58,7 +58,7 @@ ], "dependencies": { "@internationalized/date": "^3.2.1", - "@internationalized/number": "^3.2.1", + "@internationalized/number": "^3.1.0", "@spectrum-web-components/base": "^0.34.0", "@spectrum-web-components/reactive-controllers": "^0.34.0", "@spectrum-web-components/textfield": "^0.34.0" diff --git a/yarn.lock b/yarn.lock index 864d7c44161..1a77dbd3ba1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2520,13 +2520,6 @@ dependencies: "@swc/helpers" "^0.4.14" -"@internationalized/number@^3.2.1": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.2.1.tgz#570e4010544a84a8225e65b34a689a36187caaa8" - integrity sha512-hK30sfBlmB1aIe3/OwAPg9Ey0DjjXvHEiGVhNaOiBJl31G0B6wMaX8BN3ibzdlpyRNE9p7X+3EBONmxtJO9Yfg== - dependencies: - "@swc/helpers" "^0.5.0" - "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" From f93dd8e0970f57ba5d7e5cc92bc2c788163f0a92 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Mon, 24 Jul 2023 16:53:46 +0100 Subject: [PATCH 21/37] fix: add missing references --- packages/time-field/tsconfig.json | 5 ++++- tools/bundle/tsconfig.json | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/time-field/tsconfig.json b/packages/time-field/tsconfig.json index c90873db4cf..7732f273ad0 100644 --- a/packages/time-field/tsconfig.json +++ b/packages/time-field/tsconfig.json @@ -6,5 +6,8 @@ }, "include": ["*.ts", "src/*.ts"], "exclude": ["test/*.ts", "stories/*.ts"], - "references": [{ "path": "../../tools/base" }] + "references": [ + { "path": "../../tools/base" }, + { "path": "../input-segments" } + ] } diff --git a/tools/bundle/tsconfig.json b/tools/bundle/tsconfig.json index bc2b6774fe0..9168dae82ef 100644 --- a/tools/bundle/tsconfig.json +++ b/tools/bundle/tsconfig.json @@ -39,6 +39,7 @@ { "path": "../../packages/icons-workflow" }, { "path": "../../packages/iconset" }, { "path": "../../packages/illustrated-message" }, + { "path": "../../packages/input-segments" }, { "path": "../../packages/link" }, { "path": "../../packages/menu" }, { "path": "../../packages/meter" }, @@ -64,6 +65,7 @@ { "path": "../../packages/tags" }, { "path": "../../packages/textfield" }, { "path": "../theme" }, + { "path": "../../packages/time-field" }, { "path": "../../packages/thumbnail" }, { "path": "../../packages/toast" }, { "path": "../../packages/tooltip" }, From 4e9e085ef7b6c74e8adf76a56878b1caf5fb9e05 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Mon, 24 Jul 2023 17:03:35 +0100 Subject: [PATCH 22/37] refactor: maintain alphabetical order --- tools/bundle/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/bundle/tsconfig.json b/tools/bundle/tsconfig.json index 9168dae82ef..56f9013871c 100644 --- a/tools/bundle/tsconfig.json +++ b/tools/bundle/tsconfig.json @@ -65,8 +65,8 @@ { "path": "../../packages/tags" }, { "path": "../../packages/textfield" }, { "path": "../theme" }, - { "path": "../../packages/time-field" }, { "path": "../../packages/thumbnail" }, + { "path": "../../packages/time-field" }, { "path": "../../packages/toast" }, { "path": "../../packages/tooltip" }, { "path": "../../packages/top-nav" }, From 85a3aa8b93cf355f9f622fcc2e8766ac5d4edb98 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 1 Aug 2023 11:04:28 +0100 Subject: [PATCH 23/37] fix: duplicate identifier 'ClassInfo' --- tools/base/src/directives.ts | 1 - yarn.lock | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tools/base/src/directives.ts b/tools/base/src/directives.ts index 6e16d107d88..16780ee6e9b 100644 --- a/tools/base/src/directives.ts +++ b/tools/base/src/directives.ts @@ -12,7 +12,6 @@ governing permissions and limitations under the License. export { ifDefined } from 'lit/directives/if-defined.js'; export { repeat } from 'lit/directives/repeat.js'; -export type { ClassInfo } from 'lit/directives/class-map.js'; export { classMap } from 'lit/directives/class-map.js'; export type { ClassInfo } from 'lit/directives/class-map.js'; export { styleMap } from 'lit/directives/style-map.js'; diff --git a/yarn.lock b/yarn.lock index 1a77dbd3ba1..749bf73e47d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,16 +2500,16 @@ integrity sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA== "@internationalized/date@^3.2.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.3.0.tgz#90386d4b4e707f28507d1a1b3cc0e162ad5ee038" - integrity sha512-qfRd7jCIgUjabI8RxeAsxhLDRS1u8eUPX96GB5uBp1Tpm6YY6dVveE7YwsTEV6L4QOp5LKFirFHHGsL/XQwJIA== + version "3.4.0" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.4.0.tgz#e843ac40b04afafe99fe0a41bae7abdd221a9a44" + integrity sha512-QUDSGCsvrEVITVf+kv9VSAraAmCgjQmU5CiXtesUBBhBe374NmnEIIaOFBZ72t29dfGMBP0zF+v6toVnbcc6jg== dependencies: "@swc/helpers" "^0.5.0" "@internationalized/date@^3.2.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.4.0.tgz#e843ac40b04afafe99fe0a41bae7abdd221a9a44" - integrity sha512-QUDSGCsvrEVITVf+kv9VSAraAmCgjQmU5CiXtesUBBhBe374NmnEIIaOFBZ72t29dfGMBP0zF+v6toVnbcc6jg== + version "3.3.0" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.3.0.tgz#90386d4b4e707f28507d1a1b3cc0e162ad5ee038" + integrity sha512-qfRd7jCIgUjabI8RxeAsxhLDRS1u8eUPX96GB5uBp1Tpm6YY6dVveE7YwsTEV6L4QOp5LKFirFHHGsL/XQwJIA== dependencies: "@swc/helpers" "^0.5.0" From a88a69abb5c9da77255d6c84589b4a7a96735ec5 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 1 Aug 2023 11:07:44 +0100 Subject: [PATCH 24/37] refactor: upgrade `@internationalized/date` package version used by the calendar --- packages/calendar/package.json | 2 +- yarn.lock | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/calendar/package.json b/packages/calendar/package.json index 5cbdc963345..a35c1c20aec 100644 --- a/packages/calendar/package.json +++ b/packages/calendar/package.json @@ -61,7 +61,7 @@ "lit-html" ], "dependencies": { - "@internationalized/date": "^3.2.0", + "@internationalized/date": "^3.2.1", "@internationalized/number": "^3.1.0", "@spectrum-web-components/action-button": "^0.35.0", "@spectrum-web-components/base": "^0.35.0", diff --git a/yarn.lock b/yarn.lock index 749bf73e47d..1b00de26fd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2499,13 +2499,6 @@ resolved "https://registry.yarnpkg.com/@import-maps/resolve/-/resolve-1.0.1.tgz#1e9fcadcf23aa0822256a329aabca241879d37c9" integrity sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA== -"@internationalized/date@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.4.0.tgz#e843ac40b04afafe99fe0a41bae7abdd221a9a44" - integrity sha512-QUDSGCsvrEVITVf+kv9VSAraAmCgjQmU5CiXtesUBBhBe374NmnEIIaOFBZ72t29dfGMBP0zF+v6toVnbcc6jg== - dependencies: - "@swc/helpers" "^0.5.0" - "@internationalized/date@^3.2.1": version "3.3.0" resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.3.0.tgz#90386d4b4e707f28507d1a1b3cc0e162ad5ee038" From dcf80b9b81a3e4fe88dd4bc59a45382811dd791c Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 1 Aug 2023 11:37:26 +0100 Subject: [PATCH 25/37] refactor: minor code improvements and comments --- packages/input-segments/src/InputSegments.ts | 38 +++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index b01a4569ea2..9b693bbf060 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -761,21 +761,25 @@ export class InputSegments extends TextfieldBase { /** * Checks whether the segment being created or updated will have a value or not by checking the following order: * - * 1. Did the segment already have a previously defined value? If so, keep the same value + * 1. Did the segment already have a previously defined value? If yes, use it * * 2. Since the segment doesn't have a previous value to keep, was the component given a specific date/time when it * was created? If yes, then use that information * * 3. There is no value to use at this point, so it will remain as `undefined` * - * @param newValue - Current segment value, if any - * @param currentValue - Current date/time value with the same segment type as the current value + * @param previousValue - Previous segment value, if there is one + * @param currentValue - Current segment value */ - private useNewOrCurrentValue( - newValue: number | undefined, + private usePreviousOrCurrentValue( + previousValue: number | undefined, currentValue: number ): number | undefined { - return newValue ?? (this.selectedDateTime && currentValue) ?? undefined; + return ( + previousValue ?? + (this.selectedDateTime && currentValue) ?? + undefined + ); } /** @@ -788,38 +792,38 @@ export class InputSegments extends TextfieldBase { ): number | undefined { switch (type) { case 'year': - return this.useNewOrCurrentValue( + return this.usePreviousOrCurrentValue( this.yearSegment?.value, this.currentDateTime.year ); case 'month': - return this.useNewOrCurrentValue( + return this.usePreviousOrCurrentValue( this.monthSegment?.value, this.currentDateTime.month ); case 'day': - return this.useNewOrCurrentValue( + return this.usePreviousOrCurrentValue( this.daySegment?.value, this.currentDateTime.day ); case 'hour': - return this.useNewOrCurrentValue( + return this.usePreviousOrCurrentValue( this.hourSegment?.value, this.currentDateTime.hour ); case 'minute': - return this.useNewOrCurrentValue( + return this.usePreviousOrCurrentValue( this.minuteSegment?.value, this.currentDateTime.minute ); case 'second': - return this.useNewOrCurrentValue( + return this.usePreviousOrCurrentValue( this.secondSegment?.value, this.currentDateTime.second ); case 'dayPeriod': // To identify the current value of "AM/PM", we use the value of the hour, not the day period itself - return this.useNewOrCurrentValue( + return this.usePreviousOrCurrentValue( this.hourSegment?.value && this.getAmPmModifier(this.hourSegment.value), this.getAmPmModifier(this.currentDateTime.hour) @@ -1005,9 +1009,9 @@ export class InputSegments extends TextfieldBase { * their initial values */ private resetHourAndDayPeriod(): void { - const dayPeriod = this.getSegmentValueAndLimits('dayPeriod'); - if (this.dayPeriodSegment) { + const dayPeriod = this.getSegmentValueAndLimits('dayPeriod'); + this.dayPeriodSegment.value = dayPeriod.value; this.dayPeriodSegment.minValue = dayPeriod.minValue; this.dayPeriodSegment.maxValue = dayPeriod.maxValue; @@ -1018,9 +1022,9 @@ export class InputSegments extends TextfieldBase { } } - const hour = this.getSegmentValueAndLimits('hour'); - if (this.hourSegment) { + const hour = this.getSegmentValueAndLimits('hour'); + this.hourSegment.minValue = hour.minValue; this.hourSegment.maxValue = hour.maxValue; From 9dc01f6e02edd21793802119765ac4aea1f0ec05 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 15 Aug 2023 14:33:59 +0100 Subject: [PATCH 26/37] refactor: add new items to to-do list --- packages/calendar/README.md | 2 ++ packages/input-segments/README.md | 2 ++ packages/time-field/README.md | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/packages/calendar/README.md b/packages/calendar/README.md index 36045970735..b2d5ec0235b 100644 --- a/packages/calendar/README.md +++ b/packages/calendar/README.md @@ -38,3 +38,5 @@ import { Calendar } from '@spectrum-web-components/calendar'; - Implement keyboard navigation - Add support for other types of calendars - [React Calendar: International Calendars](https://react-spectrum.adobe.com/react-spectrum/Calendar.html#international-calendars) - Include `aria-label` in calendar for accessibility - [React Calendar: Labeling](https://react-spectrum.adobe.com/react-spectrum/Calendar.html#labeling) +- Complete documentation +- Add/Review unit tests diff --git a/packages/input-segments/README.md b/packages/input-segments/README.md index b9f057c4a16..d8ec940c3ab 100644 --- a/packages/input-segments/README.md +++ b/packages/input-segments/README.md @@ -23,3 +23,5 @@ export class MyInput extends InputSegments { - Include ARIA attributes for editable segments - Use `@input`/`@beforeinput` events to handle data input/content cleanup +- Complete documentation +- Add/Review unit tests diff --git a/packages/time-field/README.md b/packages/time-field/README.md index b0690625afb..808d31e2ac8 100644 --- a/packages/time-field/README.md +++ b/packages/time-field/README.md @@ -26,3 +26,8 @@ import { TimeField } from '@spectrum-web-components/time-field'; ```html ``` + +## To-do list + +- Complete documentation +- Add/Review unit tests From bd8163e7ede2aa5e4081083e3918929b96e3fdf9 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 15 Aug 2023 14:37:25 +0100 Subject: [PATCH 27/37] refactor: add missing "time-field" entry --- tools/bundle/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/bundle/package.json b/tools/bundle/package.json index 06f883bad09..766ac13cff2 100644 --- a/tools/bundle/package.json +++ b/tools/bundle/package.json @@ -127,6 +127,7 @@ "@spectrum-web-components/textfield": "^0.35.0", "@spectrum-web-components/theme": "^0.35.0", "@spectrum-web-components/thumbnail": "^0.35.0", + "@spectrum-web-components/time-field": "^0.0.1", "@spectrum-web-components/toast": "^0.35.0", "@spectrum-web-components/tooltip": "^0.35.0", "@spectrum-web-components/top-nav": "^0.35.0", From e54ffba0e0ec004fae8865084da5fe25d02682e2 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 15 Aug 2023 14:42:19 +0100 Subject: [PATCH 28/37] refactor: update packages version --- packages/input-segments/package.json | 6 +-- yarn.lock | 75 ---------------------------- 2 files changed, 3 insertions(+), 78 deletions(-) diff --git a/packages/input-segments/package.json b/packages/input-segments/package.json index d8f6f498efa..594b608945e 100644 --- a/packages/input-segments/package.json +++ b/packages/input-segments/package.json @@ -59,9 +59,9 @@ "dependencies": { "@internationalized/date": "^3.2.1", "@internationalized/number": "^3.1.0", - "@spectrum-web-components/base": "^0.34.0", - "@spectrum-web-components/reactive-controllers": "^0.34.0", - "@spectrum-web-components/textfield": "^0.34.0" + "@spectrum-web-components/base": "^0.35.0", + "@spectrum-web-components/reactive-controllers": "^0.35.0", + "@spectrum-web-components/textfield": "^0.35.0" }, "types": "./src/index.d.ts", "customElements": "custom-elements.json", diff --git a/yarn.lock b/yarn.lock index 1b00de26fd5..d82799562e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5638,84 +5638,9 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-9.0.8.tgz#6af3bcdace903b8461f5fcd4c9aa23e70128a456" integrity sha512-rGfd7jqXOdR69bEjrRP58ynuIeJU0czPfwQvzhtCzg7jKVukV+efNHqrs086sC6xutB3W4TF71K/dZMr3oyTyg== -"@spectrum-web-components/base@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/base/-/base-0.34.0.tgz#d341665a23e6431fd589727241cf2287f102f220" - integrity sha512-/2J7BT0lKkGeFv4SE5vmGxUtBE9NBo7oa8M1nYTgahm6OL/wN6O5/7qZIY+H2iCTy9719bO/BUa+7A7N5wiLFg== - dependencies: - lit "^2.5.0" - "@spectrum-web-components/eslint-plugin@file:./linters/eslint": version "0.35.0" -"@spectrum-web-components/help-text@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/help-text/-/help-text-0.34.0.tgz#9ef71d0a3e71f2d5560a338c74ecb57b583c29b0" - integrity sha512-VsFBgae+6dKDJcdf6UNMr3/aFPsyaUmeBubIV92kS52nHKTi1dKlYWtnAcvxFY6n3iH4iOn943FVkImuXboRkQ== - dependencies: - "@spectrum-web-components/base" "^0.34.0" - "@spectrum-web-components/icons-workflow" "^0.34.0" - -"@spectrum-web-components/icon@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/icon/-/icon-0.34.0.tgz#109c52b9a0c911051507ff467ee44339eb225158" - integrity sha512-gASDbootZDmXYiRaqd1jlUoXapCI0U/s1ZNqaakAWm+vGoZB6T3b5eV2fUO0aHP8/UCAt+F07T1jTPo/ygaQMA== - dependencies: - "@spectrum-web-components/base" "^0.34.0" - "@spectrum-web-components/iconset" "^0.34.0" - -"@spectrum-web-components/icons-ui@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/icons-ui/-/icons-ui-0.34.0.tgz#ab61568145759e2e0d6cc42dcc176f6d3ec8a959" - integrity sha512-zom55CdjSxpff1f5VVbvqFy3KN7PIXw2M+3RjogKF1Yl5TRt5OUGl6cMBT+vmsBJ9ZOs5g4l0POepJ2Zdom/JQ== - dependencies: - "@spectrum-web-components/base" "^0.34.0" - "@spectrum-web-components/icon" "^0.34.0" - "@spectrum-web-components/iconset" "^0.34.0" - -"@spectrum-web-components/icons-workflow@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/icons-workflow/-/icons-workflow-0.34.0.tgz#b575a32b4c8cc01958dbc21290167454ae34f613" - integrity sha512-Vsa9lzzgC86+NMrW/qYCnAHUw5vk0+FfXpFcCCc3lPYVJQD4ByCQYZ2U+uF/n85o1A88xeuMjaZsGIMHxDMPbA== - dependencies: - "@spectrum-web-components/base" "^0.34.0" - "@spectrum-web-components/icon" "^0.34.0" - -"@spectrum-web-components/iconset@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/iconset/-/iconset-0.34.0.tgz#e2b88cc19c702ec6de1b7047c91128cce1256fc2" - integrity sha512-/pvVyBcJnhrfiIAjnXxwVGMrls6/8uvsSFCt0WFAFaiw5p5/njcuDJRNMo65EctiCQs1ApksA6E6s3rXj7S8TQ== - dependencies: - "@spectrum-web-components/base" "^0.34.0" - -"@spectrum-web-components/reactive-controllers@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/reactive-controllers/-/reactive-controllers-0.34.0.tgz#692663db46bbcb9db85ebdf8a8b4c97905e1119b" - integrity sha512-hAv6gbYhFfLY5f6nRL0YT5qjXLG8IzFx9LbNQVzYpVd7afLa0WZN0EuomgGQ+i0t35VTMnstypNtN3q4FUuV2w== - dependencies: - lit "^2.5.0" - -"@spectrum-web-components/shared@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/shared/-/shared-0.34.0.tgz#60605fd85c71f5e3c6dece2b388c3dbb59b2bab1" - integrity sha512-FGCLiOCJC2/lKdcBh6TcYfxOHF2nKca2RaCDZ2jRJ1fKRx9B48F80lSnSxlwtK7N1XZ8svvKGwJEFWNNvBMH2w== - dependencies: - "@lit-labs/observers" "^2.0.0" - "@spectrum-web-components/base" "^0.34.0" - focus-visible "^5.1.0" - -"@spectrum-web-components/textfield@^0.34.0": - version "0.34.0" - resolved "https://registry.yarnpkg.com/@spectrum-web-components/textfield/-/textfield-0.34.0.tgz#ec3c64dd3a703b20ccff9180c035298e637e96f7" - integrity sha512-RPODpW7zmpSa4otI40Pn7EW5z/TLVkQOZT3eF6LPnk+ffdOmGJvutL5JB9R8U15jLUFNSt05E6XFqnNxPSRllQ== - dependencies: - "@spectrum-web-components/base" "^0.34.0" - "@spectrum-web-components/help-text" "^0.34.0" - "@spectrum-web-components/icon" "^0.34.0" - "@spectrum-web-components/icons-ui" "^0.34.0" - "@spectrum-web-components/icons-workflow" "^0.34.0" - "@spectrum-web-components/shared" "^0.34.0" - "@storybook/csf-tools@^6.4.9": version "6.4.19" resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.19.tgz#28bdea11da17501a8bc4e761b821d7721880eaf6" From 52b91a5c159206c410b06ad9329ca01e55dc3ad1 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 15 Aug 2023 14:47:05 +0100 Subject: [PATCH 29/37] refactor: use `import type` when possible --- packages/input-segments/src/InputSegments.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 9b693bbf060..86003da5fb6 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -50,11 +50,13 @@ import { minHourAM, minHourPM, PM, + timeSegmentTypes, +} from './types.js'; +import type { Segment, SegmentDetails, SegmentValueAndLimits, TimeGranularity, - timeSegmentTypes, } from './types.js'; import styles from './input-segments.css.js'; From 6987ce695522bbd33d3117f51eb4d3585f4afe4a Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 15 Aug 2023 14:49:29 +0100 Subject: [PATCH 30/37] refactor: remove unnecessary configuration --- packages/input-segments/src/InputSegments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 86003da5fb6..c16cfe9bd11 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -102,7 +102,7 @@ export class InputSegments extends TextfieldBase { /** * Defines whether a date/time should be displayed in the field */ - @property({ reflect: true, attribute: false }) + @property({ attribute: false }) selectedDateTime?: Date; @state() From 1255d428cac6ac535d3ddd5a855e955398dd1a7c Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Tue, 15 Aug 2023 15:40:12 +0100 Subject: [PATCH 31/37] refactor: remove unnecessary code --- packages/input-segments/src/InputSegments.ts | 125 +++++++++---------- 1 file changed, 57 insertions(+), 68 deletions(-) diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index c16cfe9bd11..5165a2a9470 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -23,7 +23,7 @@ import { NumberParser } from '@internationalized/number'; import { CSSResultArray, html, - PropertyValueMap, + PropertyValues, TemplateResult, } from '@spectrum-web-components/base'; import { @@ -39,7 +39,10 @@ import { styleMap, when, } from '@spectrum-web-components/base/src/directives.js'; -import { LanguageResolutionController } from '@spectrum-web-components/reactive-controllers/src/LanguageResolution.js'; +import { + LanguageResolutionController, + languageResolverUpdatedSymbol, +} from '@spectrum-web-components/reactive-controllers/src/LanguageResolution.js'; import { TextfieldBase } from '@spectrum-web-components/textfield'; import { @@ -81,17 +84,13 @@ export class InputSegments extends TextfieldBase { return [...super.styles, styles]; } - /** - * Indicates when date segments should be included in the field - */ - @state() - protected includeDate = false; + protected languageResolver = new LanguageResolutionController(this); + protected timeZone = getLocalTimeZone(); + protected formatter!: DateFormatter; + protected numberParser!: NumberParser; - /** - * Indicates when time segments should be included in the field - */ - @state() - protected includeTime = false; + @query('.editable-segment') + firstEditableSegment!: HTMLDivElement; /** * Indicates which segments that are part of time should be used @@ -105,104 +104,103 @@ export class InputSegments extends TextfieldBase { @property({ attribute: false }) selectedDateTime?: Date; + /** + * Indicates when date segments should be included in the field + */ @state() - private previousLocale?: string; + protected includeDate = false; + /** + * Indicates when time segments should be included in the field + */ @state() - private currentDateTime!: CalendarDateTime; + protected includeTime = false; @state() - private newDateTime?: CalendarDateTime; + protected currentDateTime = toCalendarDateTime(now(this.timeZone)); @state() - private segments: Segment[] = []; + protected newDateTime?: CalendarDateTime; @state() - private createSegments = true; + protected segments: Segment[] = []; - @query('.editable-segment') - firstEditableSegment!: HTMLDivElement; + /** + * The `TextfieldBase` class requires this getter to return an element of type `HTMLInputElement` or + * `HTMLTextAreaElement`, but since the segments are DIVs with the `contenteditable` attribute, we need to cast as + * an input only to be able to use autofocus. + * + * Note that `focusElement` is only used for that, so converting as an input will have no side effect as all + * functions and attributes used exist in both types, `HTMLInputElement` and `HTMLDivElement`. + */ + public override get focusElement(): HTMLInputElement { + return this.firstEditableSegment as HTMLInputElement; + } - private languageResolver = new LanguageResolutionController(this); - private timeZone = getLocalTimeZone(); - private formatter!: DateFormatter; - private numberParser!: NumberParser; + public get is12HourClock(): boolean { + return Boolean(this.formatter.resolvedOptions().hour12); + } - private get locale(): string { + protected get locale(): string { return this.languageResolver.language; } - private get daySegment(): Segment | undefined { + protected get daySegment(): Segment | undefined { return this.segments.find((segment) => segment.type === 'day'); } - private get monthSegment(): Segment | undefined { + protected get monthSegment(): Segment | undefined { return this.segments.find((segment) => segment.type === 'month'); } - private get yearSegment(): Segment | undefined { + protected get yearSegment(): Segment | undefined { return this.segments.find((segment) => segment.type === 'year'); } - private get hourSegment(): Segment | undefined { + protected get hourSegment(): Segment | undefined { return this.segments.find((segment) => segment.type === 'hour'); } - private get minuteSegment(): Segment | undefined { + protected get minuteSegment(): Segment | undefined { return this.segments.find((segment) => segment.type === 'minute'); } - private get secondSegment(): Segment | undefined { + protected get secondSegment(): Segment | undefined { return this.segments.find((segment) => segment.type === 'second'); } - private get dayPeriodSegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'dayPeriod'); - } - - private get is12HourClock(): boolean { - return Boolean(this.formatter.resolvedOptions().hour12); - } - /** - * The `TextfieldBase` class requires this getter to return an element of type `HTMLInputElement` or - * `HTMLTextAreaElement`, but since the segments are DIVs with the `contenteditable` attribute, we need to cast as - * an input only to be able to use autofocus. - * - * Note that `focusElement` is only used for that, so converting as an input will have no side effect as all - * functions and attributes used exist in both types, `HTMLInputElement` and `HTMLDivElement`. + * "Day period" is what indicates whether the hour is AM or PM for 12-hour clocks */ - public override get focusElement(): HTMLInputElement { - return this.firstEditableSegment as HTMLInputElement; + protected get dayPeriodSegment(): Segment | undefined { + return this.segments.find((segment) => segment.type === 'dayPeriod'); } - constructor() { - super(); - this.setInitialDateTime(); - } + protected override willUpdate(changedProperties: PropertyValues): void { + /** + * Segments should be created only when some properties are changed, so we control when this should happen and + * not every time the `willUpdate()` method is executed + */ + let createSegments = false; - protected override willUpdate( - changedProperties: PropertyValueMap - ): void { - if (this.locale !== this.previousLocale) { - this.previousLocale = this.locale; - this.createSegments = true; + if (changedProperties.has(languageResolverUpdatedSymbol)) { + createSegments = true; this.setFormatter(); this.setNumberParser(); } if (changedProperties.has('selectedDateTime')) { - this.createSegments = true; + createSegments = true; this.setCurrentDateTime(); } if (changedProperties.has('timeGranularity')) { - this.createSegments = true; + createSegments = true; } - if (this.createSegments) { + if (createSegments) { this.setSegments(); } } @@ -462,13 +460,6 @@ export class InputSegments extends TextfieldBase { }); } - /** - * Defines the initial date and time that will be used to render the input, if no specific date and time is provided - */ - private setInitialDateTime(): void { - this.currentDateTime = toCalendarDateTime(now(this.timeZone)); - } - /** * If a datetime is received by the component via property, it will use it as the current datetime to render the * input @@ -593,8 +584,6 @@ export class InputSegments extends TextfieldBase { .formatToParts(dateTime) .map((part) => this.mapToTimeSegment(part)) .filter((part) => segmentTypes.includes(part.type)); - - this.createSegments = false; } /** From 9a65a6a226f2dafd9a8df8002766dc61f1efc359 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Wed, 16 Aug 2023 13:02:47 +0100 Subject: [PATCH 32/37] refactor: code and comments improvements --- packages/input-segments/README.md | 3 + packages/input-segments/src/InputSegments.ts | 171 +++++++++---------- packages/input-segments/src/types.ts | 7 + 3 files changed, 95 insertions(+), 86 deletions(-) diff --git a/packages/input-segments/README.md b/packages/input-segments/README.md index d8ec940c3ab..fd4177d5364 100644 --- a/packages/input-segments/README.md +++ b/packages/input-segments/README.md @@ -23,5 +23,8 @@ export class MyInput extends InputSegments { - Include ARIA attributes for editable segments - Use `@input`/`@beforeinput` events to handle data input/content cleanup +- Move `handleKeydown()` call to a cache so that it doesn't cycle on the binding in each render pass +- Rename `handleKeydown()` to match the new events used +- Define/revise list of locales and include them in the VRT process to ensure long-term delivery - Complete documentation - Add/Review unit tests diff --git a/packages/input-segments/src/InputSegments.ts b/packages/input-segments/src/InputSegments.ts index 5165a2a9470..59567a9b63f 100644 --- a/packages/input-segments/src/InputSegments.ts +++ b/packages/input-segments/src/InputSegments.ts @@ -56,6 +56,7 @@ import { timeSegmentTypes, } from './types.js'; import type { + EditableSegmentType, Segment, SegmentDetails, SegmentValueAndLimits, @@ -146,34 +147,31 @@ export class InputSegments extends TextfieldBase { } protected get daySegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'day'); + return this.segment('day'); } protected get monthSegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'month'); + return this.segment('month'); } protected get yearSegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'year'); + return this.segment('year'); } protected get hourSegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'hour'); + return this.segment('hour'); } protected get minuteSegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'minute'); + return this.segment('minute'); } protected get secondSegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'second'); + return this.segment('second'); } - /** - * "Day period" is what indicates whether the hour is AM or PM for 12-hour clocks - */ - protected get dayPeriodSegment(): Segment | undefined { - return this.segments.find((segment) => segment.type === 'dayPeriod'); + protected get amPmSegment(): Segment | undefined { + return this.segment('dayPeriod'); } protected override willUpdate(changedProperties: PropertyValues): void { @@ -230,17 +228,16 @@ export class InputSegments extends TextfieldBase { public renderLiteralSegment(segment: Segment): TemplateResult { /** - * We use `.innerText` here to preserve the text exactly as it is, including all spaces. If we use string - * interpolation, the value can be changed because of the code format, in which the contents of the tags are - * moved to a separate line, and this causes problems for the contents of some literal segments + * We need this flag below to prevent Prettier from moving the content of the tag to the next line, which causes + * problems on rendering because of spaces added before and after the content (indentation) */ + // prettier-ignore return html` + >${segment.formatted ?? ''} `; } @@ -259,7 +256,12 @@ export class InputSegments extends TextfieldBase { : undefined, }; - // TODO: Include ARIA attributes for editable segments + /** + * TODO: Include ARIA attributes for editable segments + * TODO: Use `@input`/`@beforeinput` events to handle data input/content cleanup + * TODO: Move `handleKeydown()` call to a cache so that it doesn't cycle on the binding in each render pass + * TODO: Rename `handleKeydown()` to match the new events used + */ return html`
segment.type === type); + } + /** * Defines the formatter that will be used in the creation of segments */ @@ -582,8 +592,8 @@ export class InputSegments extends TextfieldBase { this.segments = this.formatter .formatToParts(dateTime) - .map((part) => this.mapToTimeSegment(part)) - .filter((part) => segmentTypes.includes(part.type)); + .filter((part) => segmentTypes.includes(part.type)) + .map((part) => this.mapToSegment(part)); } /** @@ -592,21 +602,24 @@ export class InputSegments extends TextfieldBase { * * @param part - Part/segment to be "translated" (mapped) */ - private mapToTimeSegment(part: Intl.DateTimeFormatPart): Segment { - const { value, minValue, maxValue } = this.getSegmentValueAndLimits( - part.type - ); + private mapToSegment(part: Intl.DateTimeFormatPart): Segment { + const type = part.type; + const formatted = part.value; + const placeholder = this.getPlaceholder(type, part.value); + const { value, minValue, maxValue } = this.getValueAndLimits(type); const segment: Segment = { - type: part.type, - placeholder: this.getPlaceholder(part.type, part.value), - formatted: part.value, - value, - minValue, - maxValue, + type, + formatted, + ...(placeholder !== undefined && { placeholder }), + ...(value !== undefined && { value }), + ...(minValue !== undefined && { minValue }), + ...(maxValue !== undefined && { maxValue }), }; - this.formatValues(segment); + if (part.type !== 'literal') { + this.formatValue(segment); + } return segment; } @@ -616,7 +629,7 @@ export class InputSegments extends TextfieldBase { * * @param segment - Segment to be updated */ - private formatValues(segment: Segment): void { + private formatValue(segment: Segment): void { if (segment.value === undefined) { return; } @@ -699,8 +712,10 @@ export class InputSegments extends TextfieldBase { private getPlaceholder( type: Intl.DateTimeFormatPartTypes, value: string - ): string { + ): string | undefined { switch (type) { + case 'literal': + return undefined; case 'dayPeriod': return value; case 'year': @@ -781,47 +796,31 @@ export class InputSegments extends TextfieldBase { private getCurrentValue( type: Intl.DateTimeFormatPartTypes ): number | undefined { + let previousValue: number | undefined; + let currentValue: number; + switch (type) { case 'year': - return this.usePreviousOrCurrentValue( - this.yearSegment?.value, - this.currentDateTime.year - ); case 'month': - return this.usePreviousOrCurrentValue( - this.monthSegment?.value, - this.currentDateTime.month - ); case 'day': - return this.usePreviousOrCurrentValue( - this.daySegment?.value, - this.currentDateTime.day - ); case 'hour': - return this.usePreviousOrCurrentValue( - this.hourSegment?.value, - this.currentDateTime.hour - ); case 'minute': - return this.usePreviousOrCurrentValue( - this.minuteSegment?.value, - this.currentDateTime.minute - ); case 'second': - return this.usePreviousOrCurrentValue( - this.secondSegment?.value, - this.currentDateTime.second - ); + previousValue = this.segment(type)?.value; + currentValue = this.currentDateTime[type]; + break; case 'dayPeriod': // To identify the current value of "AM/PM", we use the value of the hour, not the day period itself - return this.usePreviousOrCurrentValue( + previousValue = this.hourSegment?.value && - this.getAmPmModifier(this.hourSegment.value), - this.getAmPmModifier(this.currentDateTime.hour) - ); + this.getAmPmModifier(this.hourSegment.value); + currentValue = this.getAmPmModifier(this.currentDateTime.hour); + break; default: return undefined; } + + return this.usePreviousOrCurrentValue(previousValue, currentValue); } /** @@ -831,7 +830,7 @@ export class InputSegments extends TextfieldBase { * * @param type - Type of segment */ - private getSegmentValueAndLimits( + private getValueAndLimits( type: Intl.DateTimeFormatPartTypes ): SegmentValueAndLimits { const value = this.getCurrentValue(type); @@ -916,7 +915,7 @@ export class InputSegments extends TextfieldBase { } else if (segment.type === 'dayPeriod') { segment.value = this.toggleDayPeriod(segment.value); } else { - segment.value++; + segment.value += 1; if (segment.value > max) { segment.value = min; @@ -945,7 +944,7 @@ export class InputSegments extends TextfieldBase { } else if (segment.type === 'dayPeriod') { segment.value = this.toggleDayPeriod(segment.value); } else { - segment.value--; + segment.value -= 1; if (segment.value < min) { segment.value = max; @@ -969,17 +968,17 @@ export class InputSegments extends TextfieldBase { * match the new period (AM or PM). In addition, the minimum and maximum values of the hour are also changed */ private updateHour(): void { - if (!this.hourSegment || !this.dayPeriodSegment) { + if (!this.hourSegment || !this.amPmSegment) { this.resetHourAndDayPeriod(); return; } - if (this.dayPeriodSegment.value === undefined) { + if (this.amPmSegment.value === undefined) { return; } - const isAM = this.dayPeriodSegment.value === AM; - const isPM = this.dayPeriodSegment.value === PM; + const isAM = this.amPmSegment.value === AM; + const isPM = this.amPmSegment.value === PM; this.hourSegment.minValue = isPM ? minHourPM : minHourAM; this.hourSegment.maxValue = isPM ? maxHourPM : maxHourAM; @@ -1000,21 +999,20 @@ export class InputSegments extends TextfieldBase { * their initial values */ private resetHourAndDayPeriod(): void { - if (this.dayPeriodSegment) { - const dayPeriod = this.getSegmentValueAndLimits('dayPeriod'); + if (this.amPmSegment) { + const dayPeriod = this.getValueAndLimits('dayPeriod'); - this.dayPeriodSegment.value = dayPeriod.value; - this.dayPeriodSegment.minValue = dayPeriod.minValue; - this.dayPeriodSegment.maxValue = dayPeriod.maxValue; + this.amPmSegment.value = dayPeriod.value; + this.amPmSegment.minValue = dayPeriod.minValue; + this.amPmSegment.maxValue = dayPeriod.maxValue; - if (this.dayPeriodSegment.value === undefined) { - this.dayPeriodSegment.formatted = - this.dayPeriodSegment.placeholder; + if (this.amPmSegment.value === undefined) { + this.amPmSegment.formatted = this.amPmSegment.placeholder; } } if (this.hourSegment) { - const hour = this.getSegmentValueAndLimits('hour'); + const hour = this.getValueAndLimits('hour'); this.hourSegment.minValue = hour.minValue; this.hourSegment.maxValue = hour.maxValue; @@ -1030,10 +1028,8 @@ export class InputSegments extends TextfieldBase { } /** - * It validates if the day is valid for the given month and, if it is above the maximum limit, it changes the day to - * correspond to the last day of that month. In addition, updates the maximum limit of day segment - * - * @param updateDayMaxValue - Indicates when the maximum allowed value for the day should be changed + * Updates the value of the day segment to match the last day of the month if it is above the maximum limit and + * furthermore also updates the maximum limit of the day segment to use the new found limit */ private updateDay(): void { if ( @@ -1058,7 +1054,7 @@ export class InputSegments extends TextfieldBase { this.daySegment.value > this.daySegment.maxValue ) { this.daySegment.value = this.daySegment.maxValue; - this.formatValues(this.daySegment); + this.formatValue(this.daySegment); } } @@ -1084,7 +1080,7 @@ export class InputSegments extends TextfieldBase { this.updateDay(); } - this.formatValues(segment); + this.formatValue(segment); this.setNewDateTime(); this.requestUpdate(); @@ -1093,6 +1089,7 @@ export class InputSegments extends TextfieldBase { new CustomEvent('change', { bubbles: true, composed: true, + cancelable: true, detail: this.newDateTime.toDate(this.timeZone), }) ); @@ -1124,7 +1121,9 @@ export class InputSegments extends TextfieldBase { * changes to include the value the user has just entered. Also, we need to identify if it is the hour segment and * if the clock format is 12 hours, if so, we have to adjust the previous value to perform some calculations * - * @param segment - The segment being changed + * @param details - Details of the segment being changed + * @param typedValue - The value typed by the user + * @param isAmPmHour - Indicates whether it is the hour segment for 12-hour clocks */ private mergePreviousValueWithTypedValue( details: SegmentDetails, @@ -1268,8 +1267,8 @@ export class InputSegments extends TextfieldBase { } if (siblingSegment.getAttribute('contenteditable')) { - segmentFound = true; siblingSegment.focus(); + segmentFound = true; } currentSegment = siblingSegment; diff --git a/packages/input-segments/src/types.ts b/packages/input-segments/src/types.ts index ddd75dfa9fb..0567b241f2b 100644 --- a/packages/input-segments/src/types.ts +++ b/packages/input-segments/src/types.ts @@ -24,6 +24,13 @@ export const timeSegmentTypes: Intl.DateTimeFormatPartTypes[] = [ 'literal', ]; +type EditableSegmentTypes = Pick< + Intl.DateTimeFormatPartTypesRegistry, + 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'dayPeriod' +>; + +export type EditableSegmentType = keyof EditableSegmentTypes; + export interface Segment extends Omit { /** A placeholder string for the segment */ placeholder?: string; From 72fbfff15b7ec4a899950c0c7b04220d59eb288f Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Wed, 16 Aug 2023 15:30:11 +0100 Subject: [PATCH 33/37] refactor: remove unused files --- .../test/benchmark/basic-test.ts | 18 --------- .../test/input-segments.test.ts | 38 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 packages/input-segments/test/benchmark/basic-test.ts delete mode 100644 packages/input-segments/test/input-segments.test.ts diff --git a/packages/input-segments/test/benchmark/basic-test.ts b/packages/input-segments/test/benchmark/basic-test.ts deleted file mode 100644 index 6ca464b0d49..00000000000 --- a/packages/input-segments/test/benchmark/basic-test.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2023 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -import '@spectrum-web-components/input-segments/sp-input-segments.js'; -import { html } from '@spectrum-web-components/base'; -import { measureFixtureCreation } from '../../../../test/benchmark/helpers.js'; - -measureFixtureCreation(html` - -`); diff --git a/packages/input-segments/test/input-segments.test.ts b/packages/input-segments/test/input-segments.test.ts deleted file mode 100644 index 18860bbcd5e..00000000000 --- a/packages/input-segments/test/input-segments.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2023 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; - -import '../sp-input-segments.js'; -import { InputSegments } from '..'; -import { testForLitDevWarnings } from '../../../test/testing-helpers.js'; - -describe('InputSegments', () => { - testForLitDevWarnings( - async () => - await fixture( - html` - - ` - ) - ); - it('loads default input-segments accessibly', async () => { - const el = await fixture( - html` - - ` - ); - - await elementUpdated(el); - - await expect(el).to.be.accessible(); - }); -}); From 7afa9511927bec874e3436c87c1a22751366a0d5 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Wed, 16 Aug 2023 16:23:57 +0100 Subject: [PATCH 34/37] refactor: update `@spectrum-css/calendar` --- packages/calendar/package.json | 2 +- packages/calendar/src/spectrum-calendar.css | 832 ++++++++++---------- yarn.lock | 8 +- 3 files changed, 405 insertions(+), 437 deletions(-) diff --git a/packages/calendar/package.json b/packages/calendar/package.json index a35c1c20aec..bfc86e2ea11 100644 --- a/packages/calendar/package.json +++ b/packages/calendar/package.json @@ -69,7 +69,7 @@ "@spectrum-web-components/reactive-controllers": "^0.35.0" }, "devDependencies": { - "@spectrum-css/calendar": "^3.2.6" + "@spectrum-css/calendar": "^4.0.3" }, "types": "./src/index.d.ts", "customElements": "custom-elements.json", diff --git a/packages/calendar/src/spectrum-calendar.css b/packages/calendar/src/spectrum-calendar.css index eb03f0451f4..844309d8c6a 100644 --- a/packages/calendar/src/spectrum-calendar.css +++ b/packages/calendar/src/spectrum-calendar.css @@ -12,50 +12,85 @@ governing permissions and limitations under the License. /* THIS FILE IS MACHINE GENERATED. DO NOT EDIT */ :host { + --spectrum-calendar-day-width: var(--spectrum-component-height-100); + --spectrum-calendar-day-height: var(--spectrum-component-height-100); + --spectrum-calendar-button-gap: var( + --spectrum-component-edge-to-visual-only-50 + ); --spectrum-calendar-border-radius-reset: 0; --spectrum-calendar-border-width-reset: 0; + --spectrum-calendar-day-border-size: var(--spectrum-border-width-200); --spectrum-calendar-margin-y: 24px; --spectrum-calendar-margin-x: 32px; + --spectrum-calendar-day-padding: 4px; --spectrum-calendar-width: calc( ( - var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ) + - var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ) * 2 + var(--spectrum-calendar-day-width) + + var(--spectrum-calendar-day-padding) * 2 ) * 7 ); - --spectrum-calendar-button-gap: var(--spectrum-global-dimension-size-40); - --spectrum-calendar-title-text-letter-spacing: var( - --spectrum-detail-m-text-letter-spacing, - var(--spectrum-global-font-letter-spacing-medium) - ); + --spectrum-calendar-title-text-letter-spacing: 0.06em; + --spectrum-calendar-title-height: 32px; + --spectrum-calendar-title-text-size: var(--spectrum-font-size-300); + --spectrum-calendar-day-title-text-font-weight: var( + --spectrum-bold-font-weight + ); + --spectrum-calendar-day-title-text-color: var(--spectrum-gray-700); + --spectrum-calendar-day-title-text-size: var(--spectrum-font-size-50); + --spectrum-calendar-day-text-size-han: var(--spectrum-font-size-50); + --spectrum-calendar-day-text-size: var(--spectrum-font-size-100); + --spectrum-calendar-day-text-color-selected: var(--spectrum-gray-900); + --spectrum-calendar-day-text-color-hover: var(--spectrum-gray-900); + --spectrum-calendar-day-text-color-cap-selected: var(--spectrum-gray-900); + --spectrum-calendar-day-text-font-weight-selected: var( + --spectrum-bold-font-weight + ); + --spectrum-calendar-day-text-font-weight-cap-selected: var( + --spectrum-bold-font-weight + ); + --spectrum-calendar-day-today-text-color: var(--spectrum-gray-800); + --spectrum-calendar-day-today-text-font-weight: var( + --spectrum-bold-font-weight + ); + --spectrum-calendar-day-today-border-color: var(--spectrum-gray-800); + --spectrum-calendar-button-icon-color: var(--spectrum-gray-700); + --spectrum-calendar-day-today-text-color-disabled: var(--spectrum-gray-500); + --spectrum-calendar-day-today-border-color-disabled: var( + --spectrum-gray-400 + ); + --spectrum-calendar-day-text-color-disabled: var(--spectrum-gray-500); + --spectrum-calendar-day-text-color-key-focus: var(--spectrum-gray-900); } :host { display: inline-block; - width: var(--spectrum-calendar-width, 250px); + inline-size: var(--mod-calendar-width, var(--spectrum-calendar-width)); } :host([padded]) { - margin: var(--spectrum-calendar-margin-x) var(--spectrum-calendar-margin-y); + margin: var(--mod-calendar-margin-x, var(--spectrum-calendar-margin-x)) + var(--mod-calendar-margin-y, var(--spectrum-calendar-margin-y)); } .spectrum-Calendar-header { align-items: center; display: flex; - width: 100%; + inline-size: 100%; } .spectrum-Calendar-title { + color: var( + --highcontrast-calendar-day-title-text-color, + var( + --mod-calendar-day-title-text-color, + var(--spectrum-calendar-day-title-text-color) + ) + ); flex-grow: 1; font-size: var( - --spectrum-calendar-title-text-size, - var(--spectrum-global-dimension-font-size-300) + --mod-calendar-title-text-size, + var(--spectrum-calendar-title-text-size) ); font-weight: 700; line-height: var( - --spectrum-calendar-title-height, - var(--spectrum-global-dimension-static-size-400) + --mod-calendar-title-height, + var(--spectrum-calendar-title-height) ); margin: 0; order: 1; @@ -64,16 +99,20 @@ governing permissions and limitations under the License. text-overflow: ellipsis; white-space: nowrap; } -:host([dir='ltr']) .spectrum-Calendar-nextMonth, -:host([dir='ltr']) .spectrum-Calendar-prevMonth, -:host([dir='rtl']) .spectrum-Calendar-nextMonth, -:host([dir='rtl']) .spectrum-Calendar-prevMonth { - margin: 0 var(--spectrum-calendar-button-gap); -} :host([dir='rtl']) .spectrum-Calendar-nextMonth, :host([dir='rtl']) .spectrum-Calendar-prevMonth { transform: matrix(-1, 0, 0, 1, 0, 0); } +.spectrum-Calendar-nextMonth, +.spectrum-Calendar-prevMonth { + color: var( + --highcontrast-calendar-button-icon-color, + var( + --mod-calendar-button-icon-color, + var(--spectrum-calendar-button-icon-color) + ) + ); +} .spectrum-Calendar-prevMonth { order: 0; } @@ -81,31 +120,41 @@ governing permissions and limitations under the License. order: 2; } .spectrum-Calendar-dayOfWeek { - border-bottom: none !important; + block-size: 100%; + border-block-end: none !important; + color: var( + --highcontrast-calendar-day-title-text-color, + var( + --mod-calendar-day-title-text-color, + var(--spectrum-calendar-day-title-text-color) + ) + ); cursor: default; display: flex; flex-direction: column; font-size: var( - --spectrum-calendar-day-title-text-size, - var(--spectrum-global-dimension-font-size-50) + --mod-calendar-day-title-text-size, + var(--spectrum-calendar-day-title-text-size) ); font-weight: var( - --spectrum-calendar-day-title-text-font-weight, - var(--spectrum-alias-detail-text-font-weight-regular) + --mod-calendar-day-title-text-font-weight, + var(--spectrum-calendar-day-title-text-font-weight) + ); + inline-size: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); - height: 100%; justify-content: flex-end; -webkit-text-decoration: none !important; text-decoration: none !important; text-transform: uppercase; - width: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); } :host([title]) .spectrum-Calendar-dayOfWeek { - border-bottom: none; - letter-spacing: var(--spectrum-calendar-title-text-letter-spacing); + border-block-end: none; + letter-spacing: var( + --mod-calendar-title-text-letter-spacing, + var(--spectrum-calendar-title-text-letter-spacing) + ); text-decoration: underline; -webkit-text-decoration: underline dotted; text-decoration: underline dotted; @@ -121,81 +170,73 @@ governing permissions and limitations under the License. user-select: none; } .spectrum-Calendar-tableCell { + block-size: var( + --mod-calendar-day-height, + var(--spectrum-calendar-day-height) + ); box-sizing: content-box; - height: var( - --spectrum-calendar-day-height, - var(--spectrum-global-dimension-size-400) + inline-size: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); padding: 0; padding: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) ); position: relative; text-align: center; - width: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); } .spectrum-Calendar-tableCell:focus { outline: 0; } -:host([dir='ltr']) .spectrum-Calendar-date { - left: 0; -} -:host([dir='rtl']) .spectrum-Calendar-date { - right: 0; -} .spectrum-Calendar-date { + block-size: var( + --mod-calendar-day-height, + var(--spectrum-calendar-day-height) + ); border: var( - --spectrum-calendar-day-border-size, - var(--spectrum-alias-border-size-thick) + --mod-calendar-day-border-size, + var(--spectrum-calendar-day-border-size) ) solid transparent; border-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); box-sizing: border-box; cursor: pointer; display: block; font-size: var( - --spectrum-calendar-day-text-size, - var(--spectrum-alias-font-size-default) + --mod-calendar-day-text-size, + var(--spectrum-calendar-day-text-size) ); - height: var( - --spectrum-calendar-day-height, - var(--spectrum-global-dimension-size-400) + inline-size: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); + inset-block-start: 0; + inset-inline-start: 0; line-height: calc( - var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ) - + var(--mod-calendar-day-width, var(--spectrum-calendar-day-width)) - var( - --spectrum-calendar-day-border-size, - var(--spectrum-alias-border-size-thick) + --mod-calendar-day-border-size, + var(--spectrum-calendar-day-border-size) ) * 2 ); margin: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) ); position: absolute; - top: 0; white-space: nowrap; - width: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); } .spectrum-Calendar-date:lang(ja), .spectrum-Calendar-date:lang(ko), .spectrum-Calendar-date:lang(zh) { font-size: var( - --spectrum-calendar-day-text-size-han, - var(--spectrum-global-dimension-font-size-50) + --mod-calendar-day-text-size-han, + var(--spectrum-calendar-day-text-size-han) ); } .spectrum-Calendar-date.is-disabled { @@ -205,57 +246,40 @@ governing permissions and limitations under the License. .spectrum-Calendar-date.is-outsideMonth { visibility: hidden; } -:host([dir='ltr']) .spectrum-Calendar-date:before { - left: calc( - 50% - - var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ) / 2 - ); -} -:host([dir='rtl']) .spectrum-Calendar-date:before { - right: calc( - 50% - - var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ) / 2 - ); -} .spectrum-Calendar-date:before { + block-size: var( + --mod-calendar-day-height, + var(--spectrum-calendar-day-height) + ); border: var( - --spectrum-calendar-day-border-size, - var(--spectrum-alias-border-size-thick) + --mod-calendar-day-border-size, + var(--spectrum-calendar-day-border-size) ) solid transparent; border-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); box-sizing: border-box; content: ''; - height: var( - --spectrum-calendar-day-height, - var(--spectrum-global-dimension-size-400) + inline-size: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); - position: absolute; - top: calc( - 50% - - var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ) / 2 + inset-block-start: calc( + 50% - var(--mod-calendar-day-width, var(--spectrum-calendar-day-width)) / + 2 ); - width: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) + inset-inline-start: calc( + 50% - var(--mod-calendar-day-width, var(--spectrum-calendar-day-width)) / + 2 ); + position: absolute; } .spectrum-Calendar-date.is-selected:not(.is-range-selection) { font-weight: var( - --spectrum-calendar-day-text-font-weight-selected, - var(--spectrum-global-font-weight-bold) + --mod-calendar-day-text-font-weight-selected, + var(--spectrum-calendar-day-text-font-weight-selected) ); } .spectrum-Calendar-date.is-selected:not(.is-range-selection):before { @@ -263,476 +287,423 @@ governing permissions and limitations under the License. } .spectrum-Calendar-date.is-today { font-weight: var( - --spectrum-calendar-day-today-text-font-weight, - var(--spectrum-global-font-weight-bold) + --mod-calendar-day-today-text-font-weight, + var(--spectrum-calendar-day-today-text-font-weight) ); } .spectrum-Calendar-date.is-range-selection { - border-radius: var(--spectrum-calendar-border-radius-reset); - border-width: var(--spectrum-calendar-border-width-reset); + border-radius: var( + --mod-calendar-border-radius-reset, + var(--spectrum-calendar-border-radius-reset) + ); + border-width: var( + --mod-calendar-border-width-reset, + var(--spectrum-calendar-border-width-reset) + ); + inline-size: calc( + var(--mod-calendar-day-width, var(--spectrum-calendar-day-width)) + + var( + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) + ) * 2 + ); line-height: var( - --spectrum-calendar-day-height, - var(--spectrum-global-dimension-size-400) + --mod-calendar-day-height, + var(--spectrum-calendar-day-height) ); margin: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) ) 0; - width: calc( - var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ) + - var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ) * 2 - ); } .spectrum-Calendar-date.is-range-selection.is-range-end, .spectrum-Calendar-date.is-range-selection.is-range-start, .spectrum-Calendar-date.is-range-selection.is-selection-end, .spectrum-Calendar-date.is-range-selection.is-selection-start { - width: calc( - var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ) + + inline-size: calc( + var(--mod-calendar-day-width, var(--spectrum-calendar-day-width)) + var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) ) ); } .spectrum-Calendar-date.is-range-selection.is-selection-end, .spectrum-Calendar-date.is-range-selection.is-selection-start { font-weight: var( - --spectrum-calendar-day-text-font-weight-cap-selected, - var(--spectrum-global-font-weight-bold) + --mod-calendar-day-text-font-weight-cap-selected, + var(--spectrum-calendar-day-text-font-weight-cap-selected) ); } .spectrum-Calendar-date.is-range-selection.is-selection-end:after, .spectrum-Calendar-date.is-range-selection.is-selection-start:after { + block-size: var( + --mod-calendar-day-height, + var(--spectrum-calendar-day-height) + ); border-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); content: ''; display: block; - height: var( - --spectrum-calendar-day-height, - var(--spectrum-global-dimension-size-400) + inline-size: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); + inset-block-start: 0; position: absolute; - top: 0; - width: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - padding-right: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - padding-left: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - margin-left: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - margin-right: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - border-top-left-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); } -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - border-top-right-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - border-bottom-left-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); -} -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-start, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-start { - border-bottom-right-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); -} -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-range-start:after, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-range-start:before, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-start:after, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-start:before { - left: 0; -} -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-range-start:after, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-range-start:before, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-start:after, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-start:before { - right: 0; -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - padding-left: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - padding-right: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - margin-right: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - margin-left: var( - --spectrum-calendar-day-padding, - var(--spectrum-global-dimension-static-size-50) - ); -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - border-top-right-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); -} -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - border-top-left-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); -} -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='ltr']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - border-bottom-right-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) - ); -} -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-range-end, -:host([dir='rtl']) .spectrum-Calendar-date.is-range-selection.is-selection-end { - border-bottom-left-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) +.spectrum-Calendar-date.is-range-selection.is-range-start, +.spectrum-Calendar-date.is-range-selection.is-selection-start { + border-end-start-radius: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) + ); + border-start-start-radius: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) + ); + margin-inline-start: var( + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) + ); + padding-inline-end: var( + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) ); } -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-range-end:after, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-range-end:before, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:after, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:before { - left: auto; -} -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-range-end:after, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-range-end:before, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:after, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:before { - right: auto; -} -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-range-end:after, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-range-end:before, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:after, -:host([dir='ltr']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:before { - right: 0; -} -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-range-end:after, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-range-end:before, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:after, -:host([dir='rtl']) - .spectrum-Calendar-date.is-range-selection.is-selection-end:before { - left: 0; -} +.spectrum-Calendar-date.is-range-selection.is-range-start:after, +.spectrum-Calendar-date.is-range-selection.is-range-start:before, +.spectrum-Calendar-date.is-range-selection.is-selection-start:after, +.spectrum-Calendar-date.is-range-selection.is-selection-start:before { + inset-inline-start: 0; +} +.spectrum-Calendar-date.is-range-selection.is-range-end, +.spectrum-Calendar-date.is-range-selection.is-selection-end { + border-end-end-radius: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) + ); + border-start-end-radius: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) + ); + margin-inline-end: var( + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) + ); + padding-inline-start: var( + --mod-calendar-day-padding, + var(--spectrum-calendar-day-padding) + ); +} +.spectrum-Calendar-date.is-range-selection.is-range-end:after, +.spectrum-Calendar-date.is-range-selection.is-range-end:before, +.spectrum-Calendar-date.is-range-selection.is-selection-end:after, +.spectrum-Calendar-date.is-range-selection.is-selection-end:before { + inset-inline: auto 0; +} .spectrum-Calendar-date.is-range-selection.is-range-start.is-range-end, .spectrum-Calendar-date.is-range-selection.is-selection-end.is-range-start, .spectrum-Calendar-date.is-range-selection.is-selection-start.is-range-end, .spectrum-Calendar-date.is-range-selection.is-selection-start.is-selection-end { border-radius: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); - width: var( - --spectrum-calendar-day-width, - var(--spectrum-global-dimension-size-400) + inline-size: var( + --mod-calendar-day-width, + var(--spectrum-calendar-day-width) ); } -.spectrum-Calendar-nextMonth, -.spectrum-Calendar-prevMonth { - color: var( - --spectrum-calendar-button-icon-color, - var(--spectrum-global-color-gray-700) - ); -} -.spectrum-Calendar-dayOfWeek { +.spectrum-Calendar-date { color: var( - --spectrum-calendar-day-title-text-color, - var(--spectrum-global-color-gray-700) + --highcontrast-calendar-day-title-text-color, + var( + --mod-calendar-day-title-text-color, + var(--spectrum-calendar-day-title-text-color) + ) ); } .spectrum-Calendar-date:hover { color: var( - --spectrum-calendar-day-text-color-hover, - var(--spectrum-alias-text-color-hover) + --highcontrast-calendar-day-text-color-hover, + var( + --mod-calendar-day-text-color-hover, + var(--spectrum-calendar-day-text-color-hover) + ) ); } .spectrum-Calendar-date:hover:not(.is-selection-end):not(.is-selection-start):before { background: var( - --spectrum-calendar-day-background-color-hover, - var(--spectrum-alias-highlight-hover) - ); -} -.spectrum-Calendar-date:hover.is-selected { - color: var( - --spectrum-calendar-day-text-color-selected-hover, - var(--spectrum-alias-text-color-hover) + --highcontrast-calendar-day-background-color-hover, + var( + --mod-calendar-day-background-color-hover, + var(--spectrum-calendar-day-background-color-hover) + ) ); } .spectrum-Calendar-date:hover.is-selected:not(.is-selection-end):not(.is-selection-start):before { background: var( - --spectrum-calendar-day-background-color-selected-hover, - var(--spectrum-alias-highlight-selected-hover) + --highcontrast-calendar-day-background-color-selected-hover, + var( + --mod-calendar-day-background-color-selected-hover, + var(--spectrum-calendar-day-background-color-selected-hover) + ) ); } .spectrum-Calendar-date:hover.is-range-selection:before { background: var( - --spectrum-calendar-day-background-color-selected-hover, - var(--spectrum-alias-highlight-selected-hover) + --highcontrast-calendar-day-background-color-selected-hover, + var( + --mod-calendar-day-background-color-selected-hover, + var(--spectrum-calendar-day-background-color-selected-hover) + ) ); } .spectrum-Calendar-date:active { background-color: var( - --spectrum-calendar-day-background-color-down, - var(--spectrum-alias-highlight-down) + --highcontrast-calendar-day-background-color-down, + var( + --mod-calendar-day-background-color-down, + var(--spectrum-calendar-day-background-color-down) + ) ); } .spectrum-Calendar-date.is-selected { background: var( - --spectrum-calendar-day-background-color-selected, - var(--spectrum-alias-highlight-selected) + --highcontrast-calendar-day-background-color-selected, + var( + --mod-calendar-day-background-color-selected, + var(--spectrum-calendar-day-background-color-selected) + ) ); color: var( - --spectrum-calendar-day-text-color-selected, - var(--spectrum-alias-text-color-hover) + --highcontrast-calendar-day-text-color-selected, + var( + --mod-calendar-day-text-color-selected, + var(--spectrum-calendar-day-text-color-selected) + ) ); } .spectrum-Calendar-date.is-selected:not(.is-range-selection) { background: var( - --spectrum-calendar-day-background-color-cap-selected, - var(--spectrum-alias-highlight-selected-hover) + --highcontrast-calendar-day-background-color-cap-selected, + var( + --mod-calendar-day-background-color-cap-selected, + var(--spectrum-calendar-day-background-color-cap-selected) + ) ); } .spectrum-Calendar-date.is-today { border-color: var( - --spectrum-calendar-day-today-border-color, - var(--spectrum-global-color-gray-800) + --highcontrast-calendar-day-today-border-color, + var( + --mod-calendar-day-today-border-color, + var(--spectrum-calendar-day-today-border-color) + ) ); color: var( - --spectrum-calendar-day-today-text-color, - var(--spectrum-alias-text-color) + --highcontrast-calendar-day-today-text-color, + var( + --mod-calendar-day-today-text-color, + var(--spectrum-calendar-day-today-text-color) + ) ); } .spectrum-Calendar-date.is-today:before { border-color: var( - --spectrum-calendar-day-today-border-color, - var(--spectrum-global-color-gray-800) + --highcontrast-calendar-day-today-border-color, + var( + --mod-calendar-day-today-border-color, + var(--spectrum-calendar-day-today-border-color) + ) ); } .spectrum-Calendar-date.is-today:hover.is-selected:not(.is-range-selection):before { background: var( - --spectrum-calendar-day-today-background-color-selected-hover, - var(--spectrum-alias-highlight-selected-hover) + --highcontrast-calendar-day-today-background-color-selected-hover, + var( + --mod-calendar-day-today-background-color-selected-hover, + var(--spectrum-calendar-day-today-background-color-selected-hover) + ) ); } .spectrum-Calendar-date.is-today.is-disabled { border-color: var( - --spectrum-calendar-day-today-border-color-disabled, - var(--spectrum-global-color-gray-400) + --highcontrast-calendar-day-today-border-color-disabled, + var( + --mod-calendar-day-today-border-color-disabled, + var(--spectrum-calendar-day-today-border-color-disabled) + ) ); color: var( - --spectrum-calendar-day-today-text-color-disabled, - var(--spectrum-alias-text-color-disabled) + --highcontrast-calendar-day-today-text-color-disabled, + var( + --mod-calendar-day-today-text-color-disabled, + var(--spectrum-calendar-day-today-text-color-disabled) + ) ); } .spectrum-Calendar-date.is-today.is-disabled:before { border-color: var( - --spectrum-calendar-day-today-border-color-disabled, - var(--spectrum-global-color-gray-400) + --highcontrast-calendar-day-today-border-color-disabled, + var( + --mod-calendar-day-today-border-color-disabled, + var(--spectrum-calendar-day-today-border-color-disabled) + ) ); } .spectrum-Calendar-date.is-focused:not(.is-range-selection) { background: var( - --spectrum-calendar-day-background-color-key-focus, - var(--spectrum-alias-highlight-hover) + --highcontrast-calendar-day-background-color-key-focus, + var( + --mod-calendar-day-background-color-key-focus, + var(--spectrum-calendar-day-background-color-key-focus) + ) ); border-color: var( - --spectrum-calendar-day-border-color-key-focus, - var(--spectrum-alias-focus-color) + --highcontrast-calendar-day-border-color-key-focus, + var( + --mod-calendar-day-border-color-key-focus, + var(--spectrum-calendar-day-border-color-key-focus) + ) ); color: var( - --spectrum-calendar-day-text-color-key-focus, - var(--spectrum-alias-text-color-hover) + --highcontrast-calendar-day-text-color-key-focus, + var( + --mod-calendar-day-text-color-key-focus, + var(--spectrum-calendar-day-text-color-key-focus) + ) ); } .spectrum-Calendar-date.is-focused:not(.is-range-selection).is-today { border-color: var( - --spectrum-calendar-day-border-color-key-focus, - var(--spectrum-alias-focus-color) + --highcontrast-calendar-day-border-color-key-focus, + var( + --mod-calendar-day-border-color-key-focus, + var(--spectrum-calendar-day-border-color-key-focus) + ) ); } .spectrum-Calendar-date.is-focused:not(.is-range-selection).is-selected, .spectrum-Calendar-date.is-focused:not(.is-range-selection):active { background: var( - --spectrum-calendar-day-background-color-cap-selected, - var(--spectrum-alias-highlight-selected-hover) + --highcontrast-calendar-day-background-color-cap-selected, + var( + --mod-calendar-day-background-color-cap-selected, + var(--spectrum-calendar-day-background-color-cap-selected) + ) ); border-color: var( - --spectrum-calendar-day-border-color-key-focus, - var(--spectrum-alias-focus-color) + --highcontrast-calendar-day-border-color-key-focus, + var( + --mod-calendar-day-border-color-key-focus, + var(--spectrum-calendar-day-border-color-key-focus) + ) ); color: var( - --spectrum-calendar-day-text-color-selected, - var(--spectrum-alias-text-color-hover) + --highcontrast-calendar-day-text-color-selected, + var( + --mod-calendar-day-text-color-selected, + var(--spectrum-calendar-day-text-color-selected) + ) ); } .spectrum-Calendar-date.is-focused.is-selected:before { background: var( - --spectrum-calendar-day-background-color-selected-hover, - var(--spectrum-alias-highlight-selected-hover) + --highcontrast-calendar-day-background-color-selected-hover, + var( + --mod-calendar-day-background-color-selected-hover, + var(--spectrum-calendar-day-background-color-selected-hover) + ) ); } .spectrum-Calendar-date.is-focused.is-range-selection:before { background: var( - --spectrum-calendar-day-background-color-selected-hover, - var(--spectrum-alias-highlight-selected-hover) + --highcontrast-calendar-day-background-color-selected-hover, + var( + --mod-calendar-day-background-color-selected-hover, + var(--spectrum-calendar-day-background-color-selected-hover) + ) ); } .spectrum-Calendar-date.is-focused:before { border-color: var( - --spectrum-calendar-day-border-color-key-focus, - var(--spectrum-alias-focus-color) + --highcontrast-calendar-day-border-color-key-focus, + var( + --mod-calendar-day-border-color-key-focus, + var(--spectrum-calendar-day-border-color-key-focus) + ) ); } .spectrum-Calendar-date.is-disabled { color: var( - --spectrum-calendar-day-text-color-disabled, - var(--spectrum-alias-text-color-disabled) + --highcontrast-calendar-day-text-color-disabled, + var( + --mod-calendar-day-text-color-disabled, + var(--spectrum-calendar-day-text-color-disabled) + ) ); } .spectrum-Calendar-date.is-selection-end, .spectrum-Calendar-date.is-selection-start { color: var( - --spectrum-calendar-day-text-color-cap-selected, - var(--spectrum-alias-text-color-hover) + --highcontrast-calendar-day-text-color-cap-selected, + var( + --mod-calendar-day-text-color-cap-selected, + var(--spectrum-calendar-day-text-color-cap-selected) + ) ); } .spectrum-Calendar-date.is-selection-end:after, .spectrum-Calendar-date.is-selection-start:after { background-color: var( - --spectrum-calendar-day-background-color-selected, - var(--spectrum-alias-highlight-selected) + --highcontrast-calendar-day-background-color-selected, + var( + --mod-calendar-day-background-color-selected, + var(--spectrum-calendar-day-background-color-selected) + ) ); } .spectrum-Calendar-date.is-selection-end.is-disabled, .spectrum-Calendar-date.is-selection-start.is-disabled { color: var( - --spectrum-calendar-day-text-color-disabled, - var(--spectrum-alias-text-color-disabled) + --highcontrast-calendar-day-text-color-disabled, + var( + --mod-calendar-day-text-color-disabled, + var(--spectrum-calendar-day-text-color-disabled) + ) ); } @media (forced-colors: active) { .spectrum-Calendar-date { - --spectrum-calendar-button-icon-color: ButtonText; - --spectrum-calendar-day-background-color-cap-selected: Highlight; - --spectrum-calendar-day-background-color-down: ButtonFace; - --spectrum-calendar-day-background-color-hover: transparent; - --spectrum-calendar-day-background-color-key-focus: ButtonFace; - --spectrum-calendar-day-background-color-selected-hover: transparent; - --spectrum-calendar-day-background-color-selected: Highlight; - --spectrum-calendar-day-border-color-key-focus: ButtonText; - --spectrum-calendar-day-text-color-cap-selected: HighlightText; - --spectrum-calendar-day-text-color-disabled: GrayText; - --spectrum-calendar-day-text-color-hover: ButtonText; - --spectrum-calendar-day-text-color-key-focus: ButtonText; - --spectrum-calendar-day-text-color-selected-hover: HighlightText; - --spectrum-calendar-day-text-color-selected: HighlightText; - --spectrum-calendar-day-title-text-color: CanvasText; - --spectrum-calendar-day-today-background-color-selected-hover: Highlight; - --spectrum-calendar-day-today-border-color-disabled: GrayText; - --spectrum-calendar-day-today-border-color: ButtonText; - --spectrum-calendar-day-today-text-color-disabled: GrayText; - --spectrum-calendar-day-today-text-color: ButtonText; + --highcontrast-calendar-button-icon-color: ButtonText; + --highcontrast-calendar-day-background-color-cap-selected: Highlight; + --highcontrast-calendar-day-background-color-down: ButtonFace; + --highcontrast-calendar-day-background-color-hover: transparent; + --highcontrast-calendar-day-background-color-key-focus: ButtonFace; + --highcontrast-calendar-day-background-color-selected-hover: transparent; + --highcontrast-calendar-day-background-color-selected: Highlight; + --highcontrast-calendar-day-border-color-key-focus: ButtonText; + --highcontrast-calendar-day-text-color-cap-selected: HighlightText; + --highcontrast-calendar-day-text-color-disabled: GrayText; + --highcontrast-calendar-day-text-color-hover: ButtonText; + --highcontrast-calendar-day-text-color-key-focus: ButtonText; + --highcontrast-calendar-day-text-color-selected-hover: HighlightText; + --highcontrast-calendar-day-text-color-selected: HighlightText; + --highcontrast-calendar-day-title-text-color: CanvasText; + --highcontrast-calendar-day-today-background-color-selected-hover: Highlight; + --highcontrast-calendar-day-today-border-color-disabled: GrayText; + --highcontrast-calendar-day-today-border-color: ButtonText; + --highcontrast-calendar-day-today-text-color-disabled: GrayText; + --highcontrast-calendar-day-today-text-color: ButtonText; color: CanvasText; forced-color-adjust: none; } - .spectrum-Calendar-date.is-range-selection { + .spectrum-Calendar-date.is-range-selection.is-today { color: HighlightText; } .spectrum-Calendar-date.is-range-selection.is-selection-end:after, @@ -744,7 +715,4 @@ governing permissions and limitations under the License. background: Highlight; color: HighlightText; } - .spectrum-Calendar-date:hover.is-today { - color: ButtonText; - } } diff --git a/yarn.lock b/yarn.lock index d82799562e1..c9b12dc100f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5368,10 +5368,10 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/buttongroup/-/buttongroup-6.0.61.tgz#7c62d511905a0b544b5cc02ab506fddd071a7b57" integrity sha512-N5d0REziOHWL4pBHFaWzxdnJgBjCSAy4IVkJCIaoQXXqBCSisjfzgepUSiPbS8yekn78InUwKNUyvS866R0MqQ== -"@spectrum-css/calendar@^3.2.6": - version "3.2.6" - resolved "https://registry.yarnpkg.com/@spectrum-css/calendar/-/calendar-3.2.6.tgz#edd700b5b6188a67711087c9604c7dda9ac8920a" - integrity sha512-bB5CXl6B4zpJizEXmRGvv+WJHImXi2D4VaRL0Yi7EBmswYfi0eEjN6CiKljoqe+MBc5nHPp4pimI08Al08q2MA== +"@spectrum-css/calendar@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@spectrum-css/calendar/-/calendar-4.0.3.tgz#3519942dd92d2c427f782bd1d174c42ec761185e" + integrity sha512-nwBNY8HxuC0ukFV9Wptdta3T06MOXgjSBKI46TCTgPdM9K4zf2OybgUV8bpV/dACeM7GUJmN1YeNOPpaLbYK+A== "@spectrum-css/card@^6.0.9": version "6.0.9" From de22124d10d0c0dcab92f012edf3a4d701522887 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Wed, 16 Aug 2023 17:13:08 +0100 Subject: [PATCH 35/37] refactor: move `input-segments` from `/packages` to `/tools` --- packages/time-field/tsconfig.json | 5 +---- tools/bundle/tsconfig.json | 1 - {packages => tools}/input-segments/.npmignore | 0 {packages => tools}/input-segments/README.md | 0 {packages => tools}/input-segments/exports.json | 0 {packages => tools}/input-segments/package.json | 4 ++-- {packages => tools}/input-segments/src/InputSegments.ts | 0 {packages => tools}/input-segments/src/index.ts | 0 {packages => tools}/input-segments/src/input-segments.css | 0 {packages => tools}/input-segments/src/types.ts | 0 {packages => tools}/input-segments/tsconfig.json | 6 +++--- tsconfig-all.json | 2 +- 12 files changed, 7 insertions(+), 11 deletions(-) rename {packages => tools}/input-segments/.npmignore (100%) rename {packages => tools}/input-segments/README.md (100%) rename {packages => tools}/input-segments/exports.json (100%) rename {packages => tools}/input-segments/package.json (96%) rename {packages => tools}/input-segments/src/InputSegments.ts (100%) rename {packages => tools}/input-segments/src/index.ts (100%) rename {packages => tools}/input-segments/src/input-segments.css (100%) rename {packages => tools}/input-segments/src/types.ts (100%) rename {packages => tools}/input-segments/tsconfig.json (64%) diff --git a/packages/time-field/tsconfig.json b/packages/time-field/tsconfig.json index 7732f273ad0..2326f7e4c30 100644 --- a/packages/time-field/tsconfig.json +++ b/packages/time-field/tsconfig.json @@ -6,8 +6,5 @@ }, "include": ["*.ts", "src/*.ts"], "exclude": ["test/*.ts", "stories/*.ts"], - "references": [ - { "path": "../../tools/base" }, - { "path": "../input-segments" } - ] + "references": [{ "path": "../../tools/input-segments" }] } diff --git a/tools/bundle/tsconfig.json b/tools/bundle/tsconfig.json index 56f9013871c..fa49e45ed11 100644 --- a/tools/bundle/tsconfig.json +++ b/tools/bundle/tsconfig.json @@ -39,7 +39,6 @@ { "path": "../../packages/icons-workflow" }, { "path": "../../packages/iconset" }, { "path": "../../packages/illustrated-message" }, - { "path": "../../packages/input-segments" }, { "path": "../../packages/link" }, { "path": "../../packages/menu" }, { "path": "../../packages/meter" }, diff --git a/packages/input-segments/.npmignore b/tools/input-segments/.npmignore similarity index 100% rename from packages/input-segments/.npmignore rename to tools/input-segments/.npmignore diff --git a/packages/input-segments/README.md b/tools/input-segments/README.md similarity index 100% rename from packages/input-segments/README.md rename to tools/input-segments/README.md diff --git a/packages/input-segments/exports.json b/tools/input-segments/exports.json similarity index 100% rename from packages/input-segments/exports.json rename to tools/input-segments/exports.json diff --git a/packages/input-segments/package.json b/tools/input-segments/package.json similarity index 96% rename from packages/input-segments/package.json rename to tools/input-segments/package.json index 594b608945e..e7642a9ef53 100644 --- a/packages/input-segments/package.json +++ b/tools/input-segments/package.json @@ -9,10 +9,10 @@ "repository": { "type": "git", "url": "https://github.com/adobe/spectrum-web-components.git", - "directory": "packages/input-segments" + "directory": "tools/input-segments" }, "author": "", - "homepage": "https://adobe.github.io/spectrum-web-components/components/input-segments", + "homepage": "https://adobe.github.io/spectrum-web-components/tools/input-segments", "bugs": { "url": "https://github.com/adobe/spectrum-web-components/issues" }, diff --git a/packages/input-segments/src/InputSegments.ts b/tools/input-segments/src/InputSegments.ts similarity index 100% rename from packages/input-segments/src/InputSegments.ts rename to tools/input-segments/src/InputSegments.ts diff --git a/packages/input-segments/src/index.ts b/tools/input-segments/src/index.ts similarity index 100% rename from packages/input-segments/src/index.ts rename to tools/input-segments/src/index.ts diff --git a/packages/input-segments/src/input-segments.css b/tools/input-segments/src/input-segments.css similarity index 100% rename from packages/input-segments/src/input-segments.css rename to tools/input-segments/src/input-segments.css diff --git a/packages/input-segments/src/types.ts b/tools/input-segments/src/types.ts similarity index 100% rename from packages/input-segments/src/types.ts rename to tools/input-segments/src/types.ts diff --git a/packages/input-segments/tsconfig.json b/tools/input-segments/tsconfig.json similarity index 64% rename from packages/input-segments/tsconfig.json rename to tools/input-segments/tsconfig.json index 73f3c3581e0..8da83568428 100644 --- a/packages/input-segments/tsconfig.json +++ b/tools/input-segments/tsconfig.json @@ -7,8 +7,8 @@ "include": ["*.ts", "src/*.ts"], "exclude": ["test/*.ts", "stories/*.ts"], "references": [ - { "path": "../../tools/base" }, - { "path": "../../tools/reactive-controllers" }, - { "path": "../textfield" } + { "path": "../base" }, + { "path": "../reactive-controllers" }, + { "path": "../../packages/textfield" } ] } diff --git a/tsconfig-all.json b/tsconfig-all.json index 00f52c2c9ed..95574583a42 100644 --- a/tsconfig-all.json +++ b/tsconfig-all.json @@ -50,7 +50,6 @@ { "path": "packages/icons-workflow" }, { "path": "packages/iconset" }, { "path": "packages/illustrated-message" }, - { "path": "packages/input-segments" }, { "path": "packages/link" }, { "path": "packages/menu" }, { "path": "packages/meter" }, @@ -86,6 +85,7 @@ { "path": "tools/base" }, { "path": "tools/bundle" }, { "path": "tools/grid" }, + { "path": "tools/input-segments" }, { "path": "tools/reactive-controllers" }, { "path": "tools/shared" }, { "path": "tools/styles" }, From 6b7496029f6ef0bb67b4ba0f4a85146a169ba963 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Wed, 16 Aug 2023 17:25:13 +0100 Subject: [PATCH 36/37] fix: segment text colour --- tools/input-segments/src/input-segments.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/input-segments/src/input-segments.css b/tools/input-segments/src/input-segments.css index 24ad3ceb824..4cf53ceca1d 100644 --- a/tools/input-segments/src/input-segments.css +++ b/tools/input-segments/src/input-segments.css @@ -60,7 +60,7 @@ governing permissions and limitations under the License. } .editable-segment.is-placeholder, -.editable-segment.is-placeholder ~ .literal-segment { +.editable-segment.is-placeholder + .literal-segment { color: var(--spectrum-gray-500); } @@ -71,7 +71,7 @@ governing permissions and limitations under the License. .editable-segment:focus, .editable-segment:focus .placeholder { - color: var(--spectrum-textfield-text-color-default); + color: var(--spectrum-white); background-color: var(--spectrum-accent-background-color-default); } From 8eabd8e2eba3cb227b2fb4f115ce87a0c5ccf336 Mon Sep 17 00:00:00 2001 From: Paulo de Tarso Furtado Machado Date: Wed, 16 Aug 2023 18:00:39 +0100 Subject: [PATCH 37/37] refactor(calendar): remove unnecessary code --- packages/calendar/src/Calendar.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/calendar/src/Calendar.ts b/packages/calendar/src/Calendar.ts index 53da531a31a..61f18ca4cab 100644 --- a/packages/calendar/src/Calendar.ts +++ b/packages/calendar/src/Calendar.ts @@ -23,7 +23,7 @@ import { NumberFormatter } from '@internationalized/number'; import { CSSResultArray, html, - PropertyValueMap, + PropertyValues, SpectrumElement, TemplateResult, } from '@spectrum-web-components/base'; @@ -121,9 +121,7 @@ export class Calendar extends SpectrumElement { this.setInitialCalendarDate(); } - protected override willUpdate( - changedProperties: PropertyValueMap - ): void { + protected override willUpdate(changedProperties: PropertyValues): void { if (changedProperties.has('selectedDate')) { this.setCurrentCalendarDate(); } @@ -339,7 +337,6 @@ export class Calendar extends SpectrumElement { public handleDayClick(calendarDate: CalendarDate): void { this.selectedDate = calendarDate.toDate(this.timeZone); - this.setCurrentCalendarDate(); this.dispatchEvent( new CustomEvent('change', {