Skip to content

Commit

Permalink
feat: button: adds two state button component (EightfoldAI#80)
Browse files Browse the repository at this point in the history
* feat: button: adds two state button component

* chore: button: update two state button in storybook

* chore: button: removes unused references

* chore: button: updates two state button styles to work with themes

* chore: button: updates two state focus background color
  • Loading branch information
dkilgore-eightfold authored and gclark-eightfold committed Apr 27, 2022
1 parent e642053 commit 3d8fd9b
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 7 deletions.
29 changes: 28 additions & 1 deletion packages/octuple/src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
NeutralButton,
PrimaryButton,
SecondaryButton,
TwoStateButton,
} from './index';
import { Dropdown } from '../Dropdown/Dropdown';
import { IconName } from '../Icon';
import { List, ItemLayout } from '../List';
import { useBoolean } from '../../hooks/useBoolean';

interface ExampleProps {
Expand Down Expand Up @@ -346,6 +346,33 @@ export const Toggle: FC<ExampleProps> = ({ checked }) => {
);
};

export const TwoState: FC<ExampleProps> = ({ checked }) => {
const [isToggled, { toggle: setToggled }] = useBoolean(false);
return (
<>
<h1>Two State Button</h1>
<p>
Note: Like Toggle, Two State buttons require the{' '}
<code>toggle</code> attribute in addition to{' '}
<code>checked</code>. Two State button's visual state is
different than a basic Toggle button.
</p>
<TwoStateButton
ariaLabel="Two State Button"
checked={isToggled || checked}
counter={8}
iconOne={IconName.mdiCardsHeart}
iconTwo={
isToggled ? IconName.mdiChevronUp : IconName.mdiChevronDown
}
onClick={setToggled}
text="Two State Button"
toggle
/>
</>
);
};

function _alertClicked(): void {
alert('Clicked');
}
42 changes: 37 additions & 5 deletions packages/octuple/src/components/Button/Button.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ export enum ButtonType {
Secondary = 'secondary',
}

export interface InternalButtonProps extends ButtonProps {
/**
* Determines the button type.
*/
type?: ButtonType;
/**
* Ref of the button
*/
ref?: Ref<HTMLButtonElement>;
}

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

export interface SplitButtonProps
Expand All @@ -44,15 +55,36 @@ export interface SplitButtonProps
'text' | 'htmlType' | 'onContextMenu' | 'splitButtonProps' | 'toggle'
> {}

export interface InternalButtonProps extends ButtonProps {
export interface TwoStateButtonProps
extends Omit<
InternalButtonProps,
| 'htmlType'
| 'icon'
| 'iconColor'
| 'onContextMenu'
| 'split'
| 'splitButtonProps'
> {
/**
* Determines the button type.
* The button counter number.
*/
type?: ButtonType;
counter?: number;
/**
* Ref of the button
* The button icon 1.
*/
ref?: Ref<HTMLButtonElement>;
iconOne?: IconName;
/**
* The button icon 1 color.
*/
iconOneColor?: string;
/**
* The button icon 2.
*/
iconTwo?: IconName;
/**
* The button icon 2 color.
*/
iconTwoColor?: string;
}

export interface ButtonProps extends NativeButtonProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { FC, Ref } from 'react';
import {
ButtonShape,
ButtonSize,
ButtonTextAlign,
ButtonTheme,
ButtonWidth,
TwoStateButtonProps,
} from '../';
import { Icon, IconName, IconSize } from '../../Icon';
import { Breakpoints, useMatchMedia } from '../../../hooks/useMatchMedia';
import { classNames } from '../../../shared/utilities';

import styles from '../button.module.scss';

export const TwoStateButton: FC<TwoStateButtonProps> = React.forwardRef(
(
{
alignText = ButtonTextAlign.Center,
allowDisabledFocus = false,
ariaLabel,
buttonWidth = ButtonWidth.fitContent,
className,
checked = false,
counter,
disabled = false,
disruptive = false,
dropShadow = false,
iconOne,
iconOneColor,
iconTwo,
iconTwoColor,
id,
onClick,
shape = ButtonShape.Rectangle,
size = ButtonSize.Flex,
style,
text,
theme,
toggle,
type,
...rest
},
ref: Ref<HTMLButtonElement>
) => {
const largeScreenActive: boolean = useMatchMedia(Breakpoints.Large);
const mediumScreenActive: boolean = useMatchMedia(Breakpoints.Medium);
const smallScreenActive: boolean = useMatchMedia(Breakpoints.Small);
const xSmallScreenActive: boolean = useMatchMedia(Breakpoints.XSmall);

const counterExists: boolean = !!counter;
const iconOneExists: boolean = !!iconOne;
const iconTwoExists: boolean = !!iconTwo;
const textExists: boolean = !!text;

const twoStateButtonClassNames: string = classNames([
className,
styles.button,
styles.twoStateButton,
{ [styles.checked]: checked },
{
[styles.buttonPadding3]:
size === ButtonSize.Flex && largeScreenActive,
},
{
[styles.buttonPadding2]:
size === ButtonSize.Flex && mediumScreenActive,
},
{
[styles.buttonPadding2]:
size === ButtonSize.Flex && smallScreenActive,
},
{
[styles.buttonPadding1]:
size === ButtonSize.Flex && xSmallScreenActive,
},
{ [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.left]: alignText === ButtonTextAlign.Left },
{ [styles.right]: alignText === ButtonTextAlign.Right },
{ [styles.disabled]: allowDisabledFocus || disabled },
{ [styles.dark]: theme === ButtonTheme.dark },
]);

const buttonTextClassNames: string = classNames([
{ [styles.button3]: size === ButtonSize.Flex && largeScreenActive },
{
[styles.button2]:
size === ButtonSize.Flex && mediumScreenActive,
},
{ [styles.button2]: size === ButtonSize.Flex && smallScreenActive },
{
[styles.button1]:
size === ButtonSize.Flex && xSmallScreenActive,
},
{ [styles.button1]: size === ButtonSize.Large },
{ [styles.button2]: size === ButtonSize.Medium },
{ [styles.button3]: size === ButtonSize.Small },
]);

const counterClassNames: string = classNames([
styles.counter,
buttonTextClassNames,
]);

const getButtonIconSize = (): IconSize => {
let iconSize: IconSize;
if (size === ButtonSize.Flex && largeScreenActive) {
iconSize = IconSize.Small;
} else if (
size === ButtonSize.Flex &&
(mediumScreenActive || smallScreenActive)
) {
iconSize = IconSize.Medium;
} else if (size === ButtonSize.Flex && xSmallScreenActive) {
iconSize = IconSize.Large;
} else if (size === ButtonSize.Large) {
iconSize = IconSize.Large;
} else if (size === ButtonSize.Medium) {
iconSize = IconSize.Medium;
} else if (size === ButtonSize.Small) {
iconSize = IconSize.Small;
}
return iconSize;
};

const getButtonIcon = (icon: IconName, color: string): JSX.Element => (
<Icon
className={styles.icon}
color={color}
path={icon}
size={getButtonIconSize()}
/>
);

const getButtonText = (
buttonTextClassNames: string,
text: string
): JSX.Element => (
<span className={buttonTextClassNames}>
{text ? text : 'Button'}
</span>
);

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={twoStateButtonClassNames}
id={id}
onClick={!allowDisabledFocus ? onClick : null}
style={style}
type="button"
>
<span>
{iconOneExists && getButtonIcon(iconOne, iconOneColor)}
{textExists && getButtonText(buttonTextClassNames, text)}
{counterExists && (
<span className={counterClassNames}>
{counter.toLocaleString()}
</span>
)}
{iconTwoExists && getButtonIcon(iconTwo, iconTwoColor)}
</span>
</button>
);
}
);
Loading

0 comments on commit 3d8fd9b

Please sign in to comment.