Skip to content

Commit

Permalink
Components: Add next Button, ButtonGroup (#29230)
Browse files Browse the repository at this point in the history
* Components: Add hooks based Button, ButtonGroup

* Update package-lock.json with react-i18n

* Fix prefix prop for BaseButton. Update BaseButton + Button stories

* Add dependencies to tsconfig.json reference

* Add ButtonGroup documentation

* Update Button README

* Rename prefix back to pre. Update test + snapshots

* Make style diff snapshots work with CSS variables

* Improve to-match-style-diff-snapshot + update snapshot tests

* Update snapshots after rebase

* Use i18n directly instead of react-i18n

* Fix linting errors

* Remove react-i18n dependency from package-lock after rebase

* Update snapshot tests for interpolated components

Co-authored-by: Jon Q <[email protected]>
  • Loading branch information
sarayourfriend and Jon Q authored Mar 3, 2021
1 parent 838e831 commit 977c770
Show file tree
Hide file tree
Showing 29 changed files with 3,342 additions and 56 deletions.
153 changes: 153 additions & 0 deletions packages/components/src/ui/base-button/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* External dependencies
*/
import { contextConnect } from '@wp-g2/context';
import { cx, ui } from '@wp-g2/styles';
// eslint-disable-next-line no-restricted-imports
import { Radio as ReakitRadio } from 'reakit';

/**
* WordPress dependencies
*/
import { Icon, chevronDown } from '@wordpress/icons';

/**
* Internal dependencies
*/
import { useButtonGroupContext } from '../button-group';
import { Elevation } from '../elevation';
import { FlexItem } from '../flex';
import { View } from '../view';
import * as styles from './styles';
import LoadingOverlay from './loading-overlay';
import { useBaseButton } from './hook';

/**
* @param {import('@wp-g2/create-styles').ViewOwnProps<import('./types').Props, 'button'>} props
* @param {import('react').Ref<any>} forwardedRef
*/
function BaseButton( props, forwardedRef ) {
const {
as: asProp,
children,
disabled = false,
elevation = 0,
elevationActive,
elevationFocus,
elevationHover,
hasCaret = false,
href,
icon,
iconSize = 16,
isActive = false,
isDestructive = false,
isFocused = false,
isLoading = false,
noWrap = true,
pre,
suffix,
...otherProps
} = useBaseButton( props );
const { buttonGroup } = useButtonGroupContext();
const buttonGroupState = buttonGroup || {};

const BaseComponent = buttonGroup ? ReakitRadio : View;
const as = asProp || ( href ? 'a' : 'button' );

return (
// @ts-ignore No idea why TS is confused about this but ReakitRadio and View are definitely renderable
<BaseComponent
aria-busy={ isLoading }
as={ as }
data-active={ isActive }
data-destructive={ isDestructive }
data-focused={ isFocused }
data-icon={ !! icon }
disabled={ disabled || isLoading }
href={ href }
{ ...buttonGroupState }
{ ...otherProps }
ref={ forwardedRef }
>
<LoadingOverlay isLoading={ isLoading } />
{ pre && (
<FlexItem
as="span"
className={ cx(
styles.PrefixSuffix,
isLoading && styles.loading
) }
{ ...ui.$( 'ButtonPrefix' ) }
>
{ pre }
</FlexItem>
) }
{ icon && (
<FlexItem
as="span"
className={ cx(
styles.PrefixSuffix,
isLoading && styles.loading
) }
{ ...ui.$( 'ButtonIcon' ) }
>
<Icon icon={ icon } size={ iconSize } />
</FlexItem>
) }
{ children && (
<FlexItem
as="span"
className={ cx(
styles.Content,
isLoading && styles.loading,
noWrap && styles.noWrap
) }
isBlock
{ ...ui.$( 'ButtonContent' ) }
>
{ children }
</FlexItem>
) }
{ suffix && (
<FlexItem
as="span"
className={ cx(
styles.PrefixSuffix,
isLoading && styles.loading
) }
{ ...ui.$( 'ButtonSuffix' ) }
>
{ suffix }
</FlexItem>
) }
{ hasCaret && (
<FlexItem
as="span"
className={ cx(
styles.CaretWrapper,
isLoading && styles.loading
) }
{ ...ui.$( 'ButtonCaret' ) }
>
<Icon icon={ chevronDown } size={ 16 } />
</FlexItem>
) }
<Elevation
active={ elevationActive }
as="span"
focus={ elevationFocus }
hover={ elevationHover }
offset={ -1 }
value={ elevation }
{ ...ui.$( 'ButtonElevation' ) }
/>
</BaseComponent>
);
}

/**
* `BaseButton` is a primitive component used to create actionable components (e.g. `Button`).
*/
const ConnectedBaseButton = contextConnect( BaseButton, 'BaseButton' );

export default ConnectedBaseButton;
101 changes: 101 additions & 0 deletions packages/components/src/ui/base-button/hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* External dependencies
*/
import { useContextSystem } from '@wp-g2/context';
import { css, cx } from '@wp-g2/styles';

/**
* Internal dependencies
*/
import { useControlGroupContext } from '../control-group';
import { useFlex } from '../flex';
import * as styles from './styles';

/**
* @param {import('@wp-g2/create-styles').ViewOwnProps<import('./types').Props, 'button'>} props
*/
export function useBaseButton( props ) {
const {
children,
className,
css: cssProp,
currentColor,
disabled = false,
elevation = 0,
elevationActive,
elevationFocus,
elevationHover,
gap = 2,
hasCaret = false,
href,
icon,
iconSize = 16,
isBlock = false,
isControl = false,
isDestructive = false,
isLoading = false,
isNarrow = false,
isRounded = false,
isSplit = false,
isSubtle = false,
justify = 'center',
noWrap = true,
pre,
size = 'medium',
suffix,
textAlign = 'center',
...otherProps
} = useContextSystem( props, 'BaseButton' );

const { className: flexClassName, ...flexProps } = useFlex( {
gap,
justify,
} );

/** @type {import('react').ElementType} */
const as = href ? 'a' : 'button';
const { styles: controlGroupStyles } = useControlGroupContext();
const isIconOnly = !! icon && ! children;

const classes = cx(
flexClassName,
styles.Button,
isBlock && styles.block,
isDestructive && styles.destructive,
styles[ size ],
isIconOnly && styles.icon,
isSubtle && styles.subtle,
isControl && styles.control,
isSubtle && isControl && styles.subtleControl,
controlGroupStyles,
isRounded && styles.rounded,
isNarrow && styles.narrow,
isSplit && styles.split,
currentColor && styles.currentColor,
css( { textAlign } ),
css( cssProp ),
className
);

return {
...flexProps,
as,
href,
children,
disabled,
elevation,
className: classes,
elevationActive,
elevationFocus,
elevationHover,
hasCaret,
icon,
isDestructive,
pre,
suffix,
iconSize,
isLoading,
noWrap,
...otherProps,
};
}
3 changes: 3 additions & 0 deletions packages/components/src/ui/base-button/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as BaseButton } from './component';

export * from './hook';
22 changes: 22 additions & 0 deletions packages/components/src/ui/base-button/loading-overlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import { Flex } from '../flex';
import { Spinner } from '../spinner';
import * as styles from './styles';

export function LoadingOverlay( { isLoading = false } ) {
if ( ! isLoading ) return null;

return (
<Flex
aria-hidden="true"
className={ styles.LoadingOverlay }
justify="center"
>
<Spinner />
</Flex>
);
}

export default LoadingOverlay;
Loading

0 comments on commit 977c770

Please sign in to comment.