Skip to content

Commit

Permalink
feat: button: add split button component (#57)
Browse files Browse the repository at this point in the history
* feat: button: add split button component and align button text prop

* fix: dropdown�: adds contextmenu event

updates split button styles

* chore: input: pixel push label icon button

* fix: button: moves split button into its own component

* chore: button: removes unused import

* chore: split button: use spread for props and move icon ternary
  • Loading branch information
dkilgore-eightfold authored Apr 13, 2022
1 parent dad6dc0 commit 46246b7
Show file tree
Hide file tree
Showing 16 changed files with 468 additions and 67 deletions.
92 changes: 66 additions & 26 deletions src/components/Button/BaseButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import React, { FC, Ref } from 'react';
import {
ButtonShape,
ButtonSize,
ButtonTextAlign,
ButtonTheme,
ButtonWidth,
InternalButtonProps,
SplitButton,
} from './';
import { Icon, IconName, IconSize } from '../Icon';
import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia';
Expand All @@ -15,8 +17,10 @@ import styles from './button.module.scss';
export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
(
{
alignText = ButtonTextAlign.Center,
allowDisabledFocus = false,
ariaLabel,
buttonWidth = ButtonWidth.fitContent,
checked = false,
className,
disabled = false,
Expand All @@ -27,13 +31,17 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
iconColor,
id,
onClick,
onContextMenu,
shape = ButtonShape.Rectangle,
size = ButtonSize.Flex,
split,
splitButtonChecked = false,
splitButtonProps,
style,
text,
theme,
toggle,
buttonWidth = ButtonWidth.fitContent,
type,
...rest
},
ref: Ref<HTMLButtonElement>
Expand All @@ -46,7 +54,7 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
const iconExists: boolean = !!icon;
const textExists: boolean = !!text;

const buttonBaseClassNames: string = classNames([
const buttonBaseSharedClassNames: string = classNames([
className,
{
[styles.buttonPadding3]:
Expand All @@ -67,10 +75,17 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
{ [styles.buttonPadding1]: size === ButtonSize.Large },
{ [styles.buttonPadding2]: size === ButtonSize.Medium },
{ [styles.buttonPadding3]: size === ButtonSize.Small },
{ [styles.buttonStretch]: buttonWidth === ButtonWidth.fill },
{ [styles.pillShape]: shape === ButtonShape.Pill },
{ [styles.dropShadow]: dropShadow },
{ [styles.dark]: theme === ButtonTheme.dark },
]);

const buttonBaseClassNames: string = classNames([
buttonBaseSharedClassNames,
{ [styles.buttonStretch]: buttonWidth === ButtonWidth.fill },
{ [styles.splitLeft]: split },
{ [styles.left]: alignText === ButtonTextAlign.Left },
{ [styles.right]: alignText === ButtonTextAlign.Right },
{ [styles.disabled]: allowDisabledFocus || disabled },
]);

Expand Down Expand Up @@ -130,30 +145,55 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
);

return (
<button
{...rest}
ref={ref}
aria-checked={toggle ? !!checked : undefined}
aria-disabled={allowDisabledFocus}
aria-label={ariaLabel}
aria-pressed={toggle ? !!checked : undefined}
defaultChecked={checked}
disabled={disabled}
className={buttonBaseClassNames}
id={id}
onClick={!allowDisabledFocus ? onClick : null}
style={style}
type={htmlType}
>
{iconExists && !textExists && getButtonIcon(icon)}
{iconExists && textExists && (
<span>
{getButtonIcon(icon)}
{getButtonText(buttonTextClassNames, text)}
</span>
<>
<button
{...rest}
ref={ref}
aria-checked={toggle ? !!checked : undefined}
aria-disabled={allowDisabledFocus}
aria-label={ariaLabel}
aria-pressed={toggle ? !!checked : undefined}
defaultChecked={checked}
disabled={disabled}
className={buttonBaseClassNames}
id={id}
onClick={!allowDisabledFocus ? onClick : null}
style={style}
type={htmlType}
>
{iconExists && !textExists && getButtonIcon(icon)}
{iconExists && textExists && (
<span>
{getButtonIcon(icon)}
{getButtonText(buttonTextClassNames, text)}
</span>
)}
{!iconExists && getButtonText(buttonTextClassNames, text)}
</button>
{split && (
<SplitButton
{...splitButtonProps}
className={
buttonBaseSharedClassNames +
' ' +
splitButtonProps?.className
}
checked={splitButtonChecked}
disruptive={disruptive}
dropShadow={dropShadow}
onClick={
!splitButtonProps?.allowDisabledFocus
? onContextMenu
: null
}
shape={shape}
size={size}
split={split}
theme={theme}
type={type}
/>
)}
{!iconExists && getButtonText(buttonTextClassNames, text)}
</button>
</>
);
}
);
51 changes: 48 additions & 3 deletions src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import React, { FC } from 'react';
import {
ButtonShape,
ButtonSize,
ButtonTextAlign,
ButtonWidth,
DefaultButton,
NeutralButton,
PrimaryButton,
SecondaryButton,
} from './index';
import { Dropdown } from '../Dropdown/Dropdown';
import { IconName } from '../Icon';
import { List, ItemLayout } from '../List';
import { useBoolean } from '../../hooks/useBoolean';

interface ToggleButtonExampleProps {
// These are set based on the toggle shown (not needed in real code)
interface ExampleProps {
checked?: boolean;
}

Expand Down Expand Up @@ -241,7 +243,50 @@ export const Neutral = () => (
</>
);

export const Toggle: FC<ToggleButtonExampleProps> = ({ checked }) => {
export const Split: FC<ExampleProps> = ({ checked }) => {
const [menuShown, { toggle: setMenuShown }] = useBoolean(false);
return (
<>
<h1>Split Button</h1>
<p>
Note: Split buttons require the <code>onContextMenu</code>,{' '}
<code>split</code> attributes in addition to{' '}
<code>splitButtonChecked</code>.
</p>
<Dropdown
overlay={
<div>
<DefaultButton
alignText={ButtonTextAlign.Left}
buttonWidth={ButtonWidth.fill}
/>
<DefaultButton
alignText={ButtonTextAlign.Left}
buttonWidth={ButtonWidth.fill}
/>
<DefaultButton
alignText={ButtonTextAlign.Left}
buttonWidth={ButtonWidth.fill}
/>
</div>
}
trigger="contextmenu"
onVisibleChange={setMenuShown}
>
<PrimaryButton
ariaLabel="Split Button"
onClick={_alertClicked}
onContextMenu={setMenuShown}
split
splitButtonChecked={menuShown || checked}
text="Split Button"
/>
</Dropdown>
</>
);
};

export const Toggle: FC<ExampleProps> = ({ checked }) => {
const [skill1Added, { toggle: set1Added }] = useBoolean(false);
const [skill2Added, { toggle: set2Added }] = useBoolean(false);
const [skill3Added, { toggle: set3Added }] = useBoolean(false);
Expand Down
42 changes: 29 additions & 13 deletions src/components/Button/Button.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as React from 'react';
import React, { Ref } from 'react';
import { IconName } from '../Icon';
import { Ref } from 'react';

export enum ButtonTextAlign {
Center = 'center',
Left = 'left',
Right = 'right',
}

export enum ButtonSize {
Flex = 'flex',
Expand Down Expand Up @@ -33,6 +38,12 @@ export enum ButtonType {

export type NativeButtonProps = Omit<React.ButtonHTMLAttributes<any>, 'type'>;

export interface SplitButtonProps
extends Omit<
InternalButtonProps,
'text' | 'htmlType' | 'onContextMenu' | 'splitButtonProps' | 'toggle'
> {}

export interface InternalButtonProps extends ButtonProps {
/**
* Determines the button type.
Expand All @@ -53,6 +64,16 @@ export interface ButtonProps extends NativeButtonProps {
* The button aria-label text.
*/
ariaLabel?: string;
/**
* The button text alignment.
* @default ButtonTextAlign.Center
*/
alignText?: ButtonTextAlign;
/**
* The button width type
* @default fitContent
*/
buttonWidth?: ButtonWidth;
/**
* The button checked value.
*/
Expand Down Expand Up @@ -97,9 +118,9 @@ export interface ButtonProps extends NativeButtonProps {
*/
onClick?: React.MouseEventHandler<HTMLButtonElement>;
/**
* The button primary color.
* The split button click event handler.
*/
primaryColor?: string;
onContextMenu?: React.MouseEventHandler<HTMLButtonElement>;
/**
* Shape of the button.
* @default ButtonShape.Rectangle
Expand All @@ -116,13 +137,13 @@ export interface ButtonProps extends NativeButtonProps {
*/
split?: boolean;
/**
* The split button aria-label text.
* The split button checked value.
*/
splitButtonAriaLabel?: string;
splitButtonChecked?: boolean;
/**
* The split button menu props.
* The split button props.
*/
splitButtonMenuProps?: ButtonProps;
splitButtonProps?: SplitButtonProps;
/**
* The button style.
*/
Expand All @@ -140,9 +161,4 @@ export interface ButtonProps extends NativeButtonProps {
* The button is a toggle button with distinct on and off states.
*/
toggle?: boolean;
/**
* The button width type
* @default fitContent
*/
buttonWidth?: ButtonWidth;
}
11 changes: 10 additions & 1 deletion src/components/Button/DefaultButton/DefaultButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { FC, Ref } from 'react';
import {
ButtonShape,
BaseButton,
ButtonProps,
ButtonShape,
ButtonSize,
ButtonTextAlign,
ButtonType,
} from '../';
import { classNames } from '../../../shared/utilities';
Expand All @@ -13,6 +14,7 @@ import styles from '../button.module.scss';
export const DefaultButton: FC<ButtonProps> = React.forwardRef(
(
{
alignText = ButtonTextAlign.Center,
allowDisabledFocus = false,
ariaLabel,
checked = false,
Expand All @@ -27,6 +29,9 @@ export const DefaultButton: FC<ButtonProps> = React.forwardRef(
theme,
shape = ButtonShape.Rectangle,
size = ButtonSize.Flex,
split,
splitButtonChecked,
splitButtonProps,
style,
toggle,
buttonWidth,
Expand All @@ -46,6 +51,7 @@ export const DefaultButton: FC<ButtonProps> = React.forwardRef(
<BaseButton
{...rest}
ref={ref}
alignText={alignText}
allowDisabledFocus={allowDisabledFocus}
ariaLabel={ariaLabel}
checked={checked}
Expand All @@ -58,6 +64,9 @@ export const DefaultButton: FC<ButtonProps> = React.forwardRef(
onClick={onClick}
shape={shape}
size={size}
split={split}
splitButtonChecked={splitButtonChecked}
splitButtonProps={splitButtonProps}
style={style}
text={text}
theme={theme}
Expand Down
Loading

0 comments on commit 46246b7

Please sign in to comment.