diff --git a/package-lock.json b/package-lock.json index f96bd8a3a5e..89f917c0578 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32981,12 +32981,6 @@ "dev": true, "license": "MIT" }, - "node_modules/timezone-groups": { - "version": "0.8.0", - "bin": { - "timezone-groups": "dist/cli.cjs" - } - }, "node_modules/timm": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", @@ -35894,7 +35888,7 @@ "focus-trap": "7.5.4", "lodash-es": "4.17.21", "sortablejs": "1.15.1", - "timezone-groups": "0.8.0", + "timezone-groups": "0.9.0", "type-fest": "4.18.2" }, "devDependencies": { @@ -39466,6 +39460,14 @@ "node": ">=8" } }, + "packages/calcite-components/node_modules/timezone-groups": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/timezone-groups/-/timezone-groups-0.9.0.tgz", + "integrity": "sha512-6Os7VhGet5ZU5q7Sx5hxzyzyym5+IfVax5m6LBz1LqZGshlcDRykYyyD4j7yhN1yn/jjw4eJpd8KpzyPWKU+nQ==", + "engines": { + "node": ">=20.0.0" + } + }, "packages/calcite-components/node_modules/type-fest": { "version": "4.18.2", "license": "(MIT OR CC0-1.0)", diff --git a/packages/calcite-components/package.json b/packages/calcite-components/package.json index 170a52ab6fb..36092cf27c5 100644 --- a/packages/calcite-components/package.json +++ b/packages/calcite-components/package.json @@ -72,7 +72,7 @@ "focus-trap": "7.5.4", "lodash-es": "4.17.21", "sortablejs": "1.15.1", - "timezone-groups": "0.8.0", + "timezone-groups": "0.9.0", "type-fest": "4.18.2" }, "devDependencies": { diff --git a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json index 93a716c624d..378072b74f4 100644 --- a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json +++ b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json @@ -1,6 +1,7 @@ { "chooseTimeZone": "Choose time zone.", "offsetPlaceholder": "Search by city, region or offset", + "regionPlaceholder": "Search by city, country, region or offset", "namePlaceholder": "Search by time zone", "timeZoneLabel": "({offset}) {cities}", "Africa/Abidjan": "Abidjan", @@ -431,5 +432,254 @@ "Pacific/Tongatapu": "Tongatapu", "Pacific/Truk": "Truk", "Pacific/Wake": "Wake", - "Pacific/Wallis": "Wallis" + "Pacific/Wallis": "Wallis", + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "Democratic Republic of the Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Côte d'Ivoire", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macau", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn Islands", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "São Tomé and Príncipe", + "SV": "El Salvador", + "SX": "Sint Maarten", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "French Southern Territories", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Vatican City", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "United States Virgin Islands", + "VN": "Vietnam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe" } diff --git a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json index 93a716c624d..378072b74f4 100644 --- a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json +++ b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json @@ -1,6 +1,7 @@ { "chooseTimeZone": "Choose time zone.", "offsetPlaceholder": "Search by city, region or offset", + "regionPlaceholder": "Search by city, country, region or offset", "namePlaceholder": "Search by time zone", "timeZoneLabel": "({offset}) {cities}", "Africa/Abidjan": "Abidjan", @@ -431,5 +432,254 @@ "Pacific/Tongatapu": "Tongatapu", "Pacific/Truk": "Truk", "Pacific/Wake": "Wake", - "Pacific/Wallis": "Wallis" + "Pacific/Wallis": "Wallis", + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "Democratic Republic of the Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Côte d'Ivoire", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macau", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn Islands", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "São Tomé and Príncipe", + "SV": "El Salvador", + "SX": "Sint Maarten", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "French Southern Territories", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Vatican City", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "United States Virgin Islands", + "VN": "Vietnam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe" } 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 d2a0feb9cd5..da4b93d456e 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 @@ -14,7 +14,7 @@ import { } from "../../tests/commonTests"; import { TagAndPage } from "../../tests/commonTests/interfaces"; import { DEBOUNCE } from "../../utils/resources"; -import { toUserFriendlyName } from "./utils"; +import { getCity, toUserFriendlyName } from "./utils"; /* * **Notes** @@ -319,6 +319,71 @@ describe("calcite-input-time-zone", () => { expect(await timeZoneItem.getProperty("textLabel")).toMatch(toUserFriendlyName(testTimeZoneItems[0].name)); }); }); + + describe("region", () => { + describe("selects user's matching time zone name on initialization", () => { + testTimeZoneItems.forEach(({ name }) => { + it(`selects default time zone for "${name}"`, async () => { + const page = await newE2EPage(); + await page.emulateTimezone(name); + await page.setContent( + await overrideSupportedTimeZones(html``), + ); + await page.waitForChanges(); + + const input = await page.find("calcite-input-time-zone"); + expect(await input.getProperty("value")).toBe(name); + + const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]"); + + expect(await timeZoneItem.getProperty("textLabel")).toMatch(toUserFriendlyName(getCity(name))); + }); + }); + }); + + it("allows users to preselect a time zone by name", async () => { + const page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + await page.setContent( + await overrideSupportedTimeZones( + html``, + ), + ); + + const input = await page.find("calcite-input-time-zone"); + + expect(await input.getProperty("value")).toBe(testTimeZoneItems[1].name); + + const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]"); + + expect(await timeZoneItem.getProperty("textLabel")).toMatch( + toUserFriendlyName(getCity(testTimeZoneItems[1].name)), + ); + }); + + it("ignores invalid values", async () => { + const page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + await page.setContent( + await overrideSupportedTimeZones( + html``, + ), + ); + + const input = await page.find("calcite-input-time-zone"); + + expect(await input.getProperty("value")).toBe(testTimeZoneItems[0].name); + + const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]"); + + expect(await timeZoneItem.getProperty("textLabel")).toMatch( + toUserFriendlyName(getCity(testTimeZoneItems[0].name)), + ); + }); + }); }); describe("clearable", () => { diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.scss b/packages/calcite-components/src/components/input-time-zone/input-time-zone.scss index b3ac7411e63..25b19175dd4 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.scss +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.scss @@ -2,6 +2,10 @@ display: block; } +.offset { + white-space: nowrap; +} + @include base-component(); @include disabled(); @include hidden-form-input(); diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts b/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts index a19a8a8d850..aa312f37d03 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts @@ -62,10 +62,12 @@ export const clearable = (): string => html` +
+ `; clearable.parameters = { chromatic: { delay: 500 } }; @@ -74,6 +76,10 @@ export const timeZoneNameMode_TestOnly = (): string => html` `; +export const timeZoneRegionMode_TestOnly = (): string => html` + +`; + export const initialNameSelected_TestOnly = (): string => // for stability, we use a timezone unaffected by daylight savings time html``; 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 db0221fe17a..8c7a1302159 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 @@ -47,6 +47,7 @@ import { MutableValidityState, } from "../../utils/form"; import { IconNameOrString } from "../icon/interfaces"; +import { CSS } from "./resources"; import { createTimeZoneItems, findTimeZoneItemByProp, @@ -54,7 +55,7 @@ import { getUserTimeZoneOffset, } from "./utils"; import { InputTimeZoneMessages } from "./assets/input-time-zone/t9n"; -import { OffsetStyle, TimeZoneItem, TimeZoneMode } from "./interfaces"; +import { OffsetStyle, TimeZoneItem, TimeZoneItemGroup, TimeZoneMode } from "./interfaces"; @Component({ tag: "calcite-input-time-zone", @@ -324,7 +325,7 @@ export class InputTimeZone private selectedTimeZoneItem: TimeZoneItem; - private timeZoneItems: TimeZoneItem[]; + private timeZoneItems: TimeZoneItem[] | TimeZoneItemGroup[]; //-------------------------------------------------------------------------- // @@ -410,7 +411,7 @@ export class InputTimeZone this.findTimeZoneItem(valueToMatch) || this.findTimeZoneItem(fallbackValue); } - private async createTimeZoneItems(): Promise { + private async createTimeZoneItems(): Promise { if (!this.effectiveLocale || !this.messages) { return []; } @@ -488,7 +489,11 @@ export class InputTimeZone open={this.open} overlayPositioning={this.overlayPositioning} placeholder={ - this.mode === "name" ? this.messages.namePlaceholder : this.messages.offsetPlaceholder + this.mode === "name" + ? this.messages.namePlaceholder + : this.mode === "offset" + ? this.messages.offsetPlaceholder + : this.messages.regionPlaceholder } readOnly={this.readOnly} ref={this.setComboboxRef} @@ -498,24 +503,59 @@ export class InputTimeZone validation-icon={this.validationIcon} validation-message={this.validationMessage} > - {this.timeZoneItems.map((group) => { - const selected = this.selectedTimeZoneItem === group; - const { label, value } = group; - - return ( - - ); - })} + {this.renderItems()} ); } + + private renderItems(): VNode[] { + if (this.mode === "region") { + return this.renderRegionItems(); + } + + return this.timeZoneItems.map((group) => { + const selected = this.selectedTimeZoneItem === group; + const { label, value } = group; + + return ( + + ); + }); + } + + private renderRegionItems(): VNode[] { + return (this.timeZoneItems as TimeZoneItemGroup[]).flatMap(({ label, items }) => ( + + {items.map((item) => { + const selected = this.selectedTimeZoneItem === item; + const { label, value } = item; + + return ( + + + {item.metadata.offset} + + + ); + })} + + )); + } } 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 6e29ffffc8e..2b13fdc0c0a 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 @@ -1,17 +1,37 @@ declare global { namespace Intl { - function supportedValuesOf(key: "timeZone"): TimeZoneName[]; + function supportedValuesOf(key: "timeZone"): TimeZone[]; } } -export type TimeZoneName = string; +export type TimeZone = string; -export type TimeZoneMode = "offset" | "name"; +export interface TimeZoneGroup extends BasicTimeZoneGroup { + offsetGroupLabel: string; + offsetGroupRepTimeZone: string; + offsetGroupTimeZones: string[]; +} + +export interface BasicTimeZoneGroup { + offsetLabel: string; + offsetValue: number; +} + +export type TimeZoneMode = "offset" | "name" | "region"; export interface TimeZoneItem { label: string; value: T; filterValue: string | string[]; + metadata?: { + offset?: string; + country?: string; + }; +} + +export interface TimeZoneItemGroup { + label: string; + items: TimeZoneItem[]; } export type OffsetStyle = "user" | "utc" | "gmt"; diff --git a/packages/calcite-components/src/components/input-time-zone/resources.ts b/packages/calcite-components/src/components/input-time-zone/resources.ts new file mode 100644 index 00000000000..f20cb242136 --- /dev/null +++ b/packages/calcite-components/src/components/input-time-zone/resources.ts @@ -0,0 +1,3 @@ +export const CSS = { + offset: "offset", +}; 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 12f632156a0..11b99174c43 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 { OffsetStyle, TimeZoneItem, TimeZoneMode, TimeZoneName } from "./interfaces"; +import { OffsetStyle, TimeZone, TimeZoneItem, TimeZoneItemGroup, TimeZoneMode } from "./interfaces"; import { InputTimeZoneMessages } from "./assets/input-time-zone/t9n"; const hourToMinutes = 60; @@ -35,7 +35,7 @@ function timeZoneOffsetToDecimal(shortOffsetTimeZoneName: string): string { ); } -function toOffsetValue(timeZoneName: TimeZoneName, referenceDateInMs: number): number { +function toOffsetValue(timeZoneName: TimeZone, referenceDateInMs: number): number { // we use "en-US" to allow us to reliably remove the standard time token const offset = getTimeZoneShortOffset(timeZoneName, "en-US", referenceDateInMs).replace("GMT", ""); @@ -59,7 +59,19 @@ export function getUserTimeZoneName(): string { /** * The lazy-loaded timezone-groups lib to be used across instances. */ -let timeZoneGroups: Promise<[any, any]>; +let offsetGroupUtils: Promise< + [ + typeof import("timezone-groups/dist/groupByOffset/index.mjs"), + typeof import("timezone-groups/dist/groupByOffset/strategy/native.mjs"), + ] +>; +let regionGroupUtils: Promise< + [ + typeof import("timezone-groups/dist/groupByRegion/index.mjs"), + typeof import("timezone-groups/dist/utils/country.mjs"), + ] +>; +let nameGroupUtils: Promise; export async function createTimeZoneItems( locale: SupportedLocale, @@ -67,97 +79,143 @@ export async function createTimeZoneItems( mode: TimeZoneMode, referenceDate: Date, standardTime: OffsetStyle, -): Promise { +): Promise { + if (mode === "name") { + if (!nameGroupUtils) { + nameGroupUtils = import("timezone-groups/dist/groupByName/index.mjs"); + } + + return nameGroupUtils.then(async ({ groupByName }) => { + const groups = await groupByName(); + + return groups + .map>(({ label: timeZone }) => { + const label = toUserFriendlyName(timeZone); + const value = timeZone; + + return { + label, + value, + filterValue: timeZone, + }; + }) + .filter((group) => !!group) + .sort(); + }); + } + + const effectiveLocale = + standardTime === "user" + ? locale + : // we use locales that will always yield a short offset that matches `standardTime` + standardTime === "utc" + ? "fr" + : "en-GB"; const referenceDateInMs: number = referenceDate.getTime(); - const timeZoneNames = Intl.supportedValuesOf("timeZone"); - if (mode === "offset") { - if (!timeZoneGroups) { - timeZoneGroups = Promise.all([ - import("timezone-groups/dist/index.js"), - import("timezone-groups/dist/strategy/native/index.js"), + if (mode === "region") { + if (!regionGroupUtils) { + regionGroupUtils = Promise.all([ + import("timezone-groups/dist/groupByRegion/index.mjs"), + import("timezone-groups/dist/utils/country.mjs"), ]); } - return timeZoneGroups.then(async ([{ groupTimeZones }, { DateEngine }]) => { - const timeZoneGroups: { labelTzIndices: number[]; tzs: TimeZoneName[] }[] = await groupTimeZones({ - dateEngine: new DateEngine(), - groupDateRange: 1, - startDate: new Date(referenceDateInMs).toISOString(), - }); + return regionGroupUtils.then(async ([{ groupByRegion }, { getCountry }]) => { + const groups = await groupByRegion(); - const listFormatter = new Intl.ListFormat(locale, { style: "long", type: "conjunction" }); + return groups + .map(({ label: region, tzs }) => { + return { + label: region, + items: tzs.map((timeZone) => { + const decimalOffset = timeZoneOffsetToDecimal( + getTimeZoneShortOffset(timeZone, effectiveLocale, referenceDateInMs), + ); + + return { + label: getTimeZoneLabel(timeZone, messages), + value: timeZone, + filterValue: toUserFriendlyName(timeZone), + metadata: { + offset: decimalOffset, + country: getCountry(timeZone), + }, + }; + }), + }; + }) + .sort((groupA, groupB) => groupA.label.localeCompare(groupB.label)); + }); + } + + if (!offsetGroupUtils) { + offsetGroupUtils = Promise.all([ + import("timezone-groups/dist/groupByOffset/index.mjs"), + import("timezone-groups/dist/groupByOffset/strategy/native/index.mjs"), + ]); + } - // we remove blocked entries from tzs and adjust label indices accordingly - timeZoneGroups.forEach((group) => { - const indexOffsets: number[] = []; - let removedSoFar = 0; + return offsetGroupUtils.then(async ([{ groupByOffset }, { DateEngine }]) => { + const groups = await groupByOffset({ + dateEngine: new DateEngine(), + groupDateRange: 1, + startDate: new Date(referenceDateInMs).toISOString(), + }); - group.tzs.forEach((tz, index) => { - if (timeZoneNameBlockList.includes(tz)) { - removedSoFar++; - } - indexOffsets[index] = removedSoFar; - }); + const listFormatter = new Intl.ListFormat(locale, { style: "long", type: "conjunction" }); - group.tzs = group.tzs.filter((tz) => !timeZoneNameBlockList.includes(tz)); + // we remove blocked entries from tzs and adjust label indices accordingly + groups.forEach((group) => { + const indexOffsets: number[] = []; + let removedSoFar = 0; - group.labelTzIndices = group.labelTzIndices - .map((index) => index - indexOffsets[index]) - .filter((index) => index >= 0 && index < group.tzs.length); + group.tzs.forEach((tz, index) => { + if (timeZoneNameBlockList.includes(tz)) { + removedSoFar++; + } + indexOffsets[index] = removedSoFar; }); - 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, effectiveLocale, referenceDateInMs), - ); - const value = toOffsetValue(groupRepTz, referenceDateInMs); - const tzLabels = labelTzIndices.map((index: number) => { - const timeZone = tzs[index]; - const timeZoneLabel = messages[timeZone]; - return ( - timeZoneLabel || - // get city token - timeZone.split("/").pop() - ); - }); - - const label = createTimeZoneOffsetLabel(messages, decimalOffset, listFormatter.format(tzLabels)); + group.tzs = group.tzs.filter((tz) => !timeZoneNameBlockList.includes(tz)); - return { - label, - value, - filterValue: tzs.map((tz) => toUserFriendlyName(tz)), - }; - }) - .filter((group) => !!group) - .sort((groupA, groupB) => groupA.value - groupB.value); + group.labelTzIdx = group.labelTzIdx + .map((index) => index - indexOffsets[index]) + .filter((index) => index >= 0 && index < group.tzs.length); }); - } - return timeZoneNames - .map>((timeZone) => { - const label = toUserFriendlyName(timeZone); - const value = timeZone; - - return { - label, - value, - filterValue: timeZone, - }; - }) - .filter((group) => !!group) - .sort(); + return groups + .map>(({ labelTzIdx, tzs }) => { + const groupRepTz = tzs[0]; + const decimalOffset = timeZoneOffsetToDecimal( + getTimeZoneShortOffset(groupRepTz, effectiveLocale, referenceDateInMs), + ); + const value = toOffsetValue(groupRepTz, referenceDateInMs); + const tzLabels = labelTzIdx.map((index: number) => getTimeZoneLabel(tzs[index], messages)); + const label = createTimeZoneOffsetLabel(messages, decimalOffset, listFormatter.format(tzLabels)); + + return { + label, + value, + filterValue: tzs.map((tz) => toUserFriendlyName(tz)), + }; + }) + .filter((group) => !!group) + .sort((groupA, groupB) => groupA.value - groupB.value); + }); +} + +function getTimeZoneLabel(timeZone: string, messages: InputTimeZoneMessages): string { + return messages[timeZone] || getCity(timeZone); +} + +/** + * Exported for testing purposes only + * + * @internal + */ +export function getCity(timeZone: string): string { + return timeZone.split("/").pop(); } /** @@ -174,7 +232,7 @@ function createTimeZoneOffsetLabel(messages: InputTimeZoneMessages, offsetLabel: } function getTimeZoneShortOffset( - timeZone: TimeZoneName, + timeZone: TimeZone, locale: SupportedLocale, referenceDateInMs: number = Date.now(), ): string { @@ -183,14 +241,22 @@ function getTimeZoneShortOffset( return parts.find(({ type }) => type === "timeZoneName").value; } +function isGroup(item: TimeZoneItem | TimeZoneItemGroup): item is TimeZoneItemGroup { + return (item as TimeZoneItemGroup).items !== undefined; +} + +function flattenTimeZoneItems(timeZoneItems: TimeZoneItem[] | TimeZoneItemGroup[]): TimeZoneItem[] { + return isGroup(timeZoneItems[0]) ? timeZoneItems.flatMap((item) => item.items) : timeZoneItems; +} + export function findTimeZoneItemByProp( - timeZoneItems: TimeZoneItem[], + timeZoneItems: TimeZoneItem[] | TimeZoneItemGroup[], prop: string, valueToMatch: string | number | null, ): TimeZoneItem | null { return valueToMatch == null ? null - : timeZoneItems.find( + : flattenTimeZoneItems(timeZoneItems).find( (item) => // intentional == to match string to number item[prop] == valueToMatch,