Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TimezonePicker only controlled usage #2127

Merged
merged 13 commits into from
Feb 15, 2018
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"dev:all": "lerna run dev --parallel --scope '!@blueprintjs/{landing-app,table-dev-app}'",
"dev:core": "lerna run dev --parallel --scope '@blueprintjs/{core,icons,docs-app}'",
"dev:docs": "lerna run dev --parallel --scope '@blueprintjs/{docs-app,docs-theme}'",
"dev:datetime": "lerna run dev --parallel --scope '@blueprintjs/{core,datetime,docs-app}'",
"dev:datetime": "lerna run dev --parallel --scope '@blueprintjs/{core,datetime,timezone,docs-app}'",
"dev:labs": "lerna run dev --parallel --scope '@blueprintjs/{core,labs,select,docs-app}'",
"dev:landing": "lerna run dev --parallel --scope '@blueprintjs/{core,landing-app}'",
"dev:select": "lerna run dev --parallel --scope '@blueprintjs/{core,select,docs-app}'",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,17 @@ import { BaseExample, handleBooleanChange, handleStringChange } from "@blueprint
import { TimezoneDisplayFormat, TimezonePicker } from "@blueprintjs/timezone";

export interface ITimezonePickerExampleState {
date?: Date;
disabled?: boolean;
showLocalTimezone?: boolean;
targetDisplayFormat?: TimezoneDisplayFormat;
timezone?: string;
disabled: boolean;
showLocalTimezone: boolean;
targetDisplayFormat: TimezoneDisplayFormat;
timezone: string;
}

export class TimezonePickerExample extends BaseExample<ITimezonePickerExampleState> {
public state: ITimezonePickerExampleState = {
date: new Date(),
disabled: false,
showLocalTimezone: true,
targetDisplayFormat: TimezoneDisplayFormat.OFFSET,
targetDisplayFormat: TimezoneDisplayFormat.COMPOSITE,
timezone: "",
};

Expand All @@ -36,11 +34,10 @@ export class TimezonePickerExample extends BaseExample<ITimezonePickerExampleSta
);

protected renderExample() {
const { date, timezone, targetDisplayFormat, disabled, showLocalTimezone } = this.state;
const { timezone, targetDisplayFormat, disabled, showLocalTimezone } = this.state;

return (
<TimezonePicker
date={date}
value={timezone}
onChange={this.handleTimezoneChange}
valueDisplayFormat={targetDisplayFormat}
Expand Down Expand Up @@ -79,9 +76,9 @@ export class TimezonePickerExample extends BaseExample<ITimezonePickerExampleSta
selectedValue={this.state.targetDisplayFormat}
>
<Radio label="Abbreviation" value={TimezoneDisplayFormat.ABBREVIATION} />
<Radio label="Composite" value={TimezoneDisplayFormat.COMPOSITE} />
<Radio label="Name" value={TimezoneDisplayFormat.NAME} />
<Radio label="Offset" value={TimezoneDisplayFormat.OFFSET} />
<Radio label="Composite" value={TimezoneDisplayFormat.COMPOSITE} />
</RadioGroup>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,17 @@ import { getTimezoneMetadata } from "./timezoneMetadata";

export type TimezoneDisplayFormat = "offset" | "abbreviation" | "name" | "composite";
export const TimezoneDisplayFormat = {
/** Abbreviation format i.e. "HST" */
/** Abbreviation format: `"HST"` */
ABBREVIATION: "abbreviation" as "abbreviation",
/** Composite format i.e. "Pacific/Honolulu (HST) -10:00" */
/** Composite format: `"Pacific/Honolulu (HST) -10:00"` */
COMPOSITE: "composite" as "composite",
/** Name format i.e. "Pacific/Honolulu" */
/** Name format: `"Pacific/Honolulu"` */
NAME: "name" as "name",
/** Offset format i.e. "-10:00" */
/** Offset format: `"-10:00"` */
OFFSET: "offset" as "offset",
};

export function formatTimezone(
timezone: string | undefined,
date: Date,
displayFormat: TimezoneDisplayFormat,
): string | undefined {
export function formatTimezone(timezone: string, date: Date, displayFormat: TimezoneDisplayFormat): string | undefined {
if (!timezone || !moment.tz.zone(timezone)) {
return undefined;
}
Expand All @@ -39,12 +35,5 @@ export function formatTimezone(
return offsetAsString;
case TimezoneDisplayFormat.COMPOSITE:
return `${timezone}${abbreviation ? ` (${abbreviation})` : ""} ${offsetAsString}`;
default:
assertNever(displayFormat);
return undefined;
}
}

function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
63 changes: 28 additions & 35 deletions packages/timezone/src/components/timezone-picker/timezoneItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import { IconName } from "@blueprintjs/core";
import * as moment from "moment-timezone";
import { getTimezoneMetadata, ITimezoneMetadata } from "./timezoneMetadata";
import { getLocalTimezone } from "./timezoneUtils";

/** Timezone-specific QueryList item */
export interface ITimezoneItem {
Expand All @@ -32,7 +31,11 @@ export interface ITimezoneItem {
* @param date the date to use when determining timezone offsets
*/
export function getTimezoneItems(date: Date): ITimezoneItem[] {
return moment.tz.names().map(timezone => toTimezoneItem(timezone, date));
return moment.tz
.names()
.map(timezone => getTimezoneMetadata(timezone, date))
.sort((a, b) => a.offset - b.offset)
.map(toTimezoneItem);
}

/**
Expand All @@ -53,7 +56,7 @@ export function getInitialTimezoneItems(date: Date, includeLocalTimezone: boolea
* @param date the date to use when determining timezone offsets
*/
export function getLocalTimezoneItem(date: Date): ITimezoneItem | undefined {
const timezone = getLocalTimezone();
const timezone = moment.tz.guess();
if (timezone !== undefined) {
const timestamp = date.getTime();
const zonedDate = moment.tz(timestamp, timezone);
Expand All @@ -79,42 +82,32 @@ function getPopulousTimezoneItems(date: Date): ITimezoneItem[] {
// Filter out noisy timezones. See https://github.com/moment/moment-timezone/issues/227
const timezones = moment.tz.names().filter(timezone => /\//.test(timezone) && !/Etc\//.test(timezone));

const timezoneToMetadata: { [timezone: string]: ITimezoneMetadata } = {};
for (const timezone of timezones) {
timezoneToMetadata[timezone] = getTimezoneMetadata(timezone, date);
}

// Order by offset ascending, population descending, timezone name ascending
timezones.sort((timezone1, timezone2) => {
const { offset: offset1, population: population1 } = timezoneToMetadata[timezone1];
const { offset: offset2, population: population2 } = timezoneToMetadata[timezone2];
if (offset1 === offset2) {
if (population1 === population2) {
// Fall back to sorting alphabetically
return timezone1 < timezone2 ? -1 : 1;
}
// Descending order of population
return population2 - population1;
}
// Ascending order of offset
return offset1 - offset2;
});
const timezoneToMetadata = timezones.reduce<{ [timezone: string]: ITimezoneMetadata }>((store, zone) => {
store[zone] = getTimezoneMetadata(zone, date);
return store;
}, {});

// Choose the most populous locations for each offset
const initialTimezones: ITimezoneItem[] = [];
let prevOffset: number;
for (const timezone of timezones) {
const curOffset = timezoneToMetadata[timezone].offset;
if (prevOffset === undefined || prevOffset !== curOffset) {
initialTimezones.push(toTimezoneItem(timezone, date));
prevOffset = curOffset;
// reduce timezones array to maximum population per offset, for each unique offset.
const maxPopPerOffset = timezones.reduce<{ [offset: string]: string }>((maxPop, zone) => {
const data = timezoneToMetadata[zone];
const currentMax = maxPop[data.offsetAsString];
if (currentMax == null || data.population > timezoneToMetadata[currentMax].population) {
maxPop[data.offsetAsString] = zone;
}
}
return initialTimezones;
return maxPop;
}, {});
return (
Object.keys(maxPopPerOffset)
// get metadata object
.map(k => timezoneToMetadata[maxPopPerOffset[k]])
// sort by offset
.sort((tz1, tz2) => tz1.offset - tz2.offset)
// convert to renderable item
.map(toTimezoneItem)
);
}

function toTimezoneItem(timezone: string, date: Date): ITimezoneItem {
const { abbreviation, offsetAsString } = getTimezoneMetadata(timezone, date);
function toTimezoneItem({ abbreviation, offsetAsString, timezone }: ITimezoneMetadata): ITimezoneItem {
return {
key: timezone,
label: offsetAsString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

import * as moment from "moment-timezone";

// non-empty abbreviations that do not begin with -/+
const ABBR_REGEX = /^[^-+]/;

export interface ITimezoneMetadata {
timezone: string;
abbreviation: string | undefined;
Expand All @@ -20,7 +23,11 @@ export function getTimezoneMetadata(timezone: string, date: Date): ITimezoneMeta
const zonedDate = moment.tz(timestamp, timezone);
const offset = zonedDate.utcOffset();
const offsetAsString = zonedDate.format("Z");
const abbreviation = getAbbreviation(timezone, timestamp);

// Only include abbreviations that are not just a repeat of the offset:
// moment-timezone's `abbr` falls back to the time offset if a zone doesn't have an abbr.
const abbr = zone.abbr(timestamp);
const abbreviation = ABBR_REGEX.test(abbr) ? abbr : undefined;

return {
abbreviation,
Expand All @@ -30,23 +37,3 @@ export function getTimezoneMetadata(timezone: string, date: Date): ITimezoneMeta
timezone,
};
}

/**
* Get the abbreviation for a timezone.
* We need this utility because moment-timezone's `abbr` will not always give the abbreviated time zone name,
* since it falls back to the time offsets for each region.
* https://momentjs.com/timezone/docs/#/using-timezones/formatting/
*/
function getAbbreviation(timezone: string, timestamp: number): string | undefined {
const zone = moment.tz.zone(timezone);
if (zone) {
const abbreviation = zone.abbr(timestamp);

// Only include abbreviations that are not just a repeat of the offset
if (abbreviation.length > 0 && abbreviation[0] !== "-" && abbreviation[0] !== "+") {
return abbreviation;
}
}

return undefined;
}
Loading