Changeset View
Changeset View
Standalone View
Standalone View
web/calendar/thread-picker.react.js
// @flow | // @flow | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; | import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; | ||||
import type { ThreadInfo } from 'lib/types/thread-types'; | import type { ThreadInfo } from 'lib/types/thread-types'; | ||||
import { useSelector } from '../redux/redux-utils'; | import { useSelector } from '../redux/redux-utils'; | ||||
import { htmlTargetFromEvent } from '../vector-utils'; | import { htmlTargetFromEvent } from '../vector-utils'; | ||||
import { LeftPager, RightPager } from '../vectors.react'; | |||||
import css from './thread-picker.css'; | import css from './thread-picker.css'; | ||||
type OptionProps = { | type OptionProps = { | ||||
+threadInfo: ThreadInfo, | +threadInfo: ThreadInfo, | ||||
+createNewEntry: (threadID: string) => void, | +createNewEntry: (threadID: string) => void, | ||||
}; | }; | ||||
function ThreadPickerOption(props: OptionProps) { | function ThreadPickerOption(props: OptionProps) { | ||||
const { threadInfo, createNewEntry } = props; | const { threadInfo, createNewEntry } = props; | ||||
Show All 16 Lines | |||||
type BaseProps = { | type BaseProps = { | ||||
+createNewEntry: (threadID: string) => void, | +createNewEntry: (threadID: string) => void, | ||||
+closePicker: () => void, | +closePicker: () => void, | ||||
}; | }; | ||||
type Props = { | type Props = { | ||||
...BaseProps, | ...BaseProps, | ||||
+onScreenThreadInfos: $ReadOnlyArray<ThreadInfo>, | +onScreenThreadInfos: $ReadOnlyArray<ThreadInfo>, | ||||
}; | }; | ||||
type State = { | |||||
+currentPage: number, | |||||
}; | |||||
class ThreadPicker extends React.PureComponent<Props, State> { | |||||
static pageSize = 5; | |||||
class ThreadPicker extends React.PureComponent<Props> { | |||||
pickerDiv: ?HTMLDivElement; | pickerDiv: ?HTMLDivElement; | ||||
constructor(props: Props) { | constructor(props: Props) { | ||||
super(props); | super(props); | ||||
this.state = { | |||||
currentPage: 0, | |||||
}; | |||||
invariant( | invariant( | ||||
props.onScreenThreadInfos.length > 0, | props.onScreenThreadInfos.length > 0, | ||||
"ThreadPicker can't be open when onScreenThreadInfos is empty", | "ThreadPicker can't be open when onScreenThreadInfos is empty", | ||||
); | ); | ||||
} | } | ||||
componentDidMount() { | componentDidMount() { | ||||
invariant(this.pickerDiv, 'pickerDiv ref unset'); | invariant(this.pickerDiv, 'pickerDiv ref unset'); | ||||
this.pickerDiv.focus(); | this.pickerDiv.focus(); | ||||
} | } | ||||
render() { | render() { | ||||
const length = this.props.onScreenThreadInfos.length; | const length = this.props.onScreenThreadInfos.length; | ||||
invariant( | invariant( | ||||
length > 0, | length > 0, | ||||
"ThreadPicker can't be open when onScreenThreadInfos is empty", | "ThreadPicker can't be open when onScreenThreadInfos is empty", | ||||
); | ); | ||||
const firstIndex = ThreadPicker.pageSize * this.state.currentPage; | |||||
const secondIndex = Math.min( | |||||
ThreadPicker.pageSize * (this.state.currentPage + 1), | |||||
length, | |||||
); | |||||
let pager = null; | |||||
if (length > ThreadPicker.pageSize) { | |||||
let leftPager = <LeftPager className={css.pagerIcon} />; | |||||
if (this.state.currentPage > 0) { | |||||
leftPager = ( | |||||
<a | |||||
href="#" | |||||
className={css.pagerButton} | |||||
onClick={this.onBackPagerClick} | |||||
> | |||||
{leftPager} | |||||
</a> | |||||
); | |||||
} | |||||
let rightPager = <RightPager className={css.pagerIcon} />; | |||||
if (ThreadPicker.pageSize * (this.state.currentPage + 1) < length) { | |||||
rightPager = ( | |||||
<a | |||||
href="#" | |||||
className={css.pagerButton} | |||||
onClick={this.onNextPagerClick} | |||||
> | |||||
{rightPager} | |||||
</a> | |||||
); | |||||
} | |||||
pager = ( | |||||
<div className={css.pagerContainer} key="pager"> | |||||
<div className={css.pager}> | |||||
{leftPager} | |||||
<span className={css.pagerStatus}> | |||||
{`${firstIndex + 1}–${secondIndex} of ${length}`} | |||||
</span> | |||||
{rightPager} | |||||
</div> | |||||
</div> | |||||
); | |||||
} | |||||
const options = this.props.onScreenThreadInfos | const options = this.props.onScreenThreadInfos.map(threadInfo => ( | ||||
.slice(firstIndex, secondIndex) | |||||
.map(threadInfo => ( | |||||
<ThreadPickerOption | <ThreadPickerOption | ||||
threadInfo={threadInfo} | threadInfo={threadInfo} | ||||
createNewEntry={this.props.createNewEntry} | createNewEntry={this.props.createNewEntry} | ||||
key={threadInfo.id} | key={threadInfo.id} | ||||
/> | /> | ||||
)); | )); | ||||
return ( | return ( | ||||
<div | <div | ||||
className={css.container} | className={css.container} | ||||
tabIndex="0" | tabIndex="0" | ||||
onBlur={this.props.closePicker} | onBlur={this.props.closePicker} | ||||
onKeyDown={this.onPickerKeyDown} | onKeyDown={this.onPickerKeyDown} | ||||
onMouseDown={this.onMouseDown} | onMouseDown={this.onMouseDown} | ||||
ref={this.pickerDivRef} | ref={this.pickerDivRef} | ||||
> | > | ||||
{options} | {options} | ||||
{pager} | |||||
</div> | </div> | ||||
); | ); | ||||
} | } | ||||
pickerDivRef = (pickerDiv: ?HTMLDivElement) => { | pickerDivRef = (pickerDiv: ?HTMLDivElement) => { | ||||
this.pickerDiv = pickerDiv; | this.pickerDiv = pickerDiv; | ||||
}; | }; | ||||
onPickerKeyDown = (event: SyntheticKeyboardEvent<HTMLDivElement>) => { | onPickerKeyDown = (event: SyntheticKeyboardEvent<HTMLDivElement>) => { | ||||
if (event.keyCode === 27) { | if (event.keyCode === 27) { | ||||
// Esc | // Esc | ||||
this.props.closePicker(); | this.props.closePicker(); | ||||
} | } | ||||
}; | }; | ||||
onMouseDown = (event: SyntheticEvent<HTMLDivElement>) => { | onMouseDown = (event: SyntheticEvent<HTMLDivElement>) => { | ||||
const target = htmlTargetFromEvent(event); | const target = htmlTargetFromEvent(event); | ||||
invariant(this.pickerDiv, 'pickerDiv ref not set'); | invariant(this.pickerDiv, 'pickerDiv ref not set'); | ||||
if (this.pickerDiv.contains(target)) { | if (this.pickerDiv.contains(target)) { | ||||
// This prevents onBlur from firing | // This prevents onBlur from firing | ||||
event.preventDefault(); | event.preventDefault(); | ||||
} | } | ||||
}; | }; | ||||
onBackPagerClick = (event: SyntheticEvent<HTMLAnchorElement>) => { | |||||
event.preventDefault(); | |||||
this.setState(prevState => { | |||||
invariant(prevState.currentPage > 0, "can't go back from 0"); | |||||
return { currentPage: prevState.currentPage - 1 }; | |||||
}); | |||||
}; | |||||
onNextPagerClick = (event: SyntheticEvent<HTMLAnchorElement>) => { | |||||
event.preventDefault(); | |||||
this.setState((prevState, props) => { | |||||
invariant( | |||||
ThreadPicker.pageSize * (prevState.currentPage + 1) < | |||||
props.onScreenThreadInfos.length, | |||||
'page is too high', | |||||
); | |||||
return { currentPage: prevState.currentPage + 1 }; | |||||
}); | |||||
}; | |||||
} | } | ||||
const ConnectedThreadPicker: React.ComponentType<BaseProps> = React.memo<BaseProps>( | const ConnectedThreadPicker: React.ComponentType<BaseProps> = React.memo<BaseProps>( | ||||
function ConnectedThreadPicker(props) { | function ConnectedThreadPicker(props) { | ||||
const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); | const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); | ||||
return ( | return ( | ||||
<ThreadPicker {...props} onScreenThreadInfos={onScreenThreadInfos} /> | <ThreadPicker {...props} onScreenThreadInfos={onScreenThreadInfos} /> | ||||
); | ); | ||||
}, | }, | ||||
); | ); | ||||
export default ConnectedThreadPicker; | export default ConnectedThreadPicker; |