add {cmd,ctrl} + l for wunderbar focus

This commit is contained in:
Sean Yesmunt 2018-10-04 01:59:47 -04:00 committed by Sean Yesmunt
parent 3815d36f58
commit 71327875f8
7 changed files with 189 additions and 399 deletions

View file

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

View file

@ -7,6 +7,9 @@ import { parseQueryParams } from 'util/query_params';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import Autocomplete from './internal/autocomplete'; import Autocomplete from './internal/autocomplete';
const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27;
type Props = { type Props = {
updateSearchQuery: string => void, updateSearchQuery: string => void,
onSearch: string => void, onSearch: string => void,
@ -16,15 +19,23 @@ type Props = {
doFocus: () => void, doFocus: () => void,
doBlur: () => void, doBlur: () => void,
resultCount: number, resultCount: number,
focused: boolean,
}; };
class WunderBar extends React.PureComponent<Props> { class WunderBar extends React.PureComponent<Props> {
constructor(props: Props) { constructor() {
super(props); super();
(this: any).handleSubmit = this.handleSubmit.bind(this); (this: any).handleSubmit = this.handleSubmit.bind(this);
(this: any).handleChange = this.handleChange.bind(this); (this: any).handleChange = this.handleChange.bind(this);
this.input = undefined; }
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown.bind(this));
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
} }
getSuggestionIcon = (type: string) => { getSuggestionIcon = (type: string) => {
@ -38,6 +49,31 @@ class WunderBar extends React.PureComponent<Props> {
} }
}; };
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
const { ctrlKey, metaKey, keyCode } = event;
const { doFocus, doBlur, focused } = this.props;
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<*>) { handleChange(e: SyntheticInputEvent<*>) {
const { updateSearchQuery } = this.props; const { updateSearchQuery } = this.props;
const { value } = e.target; const { value } = e.target;
@ -106,6 +142,10 @@ class WunderBar extends React.PureComponent<Props> {
renderInput={props => ( renderInput={props => (
<input <input
{...props} {...props}
ref={el => {
props.ref(el);
this.input = el;
}}
className="wunderbar__input" className="wunderbar__input"
placeholder="Enter LBRY URL here or search for videos, music, games and more" placeholder="Enter LBRY URL here or search for videos, music, games and more"
/> />