diff --git a/packages/octuple/src/components/Button/Button.stories.tsx b/packages/octuple/src/components/Button/Button.stories.tsx index f32f4ee37..b4d3c18dd 100644 --- a/packages/octuple/src/components/Button/Button.stories.tsx +++ b/packages/octuple/src/components/Button/Button.stories.tsx @@ -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 { @@ -346,6 +346,33 @@ export const Toggle: FC = ({ checked }) => { ); }; +export const TwoState: FC = ({ checked }) => { + const [isToggled, { toggle: setToggled }] = useBoolean(false); + return ( + <> +

Two State Button

+

+ Note: Like Toggle, Two State buttons require the{' '} + toggle attribute in addition to{' '} + checked. Two State button's visual state is + different than a basic Toggle button. +

+ + + ); +}; + function _alertClicked(): void { alert('Clicked'); } diff --git a/packages/octuple/src/components/Button/Button.types.ts b/packages/octuple/src/components/Button/Button.types.ts index 0886bddeb..2255ad24b 100644 --- a/packages/octuple/src/components/Button/Button.types.ts +++ b/packages/octuple/src/components/Button/Button.types.ts @@ -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; +} + export type NativeButtonProps = Omit, 'type'>; export interface SplitButtonProps @@ -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; + 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 { diff --git a/packages/octuple/src/components/Button/TwoStateButton/TwoStateButton.tsx b/packages/octuple/src/components/Button/TwoStateButton/TwoStateButton.tsx new file mode 100644 index 000000000..baa10bfa1 --- /dev/null +++ b/packages/octuple/src/components/Button/TwoStateButton/TwoStateButton.tsx @@ -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 = 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 + ) => { + 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 => ( + + ); + + const getButtonText = ( + buttonTextClassNames: string, + text: string + ): JSX.Element => ( + + {text ? text : 'Button'} + + ); + + return ( + + ); + } +); diff --git a/packages/octuple/src/components/Button/button.module.scss b/packages/octuple/src/components/Button/button.module.scss index a221704a5..db0471872 100644 --- a/packages/octuple/src/components/Button/button.module.scss +++ b/packages/octuple/src/components/Button/button.module.scss @@ -25,6 +25,30 @@ --button-active-background-color: var(--primary-color-10); --button-visited-background-color: var(--primary-color-10); --button-hover-variant-background-color: var(--primary-color-20); + --button-two-state-default-foreground-color: var(--grey-color-70); + --button-two-state-hover-foreground-color: var(--primary-color-70); + --button-two-state-focus-foreground-color: var(--primary-color-80); + --button-two-state-checked-foreground-color: var(--primary-color-80); + --button-two-state-active-foreground-color: var(--primary-color-80); + --button-two-state-visited-foreground-color: var(--primary-color-80); + --button-two-state-default-background-color: var(--white-color); + --button-two-state-hover-background-color: var(--white-color); + --button-two-state-focus-background-color: var(--white-color); + --button-two-state-checked-background-color: var(--primary-color-20); + --button-two-state-active-background-color: var(--primary-color-20); + --button-two-state-visited-background-color: var(--primary-color-20); + --button-counter-default-background-color: var(--grey-color-20); + --button-counter-hover-background-color: var(--primary-color-20); + --button-counter-checked-background-color: var(--primary-color-10); + --button-counter-focus-background-color: var(--primary-color-20); + --button-counter-active-background-color: var(--primary-color-10); + --button-counter-visited-background-color: var(--primary-color-10); + --button-two-state-default-outline-color: var(--grey-color-70); + --button-two-state-hover-outline-color: var(--primary-color-60); + --button-two-state-checked-outline-color: var(--primary-color-70); + --button-two-state-focus-outline-color: var(--primary-color-40); + --button-two-state-active-outline-color: var(--primary-color-70); + --button-two-state-visited-outline-color: var(--primary-color-70); --text-color: var(--text-inverse-color); background-color: inherit; border: 2px solid rgba(var(--button-primary-default-border-color), 0.01); @@ -35,9 +59,9 @@ white-space: nowrap; span { + align-items: center; display: flex; flex-direction: row; - align-items: center; } .icon + .button1:not(:empty) { @@ -119,6 +143,14 @@ font-size: $text-font-size-5; line-height: $text-line-height-3; + &.counter { + font-size: $text-font-size-4; + height: 24px; + line-height: 24px; + margin: 0 $space-xs; + width: 24px; + } + &.open-sans { font-family: 'Open Sans', sans-serif; } @@ -131,6 +163,15 @@ font-size: $text-font-size-3; line-height: $text-line-height-2; + &.counter { + font-size: $text-font-size-2; + height: 18px; + line-height: 18px; + margin: 0 $space-xs; + padding-left: 1px; + width: 18px; + } + &.open-sans { font-family: 'Open Sans', sans-serif; } @@ -143,6 +184,15 @@ font-size: $text-font-size-2; line-height: $text-line-height-1; + &.counter { + font-size: $text-font-size-1; + height: 16px; + line-height: 16px; + margin: 0 $space-xxs; + padding-left: 0; + width: 16px; + } + &.open-sans { font-family: 'Open Sans', sans-serif; } @@ -398,6 +448,66 @@ } } +.two-state-button { + background-color: var(--button-two-state-default-background-color); + color: var(--button-two-state-default-foreground-color); + outline: var(--button-two-state-default-outline-color) solid 1px; + outline-offset: -2px; + + .counter { + background-color: var(--button-counter-default-background-color); + border-radius: 50%; + display: inline; + text-align: center; + } + + &:hover { + background-color: var(--button-two-state-hover-background-color); + color: var(--button-two-state-hover-foreground-color); + outline-color: var(--button-two-state-hover-outline-color); + + .counter { + background-color: var(--button-counter-hover-background-color); + } + } + + &:active { + background-color: var(--button-two-state-active-background-color); + color: var(--button-two-state-active-foreground-color); + outline-color: var(--button-two-state-active-outline-color); + + .counter { + background-color: var(--button-counter-active-background-color); + } + } + + &.checked { + background-color: var(--button-two-state-checked-background-color); + color: var(--button-two-state-checked-foreground-color); + outline-color: var(--button-two-state-checked-outline-color); + + .counter { + background-color: var(--button-counter-checked-background-color); + } + } + + &:focus-visible { + background-color: var(--button-two-state-focus-background-color); + color: var(--button-two-state-focus-foreground-color); + outline: 1px solid var(--button-two-state-focus-outline-color); + + &.checked { + background-color: var(--button-two-state-checked-background-color); + color: var(--button-two-state-checked-foreground-color); + outline: 1px solid var(--button-two-state-focus-outline-color); + } + + .counter { + background-color: var(--button-counter-focus-background-color); + } + } +} + .dark { .button { background-color: inherit; diff --git a/packages/octuple/src/components/Button/index.ts b/packages/octuple/src/components/Button/index.ts index 7a215a1e6..3478b82e5 100644 --- a/packages/octuple/src/components/Button/index.ts +++ b/packages/octuple/src/components/Button/index.ts @@ -5,3 +5,4 @@ export * from './NeutralButton/NeutralButton'; export * from './PrimaryButton/PrimaryButton'; export * from './SecondaryButton/SecondaryButton'; export * from './SplitButton/SplitButton'; +export * from './TwoStateButton/TwoStateButton'; diff --git a/packages/octuple/src/octuple.ts b/packages/octuple/src/octuple.ts index 0f65ce786..df65fefee 100644 --- a/packages/octuple/src/octuple.ts +++ b/packages/octuple/src/octuple.ts @@ -8,6 +8,7 @@ import { NeutralButton, PrimaryButton, SecondaryButton, + TwoStateButton, } from './components/Button'; import { ConfigProvider } from './components/ConfigProvider'; @@ -90,6 +91,7 @@ export { TextInputWidth, Tooltip, TooltipTheme, + TwoStateButton, useBoolean, useMatchMedia, useOnClickOutside,