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

[Labs] Timezone Picker #1568

Merged
merged 83 commits into from
Sep 22, 2017
Merged
Changes from 1 commit
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
356743c
Remove non-existent tsconfig include dir
Sep 12, 2017
66c6edc
Install moment and moment-timezone
Sep 12, 2017
4a4aef2
Remove unused import
Sep 12, 2017
b6d8302
Shorten imports
Sep 12, 2017
456303a
Timezone Input V0
Sep 12, 2017
7de36a1
Use Select component
Sep 12, 2017
67ca676
Seed list with representative timezones
Sep 13, 2017
dad05d1
Additional timezone metadata
Sep 13, 2017
75dd866
Specify general tab size and scss-specific tab size in workspace sett…
Sep 13, 2017
a1ee5cb
Left-aligned time zone offset label
Sep 13, 2017
5739478
Labels look better on right
Sep 13, 2017
f61607c
Enforce a min-width, so the menu doesn't jump around while filtering
Sep 13, 2017
185a671
Move abbreviation after name, so offsets align
Sep 13, 2017
4bb8e12
Additional props: disabled, defaultTimezone, selectedTimezoneFormat
Sep 13, 2017
50f8e78
Clean up naming and initial timezone getter
Sep 13, 2017
0b0237c
showUserTimezoneGuess prop
Sep 13, 2017
25176ab
Pass through popoverProps
Sep 13, 2017
c08169f
placeholder prop
Sep 13, 2017
3e4a482
targetClassName prop
Sep 13, 2017
8823063
Add disabled switch to example; Use handler helpers
Sep 13, 2017
4c2def9
Add interface to docs page
Sep 13, 2017
e5dc667
Add example switches for default timezone and use guess
Sep 13, 2017
ba10a57
Timezone query candidates
Sep 13, 2017
86ade6b
Change empty state text
Sep 13, 2017
cbf7ee3
Only show user timezone if query is empty
Sep 13, 2017
72c143a
Controlled mode for selected timezone
Sep 13, 2017
cd0f2ad
Don't exclude popular guess
Sep 14, 2017
8671cc0
Add onQueryChange to account for the query being changed through reset
Sep 14, 2017
d0f1e4f
Use onQueryChange
Sep 14, 2017
f6f8b93
Add defaultToUserTimezoneGuess prop
Sep 14, 2017
c7b76d2
Make timezone input look more like an input
Sep 14, 2017
1946977
Make placeholder consistent with display format
Sep 14, 2017
b359c1a
Show the timezone input example in the context of date and time pickers
Sep 14, 2017
020d02f
More documentation
Sep 14, 2017
0cc560c
Clean up props; Add documentation
Sep 14, 2017
44e70d1
timezone input -> timezone select
Sep 14, 2017
4d55db2
Custom target renderer
Sep 14, 2017
f237bdc
Handle empty date
Sep 14, 2017
1054cea
Merge remote-tracking branch 'origin/master' into bb/timezone
Sep 14, 2017
f6858a7
Only use text cursor if not disabled
Sep 14, 2017
9400904
Basic and extended examples
Sep 14, 2017
d2f7f78
Placeholder style
Sep 14, 2017
e10b3d3
Fix lint
Sep 14, 2017
2114c6a
Improve documentation
Sep 15, 2017
d9d00bc
Improve props docs
Sep 15, 2017
f652ffe
Optional icon name
Sep 15, 2017
dc85ca6
Don't use the `||` pattern
Sep 15, 2017
b586639
Use custom class name last
Sep 15, 2017
add9e49
Fix nits
Sep 15, 2017
900c07e
Reorder documentation
Sep 15, 2017
bbf0ff1
Rename TimezoneSelect to TimezonePicker
Sep 15, 2017
7fc4720
Remove the display tag from the example
Sep 15, 2017
8782648
Remove confusing default value
Sep 15, 2017
15947e6
Add buttonProps; Better placeholder
Sep 15, 2017
5afc0d7
Remove pt-timezone-picker-target-placeholder
Sep 15, 2017
ef932e9
Remove defaultToLocalTimezone prop
Sep 15, 2017
cd20736
Split timezone utilities into separate files
Sep 15, 2017
b23c37c
Expose input props; More descriptive input placeholder
Sep 15, 2017
4c8cb85
Don't make example button primary
Sep 15, 2017
4ca31e4
Use radio group for formats in example
Sep 15, 2017
7b96c8a
Composite display format
Sep 15, 2017
bf15ba5
Clean up timezone querying
Sep 15, 2017
11631b8
Basic query match ranking
Sep 16, 2017
76cad64
Prioritize exact matches
Sep 16, 2017
bd9ef19
Add fuzzaldrin-plus
Sep 19, 2017
b2b4788
Use fuzzaldrin-plus for timezone item sorting and filtering
Sep 19, 2017
ff49db9
Remove unneeded query candidates because fuzzaldrin is that good
Sep 19, 2017
d427502
Use filter key constant for clarity
Sep 19, 2017
ab8d5d5
Docs nits
Sep 19, 2017
b8e6c32
Merge remote-tracking branch 'origin/master' into bb/timezone
Sep 19, 2017
68ce37c
Make timezone code "prettier"
Sep 19, 2017
7f5df58
Fix lints
Sep 19, 2017
b0be6b6
[Labs/Select] Sync input props value with query value
Sep 19, 2017
ffbb9d3
[Labs/TimezonePicker] Sync input props value with query value
Sep 19, 2017
956b612
Fix misnamed test
Sep 19, 2017
76fccc2
Timezone picker tests
Sep 19, 2017
c208968
Fix nits
Sep 21, 2017
56bf58e
Use module augmentation instead of casting for zone.population
Sep 21, 2017
d5ab205
Don't import `from ".."`
Sep 21, 2017
3453ce0
Fix doc nits
Sep 21, 2017
b14a7c2
Make test style more consistent
Sep 21, 2017
248f670
Fix tests
Sep 21, 2017
2b37613
Concatenate item query candidates for better ranking
Sep 21, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Custom target renderer
  • Loading branch information
Brie Bunge committed Sep 14, 2017
commit 4d55db2a35ecdda2aa3074dbbce20e4dd6b0c07a
34 changes: 33 additions & 1 deletion packages/labs/examples/timezoneSelectExample.tsx
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import { Classes, Icon, Switch, Tag } from "@blueprintjs/core";
import { DatePicker, TimePicker, TimePickerPrecision } from "@blueprintjs/datetime";
import { BaseExample, handleBooleanChange, handleStringChange } from "@blueprintjs/docs";

import { TimezoneDisplayFormat, TimezoneSelect } from "../src";
import { ITimezoneSelectTargetRenderer, TimezoneDisplayFormat, TimezoneSelect } from "../src";

export interface ITimezoneSelectExampleState {
date?: Date;
@@ -25,6 +25,7 @@ export interface ITimezoneSelectExampleState {
showUserTimezoneGuess?: boolean;
useDefault?: boolean;
defaultToUserTimezoneGuess?: boolean;
useCustomRenderer?: boolean;
}

const EXAMPLE_DEFAULT_TIMEZONE = "Pacific/Honolulu";
@@ -39,6 +40,7 @@ export class TimezoneSelectExample extends BaseExample<ITimezoneSelectExampleSta
targetDisplayFormat: TimezoneDisplayFormat.OFFSET,
time: new Date(),
timezone: "",
useCustomRenderer: true,
useDefault: false,
};

@@ -50,6 +52,8 @@ export class TimezoneSelectExample extends BaseExample<ITimezoneSelectExampleSta
this.setState({ useDefault }));
private handleDefaultToUserTimezoneGuessChange = handleBooleanChange((defaultToUserTimezoneGuess) =>
this.setState({ defaultToUserTimezoneGuess }));
private handleUseCustomRendererChange = handleBooleanChange((useCustomRenderer) =>
this.setState({ useCustomRenderer }));
private handleFormatChange = handleStringChange((targetDisplayFormat: TimezoneDisplayFormat) =>
this.setState({ targetDisplayFormat }));

@@ -62,6 +66,7 @@ export class TimezoneSelectExample extends BaseExample<ITimezoneSelectExampleSta
targetDisplayFormat,
disabled,
showUserTimezoneGuess,
useCustomRenderer,
useDefault,
defaultToUserTimezoneGuess,
} = this.state;
@@ -98,6 +103,7 @@ export class TimezoneSelectExample extends BaseExample<ITimezoneSelectExampleSta
disabled={disabled}
defaultValue={useDefault ? EXAMPLE_DEFAULT_TIMEZONE : undefined}
defaultToLocalTimezone={defaultToUserTimezoneGuess}
targetRenderer={useCustomRenderer ? this.targetRenderer : undefined}
/>
</div>
</div>
@@ -142,13 +148,39 @@ export class TimezoneSelectExample extends BaseExample<ITimezoneSelectExampleSta
key="default-to-user-timezone-guess"
onChange={this.handleDefaultToUserTimezoneGuessChange}
/>,
<Switch
checked={this.state.useCustomRenderer}
label="Use a custom target renderer"
key="use-custom-renderer"
onChange={this.handleUseCustomRendererChange}
/>,
],
[
this.renderFormatSelect(),
],
];
}

private targetRenderer: ITimezoneSelectTargetRenderer = (targetProps) => {
const { value, displayValue, defaultDisplayValue, placeholder, disabled } = targetProps;
const hasValue = value != null;
const targetTextClasses = classNames({ [Classes.TEXT_MUTED]: !hasValue });
const targetIconClasses = classNames(Classes.ALIGN_RIGHT, { [Classes.TEXT_MUTED]: !hasValue });

return (
<button
className={Classes.INPUT}
disabled={disabled}
style={{ cursor: "text" }}
>
<span className={targetTextClasses}>
{displayValue || defaultDisplayValue || placeholder}
</span>
<Icon iconName="caret-down" className={targetIconClasses} />
</button>
);
}

private renderFormatSelect() {
return (
<label key="format-select" className={Classes.LABEL}>
Original file line number Diff line number Diff line change
@@ -9,10 +9,6 @@
@import "~@blueprintjs/core/src/components/forms/common";
@import "~@blueprintjs/core/src/components/tag/common";

.pt-timezone-select-target {
cursor: text;
}

.pt-timezone-select-popover {
min-width: 370px;
}
Original file line number Diff line number Diff line change
@@ -60,3 +60,5 @@ export class TimezoneExample extends React.PureComponent<{}, ITimezoneExampleSta
@## JavaScript API

@interface ITimezoneSelectProps

@interface ITimezoneSelectTargetProps
133 changes: 93 additions & 40 deletions packages/labs/src/components/timezone-select/timezoneSelect.tsx
Original file line number Diff line number Diff line change
@@ -12,8 +12,8 @@ import * as React from "react";

import {
AbstractComponent,
Button,
Classes as CoreClasses,
Icon,
IconName,
IPopoverProps,
IProps,
@@ -64,6 +64,11 @@ export interface ITimezoneSelectProps extends IProps {
*/
showLocalTimezone?: boolean;

/**
* Custom renderer for the target element.
*/
targetRenderer?: ITimezoneSelectTargetRenderer;

/**
* Format to use when displaying the selected (or default) timezone within the target element.
* @default TimezoneDisplayFormat.OFFSET
@@ -72,6 +77,7 @@ export interface ITimezoneSelectProps extends IProps {

/**
* A space-delimited list of class names to pass along to the target element.
* This prop is ignored when a `targetRenderer` is provided.
*/
targetClassName?: string;

@@ -93,6 +99,31 @@ export interface ITimezoneSelectProps extends IProps {
popoverProps?: Partial<IPopoverProps> & object;
}

export type ITimezoneSelectTargetRenderer = (targetProps: ITimezoneSelectTargetProps) => JSX.Element | null;

export interface ITimezoneSelectTargetProps {
/** The currently selected timezone. */
value: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, blueprint does not use strictNullChecks, so this has no effect in the compiler.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well add it now though, to ease the transition later

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i agree, just calling out that it doesn't mean anything inside blueprint

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the status of #325? would it make sense to try again now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it won't be any easier now! we should look into it for 2.0, a few of us should be able to churn through the migration in a day or two.


/** Initial timezone, when none is selected. */
defaultValue: string | undefined;

/** Display version of the currently selected timezone. */
displayValue: string | undefined;

/** Display version of the default timezone. */
defaultDisplayValue: string | undefined;

/** Placeholder for when no timezone has been selected and there is no default. */
placeholder: string;

/** Whether the target is intended to be non-interactive. */
disabled: boolean;

/** Date to use when determining timezone offsets. */
date: Date;
}

export type TimezoneDisplayFormat = "offset" | "abbreviation" | "name";
export const TimezoneDisplayFormat = {
ABBREVIATION: "abbreviation" as "abbreviation",
@@ -101,7 +132,7 @@ export const TimezoneDisplayFormat = {
};

export interface ITimezoneSelectState {
selectedTimezone?: string;
value?: string;
query?: string;
}

@@ -137,24 +168,21 @@ export class TimezoneSelect extends AbstractComponent<ITimezoneSelectProps, ITim
super(props, context);

this.state = {
selectedTimezone: props.value,
value: props.value,
};

this.updateTimezones(props);
this.updateInitialTimezones(props);
}

public render() {
const { className, disabled, popoverProps, targetClassName } = this.props;
const { selectedTimezone } = this.state;
const { className, disabled, popoverProps } = this.props;
const { query } = this.state;

const finalPopoverProps: Partial<IPopoverProps> & object = {
...popoverProps,
popoverClassName: classNames(popoverProps.popoverClassName, Classes.TIMEZONE_SELECT_POPOVER),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer putting className prop last in arguments to classNames()

};
const isPlaceholder = !selectedTimezone;
const targetTextClasses = classNames({ [CoreClasses.TEXT_MUTED]: isPlaceholder });
const targetIconClasses = classNames(CoreClasses.ALIGN_RIGHT, { [CoreClasses.TEXT_MUTED]: isPlaceholder });

return (
<TypedSelect
@@ -170,13 +198,7 @@ export class TimezoneSelect extends AbstractComponent<ITimezoneSelectProps, ITim
disabled={disabled}
onQueryChange={this.handleQueryChange}
>
<button
className={classNames(Classes.TIMEZONE_SELECT_TARGET, CoreClasses.INPUT, targetClassName)}
disabled={disabled}
>
<span className={targetTextClasses}>{this.getTargetText()}</span>
<Icon iconName="caret-down" className={targetIconClasses} />
</button>
{this.renderTarget()}
</TypedSelect>
);
}
@@ -189,11 +211,55 @@ export class TimezoneSelect extends AbstractComponent<ITimezoneSelectProps, ITim
this.props.showLocalTimezone !== nextProps.showLocalTimezone) {
this.updateInitialTimezones(nextProps);
}
if (this.state.selectedTimezone !== nextProps.value) {
this.setState({ selectedTimezone: nextProps.value });
if (this.state.value !== nextProps.value) {
this.setState({ value: nextProps.value });
}
}

private renderTarget() {
const {
date,
disabled,
targetRenderer,
targetDisplayFormat = TimezoneDisplayFormat.OFFSET,
defaultValue,
defaultToLocalTimezone,
placeholder,
} = this.props;
const { value } = this.state;

const finalDefaultValue = defaultValue || (defaultToLocalTimezone && getLocalTimezone());

const finalPlaceholder = placeholder !== undefined
? placeholder
: formatTimezone(PLACEHOLDER_TIMEZONE, date, targetDisplayFormat);

const finalTargetRenderer = targetRenderer || this.defaultTargetRenderer;
return finalTargetRenderer({
date,
defaultDisplayValue: formatTimezone(finalDefaultValue, date, targetDisplayFormat),
defaultValue: finalDefaultValue,
disabled,
displayValue: formatTimezone(value, date, targetDisplayFormat),
placeholder: finalPlaceholder,
value,
});
}

private defaultTargetRenderer: ITimezoneSelectTargetRenderer = (targetProps) => {
const { targetClassName } = this.props;
const { displayValue, defaultDisplayValue, placeholder, disabled } = targetProps;

return (
<Button
className={classNames(Classes.TIMEZONE_SELECT_TARGET, targetClassName)}
text={displayValue || defaultDisplayValue || placeholder}
rightIconName="caret-down"
disabled={disabled}
/>
);
}

private updateTimezones(props: ITimezoneSelectProps): void {
const timezones = getTimezoneItems(props.date);
this.timezones = timezones;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.timezones = getTimezoneItems(date);

why two lines?

Copy link
Contributor Author

@brieb brieb Sep 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timezones is used on the next line too
so, the ordering dependency felt clearer this way

otherwise, if we inline timezones and swap the two lines, its a bug...

this.timezoneToQueryCandidates = getTimezoneQueryCandidates(this.timezones, date);
this.timezones = getTimezoneItems(date);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough

@@ -204,27 +270,6 @@ export class TimezoneSelect extends AbstractComponent<ITimezoneSelectProps, ITim
this.initialTimezones = getInitialTimezoneItems(props.date, props.showLocalTimezone);
}

private getTargetText(): string {
const {
date,
defaultValue,
defaultToLocalTimezone,
targetDisplayFormat = TimezoneDisplayFormat.OFFSET,
placeholder,
} = this.props;
const { selectedTimezone } = this.state;
const timezone = selectedTimezone || defaultValue || (defaultToLocalTimezone && getLocalTimezone());
const timezoneExists = timezone && moment.tz.zone(timezone) != null;

if (timezoneExists) {
return formatTimezone(timezone, date, targetDisplayFormat);
} else if (placeholder !== undefined) {
return placeholder;
} else {
return formatTimezone(PLACEHOLDER_TIMEZONE, date, targetDisplayFormat);
}
}

private filterTimezones = (query: string, items: ITimezoneItem[]): ITimezoneItem[] => {
const normalizedQuery = normalizeText(query);
return items.filter((item) => {
@@ -255,7 +300,7 @@ export class TimezoneSelect extends AbstractComponent<ITimezoneSelectProps, ITim

private handleItemSelect = (timezone: ITimezoneItem) => {
if (this.props.value === undefined) {
this.setState({ selectedTimezone: timezone.timezone });
this.setState({ value: timezone.timezone });
}
Utils.safeInvoke(this.props.onChange, timezone.timezone);
}
@@ -423,7 +468,15 @@ function getAbbreviation(timezone: string, timestamp: number): string {
return "";
}

function formatTimezone(timezone: string, date: Date, targetDisplayFormat: TimezoneDisplayFormat): string {
function formatTimezone(
timezone: string | undefined,
date: Date,
targetDisplayFormat: TimezoneDisplayFormat,
): string | undefined {
if (!timezone || moment.tz.zone(timezone) == null) {
return undefined;
}

switch (targetDisplayFormat) {
case TimezoneDisplayFormat.ABBREVIATION:
return moment.tz(date.getTime(), timezone).format("z");