Button components

Summary: _typescript_

Reviewed By: passy, bnelo12

Differential Revision: D16830539

fbshipit-source-id: a44ad0914b2581648b06e421476e0ba31ae96992
This commit is contained in:
Daniel Büchele
2019-08-20 05:40:31 -07:00
committed by Facebook Github Bot
parent eaceddbb32
commit 5cb12c3b1f
5 changed files with 175 additions and 159 deletions

View File

@@ -5,15 +5,16 @@
* @format * @format
*/ */
import * as React from 'react'; import React from 'react';
import Glyph from './Glyph.tsx';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import electron from 'electron'; import electron, {MenuItemConstructorOptions} from 'electron';
import styled from '../styled/index.js'; import styled from 'react-emotion';
import {colors} from './colors.tsx'; import {colors} from './colors';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {findDOMNode} from 'react-dom'; import {findDOMNode} from 'react-dom';
import {keyframes} from 'react-emotion'; import {keyframes} from 'react-emotion';
import {State as Store} from '../../reducers/index';
import Glyph, {IconSize} from './Glyph';
const borderColor = props => { const borderColor = props => {
if (!props.windowIsFocused) { if (!props.windowIsFocused) {
@@ -84,7 +85,16 @@ const pulse = keyframes({
}, },
}); });
const StyledButton = styled('div')(props => ({ const StyledButton = styled('div')(
(props: {
windowIsFocused?: boolean;
compact?: boolean;
inButtonGroup?: boolean;
padded?: boolean;
pulse?: boolean;
disabled?: boolean;
dropdown?: Array<MenuItemConstructorOptions>;
}) => ({
backgroundColor: !props.windowIsFocused backgroundColor: !props.windowIsFocused
? colors.macOSTitleBarButtonBackgroundBlur ? colors.macOSTitleBarButtonBackgroundBlur
: colors.white, : colors.white,
@@ -110,7 +120,8 @@ const StyledButton = styled('div')(props => ({
props.pulse && props.windowIsFocused props.pulse && props.windowIsFocused
? `0 0 0 ${colors.macOSTitleBarIconSelected}` ? `0 0 0 ${colors.macOSTitleBarIconSelected}`
: '', : '',
animation: props.pulse && props.windowIsFocused ? `${pulse} 1s infinite` : '', animation:
props.pulse && props.windowIsFocused ? `${pulse} 1s infinite` : '',
'&:not(:first-child)': { '&:not(:first-child)': {
borderTopLeftRadius: props.inButtonGroup === true ? 0 : 4, borderTopLeftRadius: props.inButtonGroup === true ? 0 : 4,
@@ -156,91 +167,92 @@ const StyledButton = styled('div')(props => ({
colors.macOSTitleBarIcon colors.macOSTitleBarIcon
} transparent transparent transparent`, } transparent transparent transparent`,
}, },
})); }),
);
const Icon = styled(Glyph)(({hasText}) => ({ const Icon = styled(Glyph)(({hasText}: {hasText: boolean}) => ({
marginRight: hasText ? 3 : 0, marginRight: hasText ? 3 : 0,
})); }));
type Props = { type OwnProps = {
/** /**
* onMouseUp handler. * onMouseUp handler.
*/ */
onMouseDown?: (event: SyntheticMouseEvent<>) => any, onMouseDown?: (event: React.MouseEvent) => any;
/** /**
* onClick handler. * onClick handler.
*/ */
onClick?: (event: SyntheticMouseEvent<>) => any, onClick?: (event: React.MouseEvent) => any;
/** /**
* Whether this button is disabled. * Whether this button is disabled.
*/ */
disabled?: boolean, disabled?: boolean;
/** /**
* Whether this button is large. Increases padding and line-height. * Whether this button is large. Increases padding and line-height.
*/ */
large?: boolean, large?: boolean;
/** /**
* Whether this button is compact. Decreases padding and line-height. * Whether this button is compact. Decreases padding and line-height.
*/ */
compact?: boolean, compact?: boolean;
/** /**
* Type of button. * Type of button.
*/ */
type?: 'primary' | 'success' | 'warning' | 'danger', type?: 'primary' | 'success' | 'warning' | 'danger';
/** /**
* Children. * Children.
*/ */
children?: React$Node, children?: React.ReactNode;
/** /**
* Dropdown menu template shown on click. * Dropdown menu template shown on click.
*/ */
dropdown?: Array<MenuItemConstructorOptions>, dropdown?: Array<MenuItemConstructorOptions>;
/** /**
* Name of the icon dispalyed next to the text * Name of the icon dispalyed next to the text
*/ */
icon?: string, icon?: string;
/** /**
* Size of the icon in pixels. * Size of the icon in pixels.
*/ */
iconSize?: number, iconSize?: IconSize;
/** /**
* For toggle buttons, if the button is selected * For toggle buttons, if the button is selected
*/ */
selected?: boolean, selected?: boolean;
/** /**
* Button is pulsing * Button is pulsing
*/ */
pulse?: boolean, pulse?: boolean;
/** /**
* URL to open in the browser on click * URL to open in the browser on click
*/ */
href?: string, href?: string;
/** /**
* Whether the button should render depressed into its socket * Whether the button should render depressed into its socket
*/ */
depressed?: boolean, depressed?: boolean;
/** /**
* Style of the icon. `filled` is the default * Style of the icon. `filled` is the default
*/ */
iconVariant?: 'filled' | 'outline', iconVariant?: 'filled' | 'outline';
/** /**
* Whether the button should have additional padding left and right. * Whether the button should have additional padding left and right.
*/ */
padded?: boolean, padded?: boolean;
}; };
type State = { type State = {
active: boolean, active: boolean;
wasClosed: boolean, wasClosed: boolean;
}; };
type StateFromProps = {windowIsFocused: boolean};
type Props = OwnProps & StateFromProps;
/** /**
* A simple button, used in many parts of the application. * A simple button, used in many parts of the application.
*/ */
class Button extends React.Component< class Button extends React.Component<Props, State> {
Props & {windowIsFocused: boolean},
State,
> {
static contextTypes = { static contextTypes = {
inButtonGroup: PropTypes.bool, inButtonGroup: PropTypes.bool,
}; };
@@ -250,9 +262,9 @@ class Button extends React.Component<
wasClosed: false, wasClosed: false,
}; };
_ref = React.createRef(); _ref = React.createRef<React.Component<typeof StyledButton>>();
onMouseDown = (e: SyntheticMouseEvent<>) => { onMouseDown = (e: React.MouseEvent) => {
this.setState({active: true, wasClosed: false}); this.setState({active: true, wasClosed: false});
if (this.props.onMouseDown != null) { if (this.props.onMouseDown != null) {
this.props.onMouseDown(e); this.props.onMouseDown(e);
@@ -264,18 +276,22 @@ class Button extends React.Component<
} }
if (this.props.dropdown && !this.state.wasClosed) { if (this.props.dropdown && !this.state.wasClosed) {
const menu = electron.remote.Menu.buildFromTemplate(this.props.dropdown); const menu = electron.remote.Menu.buildFromTemplate(this.props.dropdown);
const position = {}; const position: {
x?: number;
y?: number;
} = {};
const {current} = this._ref; const {current} = this._ref;
if (current) { if (current) {
const node = findDOMNode(current); const node = findDOMNode(current);
if (node instanceof Element) { if (node instanceof Element) {
const {left, bottom} = node.getBoundingClientRect(); const {left, bottom} = node.getBoundingClientRect();
position.x = parseInt(left, 10); position.x = left;
position.y = parseInt(bottom + 6, 10); position.y = bottom + 6;
} }
} }
menu.popup({ menu.popup({
window: electron.remote.getCurrentWindow(), window: electron.remote.getCurrentWindow(),
// @ts-ignore: async is private API in electron
async: true, async: true,
...position, ...position,
callback: () => { callback: () => {
@@ -286,7 +302,7 @@ class Button extends React.Component<
this.setState({active: false, wasClosed: false}); this.setState({active: false, wasClosed: false});
}; };
onClick = (e: SyntheticMouseEvent<>) => { onClick = (e: React.MouseEvent) => {
if (this.props.disabled === true) { if (this.props.disabled === true) {
return; return;
} }
@@ -341,7 +357,7 @@ class Button extends React.Component<
return ( return (
<StyledButton <StyledButton
{...props} {...props}
ref={this._ref} ref={this._ref as any}
windowIsFocused={windowIsFocused} windowIsFocused={windowIsFocused}
onClick={this.onClick} onClick={this.onClick}
onMouseDown={this.onMouseDown} onMouseDown={this.onMouseDown}
@@ -354,9 +370,8 @@ class Button extends React.Component<
} }
} }
const ConnectedButton = connect(({application: {windowIsFocused}}) => ({ export default connect<StateFromProps, {}, OwnProps, Store>(
({application: {windowIsFocused}}) => ({
windowIsFocused, windowIsFocused,
}))(Button); }),
)(Button);
// $FlowFixMe
export default (ConnectedButton: StyledComponent<Props>);

View File

@@ -5,9 +5,8 @@
* @format * @format
*/ */
import styled from '../styled/index.js'; import styled from 'react-emotion';
import {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const ButtonGroupContainer = styled('div')({ const ButtonGroupContainer = styled('div')({
@@ -30,8 +29,12 @@ const ButtonGroupContainer = styled('div')({
* ``` * ```
*/ */
export default class ButtonGroup extends Component<{ export default class ButtonGroup extends Component<{
children: React$Node, children: React.ReactNode;
}> { }> {
static childContextTypes = {
inButtonGroup: PropTypes.bool,
};
getChildContext() { getChildContext() {
return {inButtonGroup: true}; return {inButtonGroup: true};
} }
@@ -40,7 +43,3 @@ export default class ButtonGroup extends Component<{
return <ButtonGroupContainer>{this.props.children}</ButtonGroupContainer>; return <ButtonGroupContainer>{this.props.children}</ButtonGroupContainer>;
} }
} }
ButtonGroup.childContextTypes = {
inButtonGroup: PropTypes.bool,
};

View File

@@ -6,9 +6,8 @@
*/ */
import React, {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'react-emotion';
import styled from '../styled/index.js'; import Glyph from './Glyph';
import Glyph from './Glyph.tsx';
const IconContainer = styled('div')({ const IconContainer = styled('div')({
width: 0, width: 0,
@@ -18,7 +17,8 @@ const IconContainer = styled('div')({
pointerEvents: 'none', pointerEvents: 'none',
}); });
const ButtonGroupChainContainer = styled('div')(props => ({ const ButtonGroupChainContainer = styled('div')(
(props: {iconSize: number}) => ({
display: 'inline-flex', display: 'inline-flex',
marginLeft: 10, marginLeft: 10,
'&:first-child>*:not(:first-child):nth-child(odd)': { '&:first-child>*:not(:first-child):nth-child(odd)': {
@@ -34,21 +34,22 @@ const ButtonGroupChainContainer = styled('div')(props => ({
'&:first-child>:last-child': { '&:first-child>:last-child': {
borderRightStyle: 'solid', borderRightStyle: 'solid',
}, },
})); }),
);
type Props = { type Props = {
/** /**
* Children. * Children.
*/ */
children: React$Node, children: React.ReactNode;
/** /**
* Size of the button seperator icon in pixels. * Size of the button seperator icon in pixels.
*/ */
iconSize: 8 | 10 | 12 | 16 | 18 | 20 | 24 | 32, iconSize: 8 | 10 | 12 | 16 | 18 | 20 | 24 | 32;
/** /**
* Name of the icon seperating the buttons. Defaults to 'chevron-right'. * Name of the icon seperating the buttons. Defaults to 'chevron-right'.
*/ */
icon?: string, icon?: string;
}; };
/** /**
@@ -65,6 +66,10 @@ type Props = {
* ``` * ```
*/ */
export default class ButtonGroupChain extends Component<Props> { export default class ButtonGroupChain extends Component<Props> {
static childContextTypes = {
inButtonGroup: PropTypes.bool,
};
getChildContext() { getChildContext() {
return {inButtonGroup: true}; return {inButtonGroup: true};
} }
@@ -91,7 +96,3 @@ export default class ButtonGroupChain extends Component<Props> {
); );
} }
} }
ButtonGroupChain.childContextTypes = {
inButtonGroup: PropTypes.bool,
};

View File

@@ -5,22 +5,23 @@
* @format * @format
*/ */
import ButtonGroup from './ButtonGroup.js'; import ButtonGroup from './ButtonGroup';
import Button from './Button.js'; import Button from './Button';
import React from 'react';
/** /**
* Button group to navigate back and forth. * Button group to navigate back and forth.
*/ */
export default function ButtonNavigationGroup(props: {| export default function ButtonNavigationGroup(props: {
/** Back button is enabled */ /** Back button is enabled */
canGoBack: boolean, canGoBack: boolean;
/** Forwards button is enabled */ /** Forwards button is enabled */
canGoForward: boolean, canGoForward: boolean;
/** Callback when back button is clicked */ /** Callback when back button is clicked */
onBack: () => void, onBack: () => void;
/** Callback when forwards button is clicked */ /** Callback when forwards button is clicked */
onForward: () => void, onForward: () => void;
|}) { }) {
return ( return (
<ButtonGroup> <ButtonGroup>
<Button disabled={!props.canGoBack} onClick={props.onBack}> <Button disabled={!props.canGoBack} onClick={props.onBack}>

View File

@@ -6,13 +6,13 @@
*/ */
export {default as styled} from 'react-emotion'; export {default as styled} from 'react-emotion';
export {default as Button} from './components/Button.js'; export {default as Button} from './components/Button.tsx';
export {default as ToggleButton} from './components/ToggleSwitch.tsx'; export {default as ToggleButton} from './components/ToggleSwitch.tsx';
export { export {
default as ButtonNavigationGroup, default as ButtonNavigationGroup,
} from './components/ButtonNavigationGroup.js'; } from './components/ButtonNavigationGroup.tsx';
export {default as ButtonGroup} from './components/ButtonGroup.js'; export {default as ButtonGroup} from './components/ButtonGroup.tsx';
export {default as ButtonGroupChain} from './components/ButtonGroupChain.js'; export {default as ButtonGroupChain} from './components/ButtonGroupChain.tsx';
// //
export {colors, darkColors, brandColors} from './components/colors.tsx'; export {colors, darkColors, brandColors} from './components/colors.tsx';