From dbc6c81ed087fa3ce05c4f2f4eaefc045ef05e9b Mon Sep 17 00:00:00 2001 From: JC Franco Date: Mon, 24 Jun 2024 20:21:31 -0700 Subject: [PATCH] feat(input-time-zone): add `offsetStyle` prop (#9426) **Related Issue:** #8716 ## Summary Adds `offsetStyle` prop to specify whether offset should always use `UTC` or `GMT` for the offset label instead of using the user's locale. Valid options are: * `"user"` (default) uses `UTC` or `GMT` depending on the user's locale, * `"gmt"` always uses `GMT`, and * `"utc"` always uses `UTC`. --- .../calcite-components/src/components.d.ts | 112 +++++++++++++++++- .../input-time-zone/input-time-zone.e2e.ts | 76 +++++++++++- .../input-time-zone/input-time-zone.tsx | 16 ++- .../input-time-zone/interfaces.d.ts | 2 + .../src/components/input-time-zone/utils.ts | 15 ++- 5 files changed, 215 insertions(+), 6 deletions(-) diff --git a/packages/calcite-components/src/components.d.ts b/packages/calcite-components/src/components.d.ts index 6bb9356e520..79c07aeb020 100644 --- a/packages/calcite-components/src/components.d.ts +++ b/packages/calcite-components/src/components.d.ts @@ -8,86 +8,184 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { Alignment, Appearance, CollapseDirection, FlipContext, IconType, Kind, Layout, LogicalFlowPosition, Position, Scale, SelectionAppearance as SelectionAppearance1, SelectionMode, Status, Width } from "./components/interfaces"; import { RequestedItem } from "./components/accordion/interfaces"; import { RequestedItem as RequestedItem1 } from "./components/accordion-item/interfaces"; +import { ActionMessages } from "./components/action/assets/action/t9n"; import { FlipPlacement, LogicalPlacement, MenuPlacement, OverlayPositioning, ReferenceElement } from "./utils/floating-ui"; +import { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n"; import { Columns } from "./components/action-group/interfaces"; +import { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n"; +import { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n"; import { AlertDuration, Sync } from "./components/alert/interfaces"; import { NumberingSystem } from "./utils/locale"; +import { AlertMessages } from "./components/alert/assets/alert/t9n"; import { HeadingLevel } from "./components/functional/Heading"; +import { BlockMessages } from "./components/block/assets/block/t9n"; import { BlockSectionToggleDisplay } from "./components/block-section/interfaces"; +import { BlockSectionMessages } from "./components/block-section/assets/block-section/t9n"; import { ButtonAlignment, DropdownIconType } from "./components/button/interfaces"; +import { ButtonMessages } from "./components/button/assets/button/t9n"; +import { CardMessages } from "./components/card/assets/card/t9n"; import { ArrowType, AutoplayType } from "./components/carousel/interfaces"; +import { CarouselMessages } from "./components/carousel/assets/carousel/t9n"; import { MutableValidityState } from "./utils/form"; +import { ChipMessages } from "./components/chip/assets/chip/t9n"; import { ColorValue, InternalColor } from "./components/color-picker/interfaces"; import { Format } from "./components/color-picker/utils"; +import { ColorPickerMessages } from "./components/color-picker/assets/color-picker/t9n"; import { ComboboxChildElement, SelectionDisplay } from "./components/combobox/interfaces"; +import { ComboboxMessages } from "./components/combobox/assets/combobox/t9n"; +import { DatePickerMessages } from "./components/date-picker/assets/date-picker/t9n"; import { DateLocaleData } from "./components/date-picker/utils"; import { HoverRange } from "./utils/date"; import { RequestedItem as RequestedItem2 } from "./components/dropdown-group/interfaces"; import { ItemKeyboardEvent } from "./components/dropdown/interfaces"; +import { FilterMessages } from "./components/filter/assets/filter/t9n"; import { FlowItemLikeElement } from "./components/flow/interfaces"; +import { FlowItemMessages } from "./components/flow-item/assets/flow-item/t9n"; import { ColorStop, DataSeries } from "./components/graph/interfaces"; +import { HandleMessages } from "./components/handle/assets/handle/t9n"; import { HandleChange, HandleNudge } from "./components/handle/interfaces"; +import { InlineEditableMessages } from "./components/inline-editable/assets/inline-editable/t9n"; import { InputPlacement } from "./components/input/interfaces"; -import { TimeZoneMode } from "./components/input-time-zone/interfaces"; +import { InputMessages } from "./components/input/assets/input/t9n"; +import { InputDatePickerMessages } from "./components/input-date-picker/assets/input-date-picker/t9n"; +import { InputNumberMessages } from "./components/input-number/assets/input-number/t9n"; +import { InputTextMessages } from "./components/input-text/assets/input-text/t9n"; +import { InputTimePickerMessages } from "./components/input-time-picker/assets/input-time-picker/t9n"; +import { TimePickerMessages } from "./components/time-picker/assets/time-picker/t9n"; +import { InputTimeZoneMessages } from "./components/input-time-zone/assets/input-time-zone/t9n"; +import { OffsetStyle, TimeZoneMode } from "./components/input-time-zone/interfaces"; import { ListDragDetail } from "./components/list/interfaces"; import { ItemData } from "./components/list-item/interfaces"; +import { ListMessages } from "./components/list/assets/list/t9n"; import { SelectionAppearance } from "./components/list/resources"; +import { ListItemMessages } from "./components/list-item/assets/list-item/t9n"; +import { MenuMessages } from "./components/menu/assets/menu/t9n"; +import { MenuItemMessages } from "./components/menu-item/assets/menu-item/t9n"; import { MenuItemCustomEvent } from "./components/menu-item/interfaces"; import { MeterFillType, MeterLabelType } from "./components/meter/interfaces"; +import { ModalMessages } from "./components/modal/assets/modal/t9n"; +import { NoticeMessages } from "./components/notice/assets/notice/t9n"; +import { PaginationMessages } from "./components/pagination/assets/pagination/t9n"; +import { PanelMessages } from "./components/panel/assets/panel/t9n"; import { ItemData as ItemData1, ListFocusId } from "./components/pick-list/shared-list-logic"; import { ICON_TYPES } from "./components/pick-list/resources"; +import { PickListItemMessages } from "./components/pick-list-item/assets/pick-list-item/t9n"; +import { PopoverMessages } from "./components/popover/assets/popover/t9n"; +import { RatingMessages } from "./components/rating/assets/rating/t9n"; +import { ScrimMessages } from "./components/scrim/assets/scrim/t9n"; import { DisplayMode } from "./components/sheet/interfaces"; import { DisplayMode as DisplayMode1 } from "./components/shell-panel/interfaces"; +import { ShellPanelMessages } from "./components/shell-panel/assets/shell-panel/t9n"; import { DragDetail } from "./utils/sortableComponent"; import { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEventDetail, StepperLayout } from "./components/stepper/interfaces"; +import { StepperMessages } from "./components/stepper/assets/stepper/t9n"; +import { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n"; import { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces"; +import { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n"; import { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces"; +import { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n"; import { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent, TableSelectionDisplay } from "./components/table/interfaces"; +import { TableMessages } from "./components/table/assets/table/t9n"; +import { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n"; +import { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n"; +import { TextAreaMessages } from "./components/text-area/assets/text-area/t9n"; import { TileSelectType } from "./components/tile-select/interfaces"; import { TileSelectGroupLayout } from "./components/tile-select-group/interfaces"; +import { TipMessages } from "./components/tip/assets/tip/t9n"; +import { TipManagerMessages } from "./components/tip-manager/assets/tip-manager/t9n"; import { TreeItemSelectDetail } from "./components/tree-item/interfaces"; +import { ValueListMessages } from "./components/value-list/assets/value-list/t9n"; import { ListItemAndHandle } from "./components/value-list-item/interfaces"; export { Alignment, Appearance, CollapseDirection, FlipContext, IconType, Kind, Layout, LogicalFlowPosition, Position, Scale, SelectionAppearance as SelectionAppearance1, SelectionMode, Status, Width } from "./components/interfaces"; export { RequestedItem } from "./components/accordion/interfaces"; export { RequestedItem as RequestedItem1 } from "./components/accordion-item/interfaces"; +export { ActionMessages } from "./components/action/assets/action/t9n"; export { FlipPlacement, LogicalPlacement, MenuPlacement, OverlayPositioning, ReferenceElement } from "./utils/floating-ui"; +export { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n"; export { Columns } from "./components/action-group/interfaces"; +export { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n"; +export { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n"; export { AlertDuration, Sync } from "./components/alert/interfaces"; export { NumberingSystem } from "./utils/locale"; +export { AlertMessages } from "./components/alert/assets/alert/t9n"; export { HeadingLevel } from "./components/functional/Heading"; +export { BlockMessages } from "./components/block/assets/block/t9n"; export { BlockSectionToggleDisplay } from "./components/block-section/interfaces"; +export { BlockSectionMessages } from "./components/block-section/assets/block-section/t9n"; export { ButtonAlignment, DropdownIconType } from "./components/button/interfaces"; +export { ButtonMessages } from "./components/button/assets/button/t9n"; +export { CardMessages } from "./components/card/assets/card/t9n"; export { ArrowType, AutoplayType } from "./components/carousel/interfaces"; +export { CarouselMessages } from "./components/carousel/assets/carousel/t9n"; export { MutableValidityState } from "./utils/form"; +export { ChipMessages } from "./components/chip/assets/chip/t9n"; export { ColorValue, InternalColor } from "./components/color-picker/interfaces"; export { Format } from "./components/color-picker/utils"; +export { ColorPickerMessages } from "./components/color-picker/assets/color-picker/t9n"; export { ComboboxChildElement, SelectionDisplay } from "./components/combobox/interfaces"; +export { ComboboxMessages } from "./components/combobox/assets/combobox/t9n"; +export { DatePickerMessages } from "./components/date-picker/assets/date-picker/t9n"; export { DateLocaleData } from "./components/date-picker/utils"; export { HoverRange } from "./utils/date"; export { RequestedItem as RequestedItem2 } from "./components/dropdown-group/interfaces"; export { ItemKeyboardEvent } from "./components/dropdown/interfaces"; +export { FilterMessages } from "./components/filter/assets/filter/t9n"; export { FlowItemLikeElement } from "./components/flow/interfaces"; +export { FlowItemMessages } from "./components/flow-item/assets/flow-item/t9n"; export { ColorStop, DataSeries } from "./components/graph/interfaces"; +export { HandleMessages } from "./components/handle/assets/handle/t9n"; export { HandleChange, HandleNudge } from "./components/handle/interfaces"; +export { InlineEditableMessages } from "./components/inline-editable/assets/inline-editable/t9n"; export { InputPlacement } from "./components/input/interfaces"; -export { TimeZoneMode } from "./components/input-time-zone/interfaces"; +export { InputMessages } from "./components/input/assets/input/t9n"; +export { InputDatePickerMessages } from "./components/input-date-picker/assets/input-date-picker/t9n"; +export { InputNumberMessages } from "./components/input-number/assets/input-number/t9n"; +export { InputTextMessages } from "./components/input-text/assets/input-text/t9n"; +export { InputTimePickerMessages } from "./components/input-time-picker/assets/input-time-picker/t9n"; +export { TimePickerMessages } from "./components/time-picker/assets/time-picker/t9n"; +export { InputTimeZoneMessages } from "./components/input-time-zone/assets/input-time-zone/t9n"; +export { OffsetStyle, TimeZoneMode } from "./components/input-time-zone/interfaces"; export { ListDragDetail } from "./components/list/interfaces"; export { ItemData } from "./components/list-item/interfaces"; +export { ListMessages } from "./components/list/assets/list/t9n"; export { SelectionAppearance } from "./components/list/resources"; +export { ListItemMessages } from "./components/list-item/assets/list-item/t9n"; +export { MenuMessages } from "./components/menu/assets/menu/t9n"; +export { MenuItemMessages } from "./components/menu-item/assets/menu-item/t9n"; export { MenuItemCustomEvent } from "./components/menu-item/interfaces"; export { MeterFillType, MeterLabelType } from "./components/meter/interfaces"; +export { ModalMessages } from "./components/modal/assets/modal/t9n"; +export { NoticeMessages } from "./components/notice/assets/notice/t9n"; +export { PaginationMessages } from "./components/pagination/assets/pagination/t9n"; +export { PanelMessages } from "./components/panel/assets/panel/t9n"; export { ItemData as ItemData1, ListFocusId } from "./components/pick-list/shared-list-logic"; export { ICON_TYPES } from "./components/pick-list/resources"; +export { PickListItemMessages } from "./components/pick-list-item/assets/pick-list-item/t9n"; +export { PopoverMessages } from "./components/popover/assets/popover/t9n"; +export { RatingMessages } from "./components/rating/assets/rating/t9n"; +export { ScrimMessages } from "./components/scrim/assets/scrim/t9n"; export { DisplayMode } from "./components/sheet/interfaces"; export { DisplayMode as DisplayMode1 } from "./components/shell-panel/interfaces"; +export { ShellPanelMessages } from "./components/shell-panel/assets/shell-panel/t9n"; export { DragDetail } from "./utils/sortableComponent"; export { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEventDetail, StepperLayout } from "./components/stepper/interfaces"; +export { StepperMessages } from "./components/stepper/assets/stepper/t9n"; +export { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n"; export { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces"; +export { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n"; export { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces"; +export { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n"; export { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent, TableSelectionDisplay } from "./components/table/interfaces"; +export { TableMessages } from "./components/table/assets/table/t9n"; +export { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n"; +export { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n"; +export { TextAreaMessages } from "./components/text-area/assets/text-area/t9n"; export { TileSelectType } from "./components/tile-select/interfaces"; export { TileSelectGroupLayout } from "./components/tile-select-group/interfaces"; +export { TipMessages } from "./components/tip/assets/tip/t9n"; +export { TipManagerMessages } from "./components/tip-manager/assets/tip-manager/t9n"; export { TreeItemSelectDetail } from "./components/tree-item/interfaces"; +export { ValueListMessages } from "./components/value-list/assets/value-list/t9n"; export { ListItemAndHandle } from "./components/value-list-item/interfaces"; export namespace Components { interface CalciteAccordion { @@ -2754,6 +2852,11 @@ export namespace Components { * Specifies the name of the component. Required to pass the component's `value` on form submission. */ "name": string; + /** + * Specifies how the offset will be displayed, where `"user"` uses `UTC` or `GMT` depending on the user's locale, `"gmt"` always uses `GMT`, and `"utc"` always uses `UTC`. This only applies to the `offset` mode. + * @default "user" + */ + "offsetStyle": OffsetStyle; /** * When `true`, displays and positions the component. */ @@ -10636,6 +10739,11 @@ declare namespace LocalJSX { * Specifies the name of the component. Required to pass the component's `value` on form submission. */ "name"?: string; + /** + * Specifies how the offset will be displayed, where `"user"` uses `UTC` or `GMT` depending on the user's locale, `"gmt"` always uses `GMT`, and `"utc"` always uses `UTC`. This only applies to the `offset` mode. + * @default "user" + */ + "offsetStyle"?: OffsetStyle; /** * Fires when the component is requested to be closed and before the closing transition begins. */ diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts b/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts index c5ca280a48a..a1fba7c7d56 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts @@ -15,6 +15,14 @@ import { import { TagAndPage } from "../../tests/commonTests/interfaces"; import { toUserFriendlyName } from "./utils"; +/* + * **Notes** + * + * - tests need to have an emulated time zone + * - test time zones should preferably be unaffected by daylight savings time, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for more info + * - not all time zones are supported in Puppeteer's bundled Chromium, so we patch the Intl API with test-specific values/logic + */ + describe("calcite-input-time-zone", () => { type TestTimeZoneItem = { name: string; @@ -493,6 +501,67 @@ describe("calcite-input-time-zone", () => { currComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item"); expect(currComboboxItem).not.toBe(prevComboboxItem); }); + + describe("offsetStyle", () => { + const gmtTimeZoneLocale = "en-GB"; + const utcTimeZoneLocale = "fr"; + + let page: E2EPage; + + async function assertItemLabelMatches(page: E2EPage, offsetMarker: "GMT" | "UTC"): Promise { + // all items are formatted equally, so we only need to check the first one + const firstTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item"); + + expect(await firstTimeZoneItem.getProperty("textLabel")).toContain(offsetMarker); + } + + beforeEach(async () => { + page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + }); + + describe("displays UTC or GMT based on user's locale (default)", () => { + it("displays GMT for GMT-preferred locale", async () => { + await page.setContent( + addTimeZoneNamePolyfill( + html``, + ), + ); + + await assertItemLabelMatches(page, "GMT"); + }); + + it("displays UTC for UTC-preferred locale", async () => { + await page.setContent( + addTimeZoneNamePolyfill( + html``, + ), + ); + + await assertItemLabelMatches(page, "UTC"); + }); + }); + + it("supports GMT as a style", async () => { + await page.setContent( + addTimeZoneNamePolyfill( + html``, + ), + ); + + await assertItemLabelMatches(page, "GMT"); + }); + + it("supports UTC as a style", async () => { + await page.setContent( + addTimeZoneNamePolyfill( + html``, + ), + ); + + await assertItemLabelMatches(page, "UTC"); + }); + }); }); /** @@ -511,14 +580,17 @@ function addTimeZoneNamePolyfill(testHtml: string): string { delete options?.timeZoneName; super(locales, options); this.originalOptions = originalOptions; + this.originalLocales = locales; } formatToParts(date) { const originalParts = super.formatToParts(date); const timeZoneName = this.originalOptions.timeZoneName; + const locale = this.originalLocales; if (timeZoneName === "shortOffset") { const { timeZone } = this.originalOptions; + let offsetString; // hardcoding GMT and time zone names for this particular test suite @@ -532,8 +604,10 @@ function addTimeZoneNamePolyfill(testHtml: string): string { offsetString = offsetString.replace("-", "+"); } } else { + const offsetMarker = locale === "en-GB" ? "GMT" : locale === "fr" ? "UTC" : "GMT"; + offsetString = - "GMT" + + offsetMarker + (timeZone === "America/Mexico_City" || timeZone === "Pacific/Galapagos" ? "-6" : timeZone === "America/Phoenix" diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx index 927e00b97b8..e3e54037ad3 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx @@ -53,7 +53,7 @@ import { getUserTimeZoneOffset, } from "./utils"; import { InputTimeZoneMessages } from "./assets/input-time-zone/t9n"; -import { TimeZoneItem, TimeZoneMode } from "./interfaces"; +import { OffsetStyle, TimeZoneItem, TimeZoneMode } from "./interfaces"; @Component({ tag: "calcite-input-time-zone", @@ -137,6 +137,19 @@ export class InputTimeZone this.updateTimeZoneItemsAndSelection(); } + /** + * Specifies how the offset will be displayed, where + * + * `"user"` uses `UTC` or `GMT` depending on the user's locale, + * `"gmt"` always uses `GMT`, and + * `"utc"` always uses `UTC`. + * + * This only applies to the `offset` mode. + * + * @default "user" + */ + @Prop({ reflect: true }) offsetStyle: OffsetStyle = "user"; + /** Specifies the validation message to display under the component. */ @Prop() validationMessage: string; @@ -408,6 +421,7 @@ export class InputTimeZone this.referenceDate instanceof Date ? this.referenceDate : new Date(this.referenceDate ?? Date.now()), + this.offsetStyle, ); } diff --git a/packages/calcite-components/src/components/input-time-zone/interfaces.d.ts b/packages/calcite-components/src/components/input-time-zone/interfaces.d.ts index 77461f6aef5..6e29ffffc8e 100644 --- a/packages/calcite-components/src/components/input-time-zone/interfaces.d.ts +++ b/packages/calcite-components/src/components/input-time-zone/interfaces.d.ts @@ -13,3 +13,5 @@ export interface TimeZoneItem { value: T; filterValue: string | string[]; } + +export type OffsetStyle = "user" | "utc" | "gmt"; diff --git a/packages/calcite-components/src/components/input-time-zone/utils.ts b/packages/calcite-components/src/components/input-time-zone/utils.ts index a432bca1ecc..12f632156a0 100644 --- a/packages/calcite-components/src/components/input-time-zone/utils.ts +++ b/packages/calcite-components/src/components/input-time-zone/utils.ts @@ -1,5 +1,5 @@ import { getDateTimeFormat, SupportedLocale } from "../../utils/locale"; -import { TimeZoneItem, TimeZoneMode, TimeZoneName } from "./interfaces"; +import { OffsetStyle, TimeZoneItem, TimeZoneMode, TimeZoneName } from "./interfaces"; import { InputTimeZoneMessages } from "./assets/input-time-zone/t9n"; const hourToMinutes = 60; @@ -66,6 +66,7 @@ export async function createTimeZoneItems( messages: InputTimeZoneMessages, mode: TimeZoneMode, referenceDate: Date, + standardTime: OffsetStyle, ): Promise { const referenceDateInMs: number = referenceDate.getTime(); const timeZoneNames = Intl.supportedValuesOf("timeZone"); @@ -106,10 +107,20 @@ export async function createTimeZoneItems( .filter((index) => index >= 0 && index < group.tzs.length); }); + const effectiveLocale = + standardTime === "user" + ? locale + : // we use locales that will always yield a short offset that matches `standardTime` + standardTime === "utc" + ? "fr" + : "en-GB"; + return timeZoneGroups .map>(({ labelTzIndices, tzs }) => { const groupRepTz = tzs[0]; - const decimalOffset = timeZoneOffsetToDecimal(getTimeZoneShortOffset(groupRepTz, locale, referenceDateInMs)); + const decimalOffset = timeZoneOffsetToDecimal( + getTimeZoneShortOffset(groupRepTz, effectiveLocale, referenceDateInMs), + ); const value = toOffsetValue(groupRepTz, referenceDateInMs); const tzLabels = labelTzIndices.map((index: number) => { const timeZone = tzs[index];