Skip to content
This repository has been archived by the owner on Nov 6, 2018. It is now read-only.

Commit

Permalink
feat: support configurable keybindings for CommandList/PopoverButton
Browse files Browse the repository at this point in the history
  • Loading branch information
sqs committed Oct 20, 2018
1 parent daea499 commit 19df39f
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 59 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
},
"sideEffects": false,
"dependencies": {
"@shopify/react-shortcuts": "^1.0.2",
"bootstrap": "^4.1.3",
"lodash-es": "^4.17.10",
"react": "^16.4.2",
Expand Down
8 changes: 5 additions & 3 deletions src/app/CommandList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Shortcut } from '@shopify/react-shortcuts'
import { isArray, sortBy, uniq } from 'lodash-es'
import * as React from 'react'
import { Subscription } from 'rxjs'
Expand Down Expand Up @@ -251,7 +252,9 @@ export function filterAndRankItems(
}

export class CommandListPopoverButton<S extends ConfigurationSubject, C extends Settings> extends React.PureComponent<
Props<S, C>,
Props<S, C> & {
toggleVisibilityKeybinding?: Pick<Shortcut['props'], 'held' | 'ordered'>[]
},
{ hideOnChange?: any }
> {
public state: { hideOnChange?: any } = {}
Expand All @@ -262,8 +265,7 @@ export class CommandListPopoverButton<S extends ConfigurationSubject, C extends
caretIcon={this.props.extensions.context.icons.CaretDown}
popoverClassName="rounded"
placement="auto-end"
globalKeyBinding={Key.F1}
globalKeyBindingActiveInInputs={true}
toggleVisibilityKeybinding={this.props.toggleVisibilityKeybinding}
hideOnChange={this.state.hideOnChange}
popoverElement={<CommandList {...this.props} onSelect={this.dismissPopover} />}
>
Expand Down
1 change: 0 additions & 1 deletion src/app/ExtensionStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ export class ExtensionStatusPopover<S extends ConfigurationSubject, C extends Se
<PopoverButton
caretIcon={this.props.caretIcon}
placement="auto-end"
globalKeyBinding="X"
hideOnChange={this.props.location}
popoverElement={<ExtensionStatus {...this.props} />}
>
Expand Down
69 changes: 14 additions & 55 deletions src/ui/generic/PopoverButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Shortcut } from '@shopify/react-shortcuts'
import * as React from 'react'
import Popover, { PopoverProps } from 'reactstrap/lib/Popover'
import { Subscription } from 'rxjs'
import { Key } from 'ts-key-enum'
import { LinkOrSpan } from './LinkOrSpan'

interface Props {
Expand Down Expand Up @@ -35,17 +34,10 @@ interface Props {
*/
hideOnChange?: any

/** If set, pressing this key toggles the popover's open state. */
globalKeyBinding?: string

/**
* Whether the global keybinding should be active even when the user has an input element focused. This should
* only be used for keybindings that would not conflict with routine user input. For example, use it for a
* keybinding to F1, which will ensure that F1 doesn't open Chrome help when the user is focused in an input
* field.
*
* A keybinding that toggles the visibility of this element.
*/
globalKeyBindingActiveInInputs?: boolean
toggleVisibilityKeybinding?: Pick<Shortcut['props'], 'held' | 'ordered'>[]

/** Popover placement. */
placement?: PopoverProps['placement']
Expand All @@ -65,39 +57,30 @@ interface State {
export class PopoverButton extends React.PureComponent<Props, State> {
public state: State = { open: false }

private subscriptions = new Subscription()

private rootRef: HTMLElement | null = null

public componentDidMount(): void {
window.addEventListener('keydown', this.onGlobalKeyDown)
}

public componentWillReceiveProps(props: Props): void {
if (props.hideOnChange !== this.props.hideOnChange) {
this.setState({ open: false })
}
}

public componentWillUnmount(): void {
this.subscriptions.unsubscribe()
window.removeEventListener('keydown', this.onGlobalKeyDown)
}

public render(): React.ReactFragment {
const isOpen = this.state.open || this.props.open

const popoverAnchor = this.rootRef && (
<Popover
placement={this.props.placement || 'auto-start'}
isOpen={isOpen}
toggle={this.onPopoverVisibilityToggle}
toggle={this.toggleVisibility}
target={this.rootRef}
className={`popover-button2__popover ${this.props.popoverClassName || ''}`}
>
{isOpen && <Shortcut ordered={['Escape']} onMatch={this.toggleVisibility} ignoreInput={true} />}
{this.props.popoverElement}
</Popover>
)

return (
<div
className={`popover-button2 ${isOpen ? 'popover-button2--open' : ''} ${this.props.className || ''} ${
Expand All @@ -112,7 +95,7 @@ export class PopoverButton extends React.PureComponent<Props, State> {
: 'popover-button2__container'
}
to={this.props.link}
onClick={this.props.link ? this.onClickLink : this.onPopoverVisibilityToggle}
onClick={this.props.link ? this.onClickLink : this.toggleVisibility}
>
{this.props.children}{' '}
{!this.props.link && <this.props.caretIcon className="icon-inline popover-button2__icon" />}
Expand All @@ -121,13 +104,18 @@ export class PopoverButton extends React.PureComponent<Props, State> {
<div className="popover-button2__anchor">
<this.props.caretIcon
className="icon-inline popover-button2__icon popover-button2__icon--outside"
onClick={this.onPopoverVisibilityToggle}
onClick={this.toggleVisibility}
/>
{popoverAnchor}
</div>
) : (
popoverAnchor
)}
{this.props.toggleVisibilityKeybinding &&
!isOpen &&
this.props.toggleVisibilityKeybinding.map((keybinding, i) => (
<Shortcut key={i} {...keybinding} onMatch={this.toggleVisibility} />
))}
</div>
)
}
Expand All @@ -136,36 +124,7 @@ export class PopoverButton extends React.PureComponent<Props, State> {
this.setState({ open: false })
}

private onGlobalKeyDown = (event: KeyboardEvent) => {
if (event.key === Key.Escape) {
// Always close the popover when Escape is pressed, even when in an input.
this.setState({ open: false })
return
}

// Otherwise don't interfere with user keyboard input.
if (!this.props.globalKeyBindingActiveInInputs && isInputLike(event.target as HTMLElement)) {
return
}

if (
this.props.globalKeyBinding &&
!event.ctrlKey &&
!event.altKey &&
!event.metaKey &&
event.key === this.props.globalKeyBinding
) {
event.preventDefault()
this.onPopoverVisibilityToggle()
}
}

private setRootRef = (e: HTMLElement | null) => (this.rootRef = e)

private onPopoverVisibilityToggle = () => this.setState(prevState => ({ open: !prevState.open }))
}

/** Reports whether elem is a field that accepts user keyboard input. */
function isInputLike(elem: HTMLElement): boolean {
return elem.tagName === 'INPUT' || elem.tagName === 'TEXTAREA' || elem.tagName === 'SELECT'
private toggleVisibility = () => this.setState(prevState => ({ open: !prevState.open }))
}
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,13 @@
into-stream "^3.1.0"
lodash "^4.17.4"

"@shopify/react-shortcuts@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@shopify/react-shortcuts/-/react-shortcuts-1.0.2.tgz#01d9f6979ff5cce1d70474e1452fb750f7bbe819"
integrity sha512-KEQd0IKSE/IRmPn5MfboeBxJzh5KGb7OLvpfoxjhcbijj/YsU117H62PKsjBuPMc0fi0f8jTIeg938w2y0U5xg==
dependencies:
prop-types "^15.6.2"

"@sourcegraph/prettierrc@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@sourcegraph/prettierrc/-/prettierrc-2.2.0.tgz#af4a6fcd465b0a39a07ffbd8f2d3414d01e603e8"
Expand Down

0 comments on commit 19df39f

Please sign in to comment.