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,163 +85,174 @@ const pulse = keyframes({
}, },
}); });
const StyledButton = styled('div')(props => ({ const StyledButton = styled('div')(
backgroundColor: !props.windowIsFocused (props: {
? colors.macOSTitleBarButtonBackgroundBlur windowIsFocused?: boolean;
: colors.white, compact?: boolean;
backgroundImage: backgroundImage(props), inButtonGroup?: boolean;
borderStyle: 'solid', padded?: boolean;
borderWidth: 1, pulse?: boolean;
borderColor: borderColor(props), disabled?: boolean;
borderBottomColor: borderBottomColor(props), dropdown?: Array<MenuItemConstructorOptions>;
color: color(props), }) => ({
borderRadius: 4, backgroundColor: !props.windowIsFocused
position: 'relative', ? colors.macOSTitleBarButtonBackgroundBlur
padding: props.padded ? '0 15px' : '0 6px', : colors.white,
height: props.compact === true ? 24 : 28, backgroundImage: backgroundImage(props),
margin: 0, borderStyle: 'solid',
marginLeft: props.inButtonGroup === true ? 0 : 10, borderWidth: 1,
minWidth: 34,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow:
props.pulse && props.windowIsFocused
? `0 0 0 ${colors.macOSTitleBarIconSelected}`
: '',
animation: props.pulse && props.windowIsFocused ? `${pulse} 1s infinite` : '',
'&:not(:first-child)': {
borderTopLeftRadius: props.inButtonGroup === true ? 0 : 4,
borderBottomLeftRadius: props.inButtonGroup === true ? 0 : 4,
},
'&:not(:last-child)': {
borderTopRightRadius: props.inButtonGroup === true ? 0 : 4,
borderBottomRightRadius: props.inButtonGroup === true ? 0 : 4,
borderRight: props.inButtonGroup === true ? 0 : '',
},
'&:first-of-type': {
marginLeft: 0,
},
'&:active': props.disabled
? null
: {
borderColor: colors.macOSTitleBarButtonBorder,
borderBottomColor: colors.macOSTitleBarButtonBorderBottom,
background: `linear-gradient(to bottom, ${
colors.macOSTitleBarButtonBackgroundActiveHighlight
} 1px, ${colors.macOSTitleBarButtonBackgroundActive} 0%, ${
colors.macOSTitleBarButtonBorderBlur
} 100%)`,
},
'&:disabled': {
borderColor: borderColor(props), borderColor: borderColor(props),
borderBottomColor: borderBottomColor(props), borderBottomColor: borderBottomColor(props),
pointerEvents: 'none', color: color(props),
}, borderRadius: 4,
position: 'relative',
padding: props.padded ? '0 15px' : '0 6px',
height: props.compact === true ? 24 : 28,
margin: 0,
marginLeft: props.inButtonGroup === true ? 0 : 10,
minWidth: 34,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
'&:hover::before': { boxShadow:
content: props.dropdown ? "''" : 'normal', props.pulse && props.windowIsFocused
position: 'absolute', ? `0 0 0 ${colors.macOSTitleBarIconSelected}`
bottom: 1, : '',
right: 2, animation:
borderStyle: 'solid', props.pulse && props.windowIsFocused ? `${pulse} 1s infinite` : '',
borderWidth: '4px 3px 0 3px',
borderColor: `${
colors.macOSTitleBarIcon
} transparent transparent transparent`,
},
}));
const Icon = styled(Glyph)(({hasText}) => ({ '&:not(:first-child)': {
borderTopLeftRadius: props.inButtonGroup === true ? 0 : 4,
borderBottomLeftRadius: props.inButtonGroup === true ? 0 : 4,
},
'&:not(:last-child)': {
borderTopRightRadius: props.inButtonGroup === true ? 0 : 4,
borderBottomRightRadius: props.inButtonGroup === true ? 0 : 4,
borderRight: props.inButtonGroup === true ? 0 : '',
},
'&:first-of-type': {
marginLeft: 0,
},
'&:active': props.disabled
? null
: {
borderColor: colors.macOSTitleBarButtonBorder,
borderBottomColor: colors.macOSTitleBarButtonBorderBottom,
background: `linear-gradient(to bottom, ${
colors.macOSTitleBarButtonBackgroundActiveHighlight
} 1px, ${colors.macOSTitleBarButtonBackgroundActive} 0%, ${
colors.macOSTitleBarButtonBorderBlur
} 100%)`,
},
'&:disabled': {
borderColor: borderColor(props),
borderBottomColor: borderBottomColor(props),
pointerEvents: 'none',
},
'&:hover::before': {
content: props.dropdown ? "''" : 'normal',
position: 'absolute',
bottom: 1,
right: 2,
borderStyle: 'solid',
borderWidth: '4px 3px 0 3px',
borderColor: `${
colors.macOSTitleBarIcon
} transparent transparent transparent`,
},
}),
);
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>(
windowIsFocused, ({application: {windowIsFocused}}) => ({
}))(Button); windowIsFocused,
}),
// $FlowFixMe )(Button);
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,37 +17,39 @@ const IconContainer = styled('div')({
pointerEvents: 'none', pointerEvents: 'none',
}); });
const ButtonGroupChainContainer = styled('div')(props => ({ const ButtonGroupChainContainer = styled('div')(
display: 'inline-flex', (props: {iconSize: number}) => ({
marginLeft: 10, display: 'inline-flex',
'&:first-child>*:not(:first-child):nth-child(odd)': { marginLeft: 10,
paddingLeft: props.iconSize + 6, '&:first-child>*:not(:first-child):nth-child(odd)': {
}, paddingLeft: props.iconSize + 6,
'&:first-child>*': { },
borderRightStyle: 'none', '&:first-child>*': {
borderLeftStyle: 'none', borderRightStyle: 'none',
}, borderLeftStyle: 'none',
'&:first-child>:first-child': { },
borderLeftStyle: 'solid', '&:first-child>:first-child': {
}, borderLeftStyle: 'solid',
'&:first-child>:last-child': { },
borderRightStyle: 'solid', '&:first-child>:last-child': {
}, 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';