Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import { Colors, Icon, Intent, Tooltip } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { getTimezoneMetadata } from "@blueprintjs/timezone";
import React from "react";

export interface ICustomTimezonePickerTargetProps {
timezone: string;
}

export interface ICustomTimezonePickerTargetState {
isHovering: boolean;
}

// This is a little component that isn't meant to see the light of day outside
// the TimezonePickerExample. Coding style is thus a *little* scrappy.
export class CustomTimezonePickerTarget extends React.PureComponent<
ICustomTimezonePickerTargetProps,
ICustomTimezonePickerTargetState
> {
public state: ICustomTimezonePickerTargetState = {
isHovering: false,
};

public render() {
const { isHovering } = this.state;
return (
<Tooltip content={this.getTooltipContent()}>
<div
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={{ cursor: "pointer" }}
>
<Icon icon={IconNames.GLOBE} intent={isHovering ? Intent.PRIMARY : undefined} />
&nbsp;
<Icon color={Colors.GRAY1} icon={IconNames.CARET_DOWN} />
</div>
</Tooltip>
);
}

private getTooltipContent() {
const { timezone } = this.props;

if (timezone == null || timezone.length === 0) {
return "No selection";
}

const { abbreviation, offsetAsString } = getTimezoneMetadata(timezone);

return (
<span>
GMT {offsetAsString}
<span style={{ color: Colors.GRAY4 }}>{abbreviation ? ` (${abbreviation})` : ""}</span>
</span>
);
}

private handleMouseEnter = () => this.setState({ isHovering: true });
private handleMouseLeave = () => this.setState({ isHovering: false });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

export * from "./customTimezoneTarget";
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

import * as React from "react";

import { H5, Radio, RadioGroup, Switch } from "@blueprintjs/core";
import { H5, Position, Radio, RadioGroup, Switch } from "@blueprintjs/core";
import { Example, handleBooleanChange, handleStringChange, IExampleProps } from "@blueprintjs/docs-theme";
import { TimezoneDisplayFormat, TimezonePicker } from "@blueprintjs/timezone";
import { CustomTimezonePickerTarget } from "./components";

export interface ITimezonePickerExampleState {
disabled: boolean;
showCustomTarget: boolean;
showLocalTimezone: boolean;
targetDisplayFormat: TimezoneDisplayFormat;
timezone: string;
Expand All @@ -20,25 +22,28 @@ export interface ITimezonePickerExampleState {
export class TimezonePickerExample extends React.PureComponent<IExampleProps, ITimezonePickerExampleState> {
public state: ITimezonePickerExampleState = {
disabled: false,
showCustomTarget: false,
showLocalTimezone: true,
targetDisplayFormat: TimezoneDisplayFormat.COMPOSITE,
timezone: "",
};

private handleDisabledChange = handleBooleanChange(disabled => this.setState({ disabled }));
private handleShowLocalChange = handleBooleanChange(showLocalTimezone => this.setState({ showLocalTimezone }));
private handleCustomChildChange = handleBooleanChange(showCustomTarget => this.setState({ showCustomTarget }));
private handleFormatChange = handleStringChange((targetDisplayFormat: TimezoneDisplayFormat) =>
this.setState({ targetDisplayFormat }),
);

public render() {
const { timezone, targetDisplayFormat, disabled, showLocalTimezone } = this.state;
const { timezone, targetDisplayFormat, disabled, showCustomTarget, showLocalTimezone } = this.state;

const options = (
<>
<H5>Props</H5>
<Switch checked={showLocalTimezone} label="Show local timezone" onChange={this.handleShowLocalChange} />
<Switch checked={disabled} label="Disabled" onChange={this.handleDisabledChange} />
<Switch checked={showCustomTarget} label="Custom target" onChange={this.handleCustomChildChange} />
<RadioGroup
label="Display format"
onChange={this.handleFormatChange}
Expand All @@ -58,12 +63,19 @@ export class TimezonePickerExample extends React.PureComponent<IExampleProps, IT
value={timezone}
onChange={this.handleTimezoneChange}
valueDisplayFormat={targetDisplayFormat}
popoverProps={{ position: Position.BOTTOM }}
showLocalTimezone={showLocalTimezone}
disabled={disabled}
/>
>
{showCustomTarget ? this.renderCustomTarget() : undefined}
</TimezonePicker>
</Example>
);
}

private renderCustomTarget() {
return <CustomTimezonePickerTarget timezone={this.state.timezone} />;
}

private handleTimezoneChange = (timezone: string) => this.setState({ timezone });
}
12 changes: 12 additions & 0 deletions packages/timezone/src/common/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

const ns = "[Blueprint]";

export const TIMEZONE_PICKER_WARN_TOO_MANY_CHILDREN =
ns +
` <TimezonePicker> supports up to one child; additional children are ignored.` +
` If a child is present, it it will be rendered in place of the default button target.`;
1 change: 1 addition & 0 deletions packages/timezone/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
* Licensed under the terms of the LICENSE file distributed with this project.
*/

export * from "./timezone-picker/timezoneMetadata";
export * from "./timezone-picker/timezonePicker";
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ using the most populous location for each offset.
Moment Timezone uses a similar [heuristic for guessing](http://momentjs.com/timezone/docs/#/using-timezones/guessing-user-timezone/)
the user's timezone.

By default, the component will show a clickable button target,
which will display the selected timezone formatted according to `valueDisplayFormat`.
The button can also be managed via `disabled`, `placeholder`, and more generally via `buttonProps`.
You can show a custom element instead the default button by passing a child; in this case,
all button-specific props will be ignored:

```tsx
<TimezonePicker value={...} onChange={...}>
<Icon icon="globe" />
</TimezontPicker>
```

<div class="@ns-callout @ns-intent-warning @ns-icon-warning-sign">
<h4 class="@ns-heading">Local timezone detection</h4>
We detect the local timezone when the `showLocalTimezone` prop is enabled and cannot guarantee correctness in all browsers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ITimezoneMetadata {
population: number | undefined;
}

export function getTimezoneMetadata(timezone: string, date: Date): ITimezoneMetadata {
export function getTimezoneMetadata(timezone: string, date: Date = new Date()): ITimezoneMetadata {
const timestamp = date.getTime();
const zone = moment.tz.zone(timezone);
const zonedDate = moment.tz(timestamp, timezone);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "@blueprintjs/core";
import { ItemListPredicate, ItemRenderer, Select } from "@blueprintjs/select";
import * as Classes from "../../common/classes";
import * as Errors from "../../common/errors";
import { formatTimezone, TimezoneDisplayFormat } from "./timezoneDisplayFormat";
import { getInitialTimezoneItems, getTimezoneItems, ITimezoneItem } from "./timezoneItems";

Expand All @@ -39,12 +40,6 @@ export interface ITimezonePickerProps extends IProps {
*/
onChange: (timezone: string) => void;

/**
* This component does not support children.
* Use `value`, `valueDisplayFormat` and `buttonProps` to customize the button child.
*/
children?: never;

/**
* The date to use when formatting timezone offsets.
* An offset date is necessary to account for DST, but typically the default value of `now` will be sufficient.
Expand All @@ -54,6 +49,7 @@ export interface ITimezonePickerProps extends IProps {

/**
* Whether this component is non-interactive.
* This prop will be ignored if a custom child is provided.
* @default false
*/
disabled?: boolean;
Expand All @@ -66,17 +62,22 @@ export interface ITimezonePickerProps extends IProps {

/**
* Format to use when displaying the selected (or default) timezone within the target element.
* This prop will be ignored if a custom child is provided.
* @default TimezoneDisplayFormat.OFFSET
*/
valueDisplayFormat?: TimezoneDisplayFormat;

/**
* Text to show when no timezone has been selected (`value === undefined`).
* This prop will be ignored if a custom child is provided.
* @default "Select timezone..."
*/
placeholder?: string;

/** Props to spread to the target `Button`. */
/**
* Props to spread to the target `Button`.
* This prop will be ignored if a custom child is provided.
*/
buttonProps?: Partial<IButtonProps>;

/**
Expand Down Expand Up @@ -124,7 +125,7 @@ export class TimezonePicker extends AbstractPureComponent<ITimezonePickerProps,
}

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

const finalInputProps: IInputGroupProps & HTMLInputProps = {
Expand All @@ -151,7 +152,7 @@ export class TimezonePicker extends AbstractPureComponent<ITimezonePickerProps,
disabled={disabled}
onQueryChange={this.handleQueryChange}
>
{this.renderButton()}
{children != null ? children : this.renderButton()}
</TypedSelect>
);
}
Expand All @@ -167,6 +168,13 @@ export class TimezonePicker extends AbstractPureComponent<ITimezonePickerProps,
}
}

protected validateProps(props: IPopoverProps & { children?: React.ReactNode }) {
const childrenCount = React.Children.count(props.children);
if (childrenCount > 1) {
console.warn(Errors.TIMEZONE_PICKER_WARN_TOO_MANY_CHILDREN);
}
}

private renderButton() {
const { buttonProps = {}, date, disabled, placeholder, value, valueDisplayFormat } = this.props;
const buttonContent = value ? (
Expand Down
12 changes: 12 additions & 0 deletions packages/timezone/test/timezonePickerTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ describe("<TimezonePicker>", () => {
}
});

it("renders a custom target via <children>", () => {
const timezonePicker = shallow(
<TimezonePicker {...DEFAULT_PROPS}>
<span className="foo">Hello world</span>
</TimezonePicker>,
);
const button = timezonePicker.find(Button);
const span = timezonePicker.find(".foo");
assert.lengthOf(button, 0, "expected no button");
assert.lengthOf(span, 1, "expected custom target with class '.foo'");
});

function findSelect(timezonePicker: TimezonePickerShallowWrapper) {
return timezonePicker.find<ISelectProps<ITimezoneItem>>(Select);
}
Expand Down