add {cmd,ctrl} + l for wunderbar focus #2003

Merged
neb-b merged 3 commits from wunderbar-shortcut into master 2018-10-09 22:25:54 +02:00
8 changed files with 192 additions and 401 deletions

View file

@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Added
* Allow typing of encryption password without clicking entry box ([#1977](https://github.com/lbryio/lbry-desktop/pull/1977))
* Focus on search bar with {cmd,ctrl} + "l" ([#2003](https://github.com/lbryio/lbry-desktop/pull/2003))
### Changed
* Make tooltip smarter ([#1979](https://github.com/lbryio/lbry-desktop/pull/1979))
* Change channel pages to have 48 items instead of 10 ([#2002](https://github.com/lbryio/lbry-desktop/pull/2002))

View file

@ -1,5 +1,5 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'component/button';
import * as icons from 'constants/icons';
@ -7,30 +7,29 @@ let scriptLoading = false;
let scriptLoaded = false;
let scriptDidError = false;
type Props = {
disabled: boolean,
label: ?string,
// =====================================================
// Required by stripe
// see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom
// =====================================================
// Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname
stripeKey: string,
// The callback to invoke when the Checkout process is complete.
// function(token)
// token is the token object created.
// token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user.
token: string,
};
class CardVerify extends React.Component {
static propTypes = {
disabled: PropTypes.bool,
label: PropTypes.string,
// =====================================================
// Required by stripe
// see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom
// =====================================================
// Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname
stripeKey: PropTypes.string.isRequired,
// The callback to invoke when the Checkout process is complete.
// function(token)
// token is the token object created.
// token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user.
token: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {

View file

@ -1,5 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import FormField from './view';
export default connect(null, null, null, { withRef: true })(FormField);

View file

@ -1,200 +0,0 @@
// This file is going to die
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';
import FileSelector from 'component/common/file-selector';
import SimpleMDE from 'react-simplemde-editor';
import { formFieldNestedLabelTypes, formFieldId } from 'component/common/form';
import style from 'react-simplemde-editor/dist/simplemde.min.css';
const formFieldFileSelectorTypes = ['file', 'directory'];
class FormField extends React.PureComponent {
static propTypes = {
type: PropTypes.string.isRequired,
prefix: PropTypes.string,
postfix: PropTypes.string,
hasError: PropTypes.bool,
trim: PropTypes.bool,
regexp: PropTypes.oneOfType([PropTypes.instanceOf(RegExp), PropTypes.string]),
};
static defaultProps = {
trim: false,
};
constructor(props) {
super(props);
this._fieldRequiredText = __('This field is required');
this._type = null;
this._element = null;
this._extraElementProps = {};
this.state = {
isError: null,
errorMessage: null,
};
}
componentWillMount() {
if (['text', 'number', 'radio', 'checkbox'].includes(this.props.type)) {
this._element = 'input';
this._type = this.props.type;
} else if (this.props.type == 'text-number') {
this._element = 'input';
this._type = 'text';
} else if (this.props.type == 'SimpleMDE') {
this._element = SimpleMDE;
this._type = 'textarea';
this._extraElementProps.options = {
placeholder: this.props.placeholder,
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
};
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = 'input';
this._type = 'hidden';
} else {
// Non <input> field, e.g. <select>, <textarea>
this._element = this.props.type;
}
}
componentDidMount() {
/**
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
* https://github.com/facebook/react/issues/3468
*/
if (this.props.type == 'directory') {
this.refs.field.webkitdirectory = true;
}
}
handleFileChosen(path) {
this.refs.field.value = path;
if (this.props.onChange) {
// Updating inputs programmatically doesn't generate an event, so we have to make our own
const event = new Event('change', { bubbles: true });
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
this.props.onChange(event);
}
}
showError(text) {
this.setState({
isError: true,
errorMessage: text,
});
}
clearError() {
this.setState({
isError: false,
errorMessage: '',
});
}
getValue() {
if (this.props.type == 'checkbox') {
return this.refs.field.checked;
} else if (this.props.type == 'SimpleMDE') {
return this.refs.field.simplemde.value();
}
return this.props.trim ? this.refs.field.value.trim() : this.refs.field.value;
}
getSelectedElement() {
return this.refs.field.options[this.refs.field.selectedIndex];
}
getOptions() {
return this.refs.field.options;
}
validate() {
if ('regexp' in this.props) {
if (!this.getValue().match(this.props.regexp)) {
this.showError(__('Invalid format.'));
} else {
this.clearError();
}
}
this.props.onBlur && this.props.onBlur();
}
focus() {
this.refs.field.focus();
}
render() {
// Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props),
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
elementId = this.props.elementId ? this.props.elementId : formFieldId(),
renderElementInsideLabel =
this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
delete otherProps.type;
delete otherProps.label;
delete otherProps.hasError;
delete otherProps.className;
delete otherProps.postfix;
delete otherProps.prefix;
delete otherProps.dispatch;
delete otherProps.regexp;
delete otherProps.trim;
const element = (
<this._element
id={elementId}
type={this._type}
name={this.props.name}
ref="field"
placeholder={this.props.placeholder}
onBlur={() => this.validate()}
onFocus={() => this.props.onFocus && this.props.onFocus()}
className={`form-field__input form-field__input-${this.props.type} ${this.props.className ||
''}${isError ? 'form-field__input--error' : ''}`}
{...otherProps}
{...this._extraElementProps}
>
{this.props.children}
</this._element>
);
return (
<div className={`form-field form-field--${this.props.type}`}>
{this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : ''}
{element}
{renderElementInsideLabel && (
<label
htmlFor={elementId}
className={`form-field__label ${isError ? 'form-field__label--error' : ''}`}
>
{this.props.label}
</label>
)}
{formFieldFileSelectorTypes.includes(this.props.type) ? (
<FileSelector
type={this.props.type}
onFileChosen={this.handleFileChosen.bind(this)}
{...(this.props.defaultValue ? { initPath: this.props.defaultValue } : {})}
/>
) : null}
{this.props.postfix ? (
<span className="form-field__postfix">{this.props.postfix}</span>
) : (
''
)}
{isError && this.state.errorMessage ? (
<div className="form-field__error">{this.state.errorMessage}</div>
) : (
''
)}
</div>
);
}
}
export default FormField;
/* eslint-enable */

View file

@ -1,5 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import TruncatedMarkdown from './view';
export default connect()(TruncatedMarkdown);

View file

@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactMarkdown from 'react-markdown';
import ReactDOMServer from 'react-dom/server';
class TruncatedMarkdown extends React.PureComponent {
static propTypes = {
lines: PropTypes.number,
};
static defaultProps = {
lines: null,
};
transformMarkdown(text) {
// render markdown to html string then trim html tag
const htmlString = ReactDOMServer.renderToStaticMarkup(
<ReactMarkdown source={this.props.children} />
);
const txt = document.createElement('textarea');
txt.innerHTML = htmlString;
return txt.value.replace(/<(?:.|\n)*?>/gm, '');
}
render() {
const content =
this.props.children && typeof this.props.children === 'string'
? this.transformMarkdown(this.props.children)
: this.props.children;
return (
<span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>
{content}
</span>
);
}
}
export default TruncatedMarkdown;

View file

@ -16,7 +16,6 @@ https://github.com/reactjs/react-autocomplete/issues/239
/* eslint-disable */
const React = require('react');
const PropTypes = require('prop-types');
const { findDOMNode } = require('react-dom');
const scrollIntoView = require('dom-scroll-into-view');
@ -45,129 +44,129 @@ function getScrollOffset() {
}
export default class Autocomplete extends React.Component {
static propTypes = {
/**
* The items to display in the dropdown menu
*/
items: PropTypes.array.isRequired,
/**
* The value to display in the input field
*/
value: PropTypes.any,
/**
* Arguments: `event: Event, value: String`
*
* Invoked every time the user changes the input's value.
*/
onChange: PropTypes.func,
/**
* Arguments: `value: String, item: Any`
*
* Invoked when the user selects an item from the dropdown menu.
*/
onSelect: PropTypes.func,
/**
* Arguments: `item: Any, value: String`
*
* Invoked for each entry in `items` and its return value is used to
* determine whether or not it should be displayed in the dropdown menu.
* By default all items are always rendered.
*/
shouldItemRender: PropTypes.func,
/**
* Arguments: `itemA: Any, itemB: Any, value: String`
*
* The function which is used to sort `items` before display.
*/
sortItems: PropTypes.func,
/**
* Arguments: `item: Any`
*
* Used to read the display value from each entry in `items`.
*/
getItemValue: PropTypes.func.isRequired,
/**
* Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
*
* Invoked for each entry in `items` that also passes `shouldItemRender` to
* generate the render tree for each item in the dropdown menu. `styles` is
* an optional set of styles that can be applied to improve the look/feel
* of the items in the dropdown menu.
*/
renderItem: PropTypes.func.isRequired,
/**
* Arguments: `items: Array<Any>, value: String, styles: Object`
*
* Invoked to generate the render tree for the dropdown menu. Ensure the
* returned tree includes every entry in `items` or else the highlight order
* and keyboard navigation logic will break. `styles` will contain
* { top, left, minWidth } which are the coordinates of the top-left corner
* and the width of the dropdown menu.
*/
renderMenu: PropTypes.func,
/**
* Styles that are applied to the dropdown menu in the default `renderMenu`
* implementation. If you override `renderMenu` and you want to use
* `menuStyle` you must manually apply them (`this.props.menuStyle`).
*/
menuStyle: PropTypes.object,
/**
* Arguments: `props: Object`
*
* Invoked to generate the input element. The `props` argument is the result
* of merging `props.inputProps` with a selection of props that are required
* both for functionality and accessibility. At the very least you need to
* apply `props.ref` and all `props.on<event>` event handlers. Failing to do
* this will cause `Autocomplete` to behave unexpectedly.
*/
renderInput: PropTypes.func,
/**
* Props passed to `props.renderInput`. By default these props will be
* applied to the `<input />` element rendered by `Autocomplete`, unless you
* have specified a custom value for `props.renderInput`. Any properties
* supported by `HTMLInputElement` can be specified, apart from the
* following which are set by `Autocomplete`: value, autoComplete, role,
* aria-autocomplete. `inputProps` is commonly used for (but not limited to)
* placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
*/
inputProps: PropTypes.object,
/**
* Props that are applied to the element which wraps the `<input />` and
* dropdown menu elements rendered by `Autocomplete`.
*/
wrapperProps: PropTypes.object,
/**
* This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
* Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
* will win if it contains a `style` entry.
*/
wrapperStyle: PropTypes.object,
/**
* Whether or not to automatically highlight the top match in the dropdown
* menu.
*/
autoHighlight: PropTypes.bool,
/**
* Whether or not to automatically select the highlighted item when the
* `<input>` loses focus.
*/
selectOnBlur: PropTypes.bool,
/**
* Arguments: `isOpen: Boolean`
*
* Invoked every time the dropdown menu's visibility changes (i.e. every
* time it is displayed/hidden).
*/
onMenuVisibilityChange: PropTypes.func,
/**
* Used to override the internal logic which displays/hides the dropdown
* menu. This is useful if you want to force a certain state based on your
* UX/business logic. Use it together with `onMenuVisibilityChange` for
* fine-grained control over the dropdown menu dynamics.
*/
open: PropTypes.bool,
debug: PropTypes.bool,
};
// static propTypes = {
// /**
// * The items to display in the dropdown menu
// */
// items: PropTypes.array.isRequired,
// /**
// * The value to display in the input field
// */
// value: PropTypes.any,
// /**
// * Arguments: `event: Event, value: String`
// *
// * Invoked every time the user changes the input's value.
// */
// onChange: PropTypes.func,
// /**
// * Arguments: `value: String, item: Any`
// *
// * Invoked when the user selects an item from the dropdown menu.
// */
// onSelect: PropTypes.func,
// /**
// * Arguments: `item: Any, value: String`
// *
// * Invoked for each entry in `items` and its return value is used to
// * determine whether or not it should be displayed in the dropdown menu.
// * By default all items are always rendered.
// */
// shouldItemRender: PropTypes.func,
// /**
// * Arguments: `itemA: Any, itemB: Any, value: String`
// *
// * The function which is used to sort `items` before display.
// */
// sortItems: PropTypes.func,
// /**
// * Arguments: `item: Any`
// *
// * Used to read the display value from each entry in `items`.
// */
// getItemValue: PropTypes.func.isRequired,
// /**
// * Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
// *
// * Invoked for each entry in `items` that also passes `shouldItemRender` to
// * generate the render tree for each item in the dropdown menu. `styles` is
// * an optional set of styles that can be applied to improve the look/feel
// * of the items in the dropdown menu.
// */
// renderItem: PropTypes.func.isRequired,
// /**
// * Arguments: `items: Array<Any>, value: String, styles: Object`
// *
// * Invoked to generate the render tree for the dropdown menu. Ensure the
// * returned tree includes every entry in `items` or else the highlight order
// * and keyboard navigation logic will break. `styles` will contain
// * { top, left, minWidth } which are the coordinates of the top-left corner
// * and the width of the dropdown menu.
// */
// renderMenu: PropTypes.func,
// /**
// * Styles that are applied to the dropdown menu in the default `renderMenu`
// * implementation. If you override `renderMenu` and you want to use
// * `menuStyle` you must manually apply them (`this.props.menuStyle`).
// */
// menuStyle: PropTypes.object,
// /**
// * Arguments: `props: Object`
// *
// * Invoked to generate the input element. The `props` argument is the result
// * of merging `props.inputProps` with a selection of props that are required
// * both for functionality and accessibility. At the very least you need to
// * apply `props.ref` and all `props.on<event>` event handlers. Failing to do
// * this will cause `Autocomplete` to behave unexpectedly.
// */
// renderInput: PropTypes.func,
// /**
// * Props passed to `props.renderInput`. By default these props will be
// * applied to the `<input />` element rendered by `Autocomplete`, unless you
// * have specified a custom value for `props.renderInput`. Any properties
// * supported by `HTMLInputElement` can be specified, apart from the
// * following which are set by `Autocomplete`: value, autoComplete, role,
// * aria-autocomplete. `inputProps` is commonly used for (but not limited to)
// * placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
// */
// inputProps: PropTypes.object,
// /**
// * Props that are applied to the element which wraps the `<input />` and
// * dropdown menu elements rendered by `Autocomplete`.
// */
// wrapperProps: PropTypes.object,
// /**
// * This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
// * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
// * will win if it contains a `style` entry.
// */
// wrapperStyle: PropTypes.object,
// /**
// * Whether or not to automatically highlight the top match in the dropdown
// * menu.
// */
// autoHighlight: PropTypes.bool,
// /**
// * Whether or not to automatically select the highlighted item when the
// * `<input>` loses focus.
// */
// selectOnBlur: PropTypes.bool,
// /**
// * Arguments: `isOpen: Boolean`
// *
// * Invoked every time the dropdown menu's visibility changes (i.e. every
// * time it is displayed/hidden).
// */
// onMenuVisibilityChange: PropTypes.func,
// /**
// * Used to override the internal logic which displays/hides the dropdown
// * menu. This is useful if you want to force a certain state based on your
// * UX/business logic. Use it together with `onMenuVisibilityChange` for
// * fine-grained control over the dropdown menu dynamics.
// */
// open: PropTypes.bool,
// debug: PropTypes.bool,
// };
static defaultProps = {
value: '',

View file

@ -7,24 +7,36 @@ import { parseQueryParams } from 'util/query_params';
import * as icons from 'constants/icons';
import Autocomplete from './internal/autocomplete';
const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27;
type Props = {
updateSearchQuery: string => void,
onSearch: string => void,
onSearch: (string, ?number) => void,
onSubmit: (string, {}) => void,
wunderbarValue: ?string,
suggestions: Array<string>,
doFocus: () => void,
doBlur: () => void,
resultCount: number,
focused: boolean,
};
class WunderBar extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
constructor() {
super();
(this: any).handleSubmit = this.handleSubmit.bind(this);
(this: any).handleChange = this.handleChange.bind(this);
this.input = undefined;
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
}
getSuggestionIcon = (type: string) => {
@ -38,6 +50,31 @@ class WunderBar extends React.PureComponent<Props> {
}
};
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
const { ctrlKey, metaKey, keyCode } = event;
const { doFocus, doBlur, focused } = this.props;
skhameneh commented 2018-10-05 07:47:10 +02:00 (Migrated from github.com)
Review

Does this correctly remove the event? Using bind above creates a handle that isn't stored.

Does this correctly remove the event? Using `bind` above creates a handle that isn't stored.
neb-b commented 2018-10-05 20:05:59 +02:00 (Migrated from github.com)
Review

👍 I'll move the bind into the constructor and use this.handleKeyDown for both.

👍 I'll move the `bind` into the constructor and use `this.handleKeyDown` for both.
if (!this.input) {
return;
}
if (focused && keyCode === ESC_KEY_CODE) {
doBlur();
this.input.blur();
return;
}
const shouldFocus =
process.platform === 'darwin'
? keyCode === L_KEY_CODE && metaKey
: keyCode === L_KEY_CODE && ctrlKey;
if (shouldFocus) {
doFocus();
this.input.focus();
}
}
handleChange(e: SyntheticInputEvent<*>) {
const { updateSearchQuery } = this.props;
const { value } = e.target;
@ -106,6 +143,10 @@ class WunderBar extends React.PureComponent<Props> {
renderInput={props => (
<input
{...props}
ref={el => {
props.ref(el);
this.input = el;
}}
className="wunderbar__input"
placeholder="Enter LBRY URL here or search for videos, music, games and more"
/>