-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
[Labs] Timezone Picker #1568
Changes from 76 commits
356743c
66c6edc
4a4aef2
b6d8302
456303a
7de36a1
67ca676
dad05d1
75dd866
a1ee5cb
5739478
f61607c
185a671
4bb8e12
50f8e78
0b0237c
25176ab
c08169f
3e4a482
8823063
4c2def9
e5dc667
ba10a57
86ade6b
cbf7ee3
72c143a
cd0f2ad
8671cc0
d0f1e4f
f6f8b93
c7b76d2
1946977
b359c1a
020d02f
0cc560c
44e70d1
4d55db2
f237bdc
1054cea
f6858a7
9400904
d2f7f78
e10b3d3
2114c6a
d9d00bc
f652ffe
dc85ca6
b586639
add9e49
900c07e
bbf0ff1
7fc4720
8782648
15947e6
5afc0d7
ef932e9
cd20736
b23c37c
4c8cb85
4ca31e4
7b96c8a
bf15ba5
11631b8
76cad64
bd9ef19
b2b4788
ff49db9
d427502
ab8d5d5
b8e6c32
68ce37c
7f5df58
b0be6b6
ffbb9d3
956b612
76fccc2
c208968
56bf58e
d5ab205
3453ce0
b14a7c2
248f670
2b37613
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* Copyright 2017 Palantir Technologies, Inc. All rights reserved. | ||
* Licensed under the BSD-3 License as modified (the “License”); you may obtain a copy | ||
* of the license at https://github.com/palantir/blueprint/blob/master/LICENSE | ||
* and https://github.com/palantir/blueprint/blob/master/PATENTS | ||
*/ | ||
|
||
import * as React from "react"; | ||
|
||
import { Radio, RadioGroup, Switch } from "@blueprintjs/core"; | ||
import { BaseExample, handleBooleanChange, handleStringChange } from "@blueprintjs/docs"; | ||
|
||
import { TimezoneDisplayFormat, TimezonePicker } from "../src"; | ||
|
||
export interface ITimezonePickerExampleState { | ||
date?: Date; | ||
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, | ||
timezone: "", | ||
}; | ||
|
||
private handleDisabledChange = handleBooleanChange(disabled => this.setState({ disabled })); | ||
private handleShowLocalTimezoneChange = handleBooleanChange(showLocalTimezone => | ||
this.setState({ showLocalTimezone }), | ||
); | ||
private handleFormatChange = handleStringChange((targetDisplayFormat: TimezoneDisplayFormat) => | ||
this.setState({ targetDisplayFormat }), | ||
); | ||
|
||
protected renderExample() { | ||
const { date, timezone, targetDisplayFormat, disabled, showLocalTimezone } = this.state; | ||
|
||
return ( | ||
<TimezonePicker | ||
date={date} | ||
value={timezone} | ||
onChange={this.handleTimezoneChange} | ||
valueDisplayFormat={targetDisplayFormat} | ||
showLocalTimezone={showLocalTimezone} | ||
disabled={disabled} | ||
/> | ||
); | ||
} | ||
|
||
protected renderOptions() { | ||
return [ | ||
[ | ||
<Switch | ||
checked={this.state.showLocalTimezone} | ||
label="Show local timezone in initial list" | ||
key="show-local-timezone" | ||
onChange={this.handleShowLocalTimezoneChange} | ||
/>, | ||
<Switch | ||
checked={this.state.disabled} | ||
label="Disabled" | ||
key="disabled" | ||
onChange={this.handleDisabledChange} | ||
/>, | ||
], | ||
[this.renderDisplayFormatOption()], | ||
]; | ||
} | ||
|
||
private renderDisplayFormatOption() { | ||
return ( | ||
<RadioGroup | ||
key="display-format" | ||
label="Display format" | ||
onChange={this.handleFormatChange} | ||
selectedValue={this.state.targetDisplayFormat} | ||
> | ||
<Radio label="Abbreviation" value={TimezoneDisplayFormat.ABBREVIATION} /> | ||
<Radio label="Name" value={TimezoneDisplayFormat.NAME} /> | ||
<Radio label="Offset" value={TimezoneDisplayFormat.OFFSET} /> | ||
<Radio label="Composite" value={TimezoneDisplayFormat.COMPOSITE} /> | ||
</RadioGroup> | ||
); | ||
} | ||
|
||
private handleTimezoneChange = (timezone: string) => { | ||
this.setState({ timezone }); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,6 +79,12 @@ export interface ISelectProps<T> extends IListItemsProps<T> { | |
* @default false | ||
*/ | ||
resetOnClose?: boolean; | ||
|
||
/** | ||
* Callback invoked when the query value changes, | ||
* through user input or when the filter is reset. | ||
*/ | ||
onQueryChange?: (query: string) => void; | ||
} | ||
|
||
export interface ISelectItemRendererProps<T> { | ||
|
@@ -115,15 +121,20 @@ export class Select<T> extends React.Component<ISelectProps<T>, ISelectState<T>> | |
return Select as new () => Select<T>; | ||
} | ||
|
||
public state: ISelectState<T> = { isOpen: false, query: "" }; | ||
|
||
private TypedQueryList = QueryList.ofType<T>(); | ||
private list: QueryList<T>; | ||
private refHandlers = { | ||
queryList: (ref: QueryList<T>) => (this.list = ref), | ||
}; | ||
private previousFocusedElement: HTMLElement; | ||
|
||
constructor(props?: ISelectProps<T>, context?: any) { | ||
super(props, context); | ||
|
||
const query = props && props.inputProps && props.inputProps.value !== undefined ? props.inputProps.value : ""; | ||
this.state = { isOpen: false, query }; | ||
} | ||
|
||
public render() { | ||
// omit props specific to this component, spread the rest. | ||
const { | ||
|
@@ -149,6 +160,13 @@ export class Select<T> extends React.Component<ISelectProps<T>, ISelectState<T>> | |
); | ||
} | ||
|
||
public componentWillReceiveProps(nextProps: ISelectProps<T>) { | ||
const { inputProps: nextInputProps = {} } = nextProps; | ||
if (nextInputProps.value !== undefined && this.state.query !== nextInputProps.value) { | ||
this.setState({ query: nextInputProps.value }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! this seems like a valuable fix and could use a unit test (since in fact you could go so far as to pull this change to a separate PR focused solely on supporting controlled input value. though in truth we probably want an obvious There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going to start on this in a separate PR There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
} | ||
|
||
public componentDidUpdate(_prevProps: ISelectProps<T>, prevState: ISelectState<T>) { | ||
if (this.state.isOpen && !prevState.isOpen && this.list != null) { | ||
this.list.scrollActiveItemIntoView(); | ||
|
@@ -291,10 +309,17 @@ export class Select<T> extends React.Component<ISelectProps<T>, ISelectState<T>> | |
}; | ||
|
||
private handleQueryChange = (event: React.FormEvent<HTMLInputElement>) => { | ||
const { inputProps = {} } = this.props; | ||
this.setState({ query: event.currentTarget.value }); | ||
const { inputProps = {}, onQueryChange } = this.props; | ||
const query = event.currentTarget.value; | ||
this.setState({ query }); | ||
Utils.safeInvoke(inputProps.onChange, event); | ||
Utils.safeInvoke(onQueryChange, query); | ||
}; | ||
|
||
private resetQuery = () => this.setState({ activeItem: this.props.items[0], query: "" }); | ||
private resetQuery = () => { | ||
const { items, onQueryChange } = this.props; | ||
const query = ""; | ||
this.setState({ activeItem: items[0], query }); | ||
Utils.safeInvoke(onQueryChange, query); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* | ||
* Copyright 2017 Palantir Technologies, Inc. All rights reserved. | ||
* Licensed under the BSD-3 License as modified (the “License”); you may obtain a copy | ||
* of the license at https://github.com/palantir/blueprint/blob/master/LICENSE | ||
* and https://github.com/palantir/blueprint/blob/master/PATENTS | ||
*/ | ||
|
||
@import "~@blueprintjs/core/src/common/variables"; | ||
@import "~@blueprintjs/core/src/components/forms/common"; | ||
@import "~@blueprintjs/core/src/components/tag/common"; | ||
|
||
.pt-timezone-picker-popover { | ||
min-width: 370px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
@# TimezonePicker | ||
|
||
`TimezonePicker` allows the user to select from a list of timezones. | ||
|
||
@reactExample TimezonePickerExample | ||
|
||
@## JavaScript API | ||
|
||
[Moment Timezone](http://momentjs.com/timezone/) is used internally | ||
for the list of available timezones and timezone metadata. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this line belongs at the end of the section, not the beginning: it's an implementation detail, not relevant to usage. |
||
|
||
This component can be used in controlled or uncontrolled mode. | ||
Use the `onChange` prop to listen for changes to the selected timezone. | ||
You can control the selected timezone by setting the `value` prop. | ||
Or, use the component in uncontrolled mode and specify an initial timezone by setting `defaultValue`. | ||
|
||
The `date` prop is used to determine the timezone offsets. | ||
This is because a timezone usually has more than one offset from UTC due to daylight saving time. | ||
See [here](https://momentjs.com/guides/#/lib-concepts/timezone-offset/) | ||
and [here](http://momentjs.com/timezone/docs/#/using-timezones/parsing-ambiguous-inputs/) | ||
for more information. | ||
|
||
The initial list (shown before filtering) shows one timezone per timezone offset, | ||
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. | ||
|
||
<div class="pt-callout pt-intent-warning pt-icon-warning-sign"> | ||
<h5>Local timezone detection</h5> | ||
We detect the local timezone when the `showLocalTimezone` prop is used. | ||
We cannot guarantee that we'll get the correct local timezone in all browsers. | ||
In supported browsers, the [i18n API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/resolvedOptions) is used. | ||
In other browsers, `Date` methods and a population heuristic are used. | ||
See [Moment Timezone's documentation](https://momentjs.com/timezone/docs/#/using-timezones/guessing-user-timezone/) | ||
for more information. | ||
</div> | ||
|
||
```tsx | ||
import { TimezonePicker } from "@blueprintjs/labs"; | ||
|
||
export interface ITimezoneExampleState { | ||
timezone: string; | ||
} | ||
|
||
export class TimezoneExample extends React.PureComponent<{}, ITimezoneExampleState> { | ||
public state: ITimezoneExampleState = { | ||
timezone: "", | ||
}; | ||
|
||
public render() { | ||
return ( | ||
<TimezonePicker | ||
value={this.state.timezone} | ||
onChange={this.handleTimezoneChange} | ||
/> | ||
); | ||
} | ||
|
||
private handleTimezoneChange = (timezone: string) => this.setState({ timezone }); | ||
} | ||
``` | ||
|
||
@interface ITimezonePickerProps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please always wrap params in parens. this should be a lint failure...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, this is the prettier standard