From 0270db2d087007a64d4e1d96d9441f2ec3e73708 Mon Sep 17 00:00:00 2001 From: Sugan G Date: Mon, 16 Oct 2023 16:53:28 +0530 Subject: [PATCH] selectable button --- .../__snapshots__/ActionHeader.test.jsx.snap | 17 ++++++++++ .../jest/__snapshots__/Alert.test.jsx.snap | 4 +++ .../__snapshots__/ButtonGroup.test.jsx.snap | 2 ++ .../ButtonGroupButton.test.jsx.snap | 9 +++++ packages/terra-button/src/Button.jsx | 31 ++++++++++++++++-- packages/terra-button/src/Button.module.scss | 29 ++++++++++++++++ .../Button.module.scss | 21 ++++++++++++ .../src/orion-fusion-theme/Button.module.scss | 21 ++++++++++++ .../terra-button/tests/jest/Button.test.jsx | 5 +++ .../jest/__snapshots__/Button.test.jsx.snap | 29 ++++++++++++++++ .../button-spec/selected-button.png | Bin 0 -> 3826 bytes .../button-spec/unselected-button.png | Bin 0 -> 3958 bytes .../button-spec/selected-button.png | Bin 0 -> 3265 bytes .../button-spec/unselected-button.png | Bin 0 -> 3652 bytes .../button-spec/selected-button.png | Bin 0 -> 3886 bytes .../button-spec/unselected-button.png | Bin 0 -> 4042 bytes .../terra-button/tests/wdio/button-spec.js | 13 ++++++++ .../terra-dev-site/doc/button/About.1.doc.mdx | 2 ++ .../doc/button/example/ButtonIsSelected.jsx | 31 ++++++++++++++++++ .../test/button/SelectableButton.test.jsx | 31 ++++++++++++++++++ .../__snapshots__/SearchField.test.jsx.snap | 15 +++++++++ .../__snapshots__/StatusView.test.jsx.snap | 2 ++ .../__snapshots__/ToggleButton.test.jsx.snap | 5 +++ 23 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 packages/terra-button/tests/wdio/__snapshots__/reference/clinical-lowlight-theme/en/chrome_tiny/button-spec/selected-button.png create mode 100644 packages/terra-button/tests/wdio/__snapshots__/reference/clinical-lowlight-theme/en/chrome_tiny/button-spec/unselected-button.png create mode 100644 packages/terra-button/tests/wdio/__snapshots__/reference/orion-fusion-theme/en/chrome_tiny/button-spec/selected-button.png create mode 100644 packages/terra-button/tests/wdio/__snapshots__/reference/orion-fusion-theme/en/chrome_tiny/button-spec/unselected-button.png create mode 100644 packages/terra-button/tests/wdio/__snapshots__/reference/terra-default-theme/en/chrome_tiny/button-spec/selected-button.png create mode 100644 packages/terra-button/tests/wdio/__snapshots__/reference/terra-default-theme/en/chrome_tiny/button-spec/unselected-button.png create mode 100644 packages/terra-core-docs/src/terra-dev-site/doc/button/example/ButtonIsSelected.jsx create mode 100644 packages/terra-core-docs/src/terra-dev-site/test/button/SelectableButton.test.jsx diff --git a/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap b/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap index 82c6fbaba19..7b0381ee2ac 100644 --- a/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap +++ b/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap @@ -21,6 +21,7 @@ exports[`ActionHeader correctly applies the theme context className 1`] = ` isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.close" type="button" @@ -75,6 +76,7 @@ exports[`ActionHeader correctly applies the theme context className 1`] = ` isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.maximize" type="button" @@ -115,6 +117,7 @@ exports[`ActionHeader should render an action header with back and close buttons isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.close" type="button" @@ -141,6 +144,7 @@ exports[`ActionHeader should render an action header with back and close buttons isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.back" type="button" @@ -174,6 +178,7 @@ exports[`ActionHeader should render an action header with back button and title isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.back" type="button" @@ -205,6 +210,7 @@ exports[`ActionHeader should render an action header with close button and title isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.close" type="button" @@ -233,6 +239,7 @@ exports[`ActionHeader should render an action header with custom button and titl isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onClick={[Function]} text="Custom Button" type="button" @@ -274,6 +281,7 @@ exports[`ActionHeader should render an action header with maximize button and ti isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.maximize" type="button" @@ -308,6 +316,7 @@ exports[`ActionHeader should render an action header with minimize button and ti isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.minimize" type="button" @@ -334,6 +343,7 @@ exports[`ActionHeader should render an action header with multiple custom button isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onClick={[Function]} text="Custom Button" type="button" @@ -345,6 +355,7 @@ exports[`ActionHeader should render an action header with multiple custom button isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onClick={[Function]} text="Custom Button" type="button" @@ -379,6 +390,7 @@ exports[`ActionHeader should render an action header with next and previous butt isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.previous" type="button" @@ -397,6 +409,7 @@ exports[`ActionHeader should render an action header with next and previous butt isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.next" type="button" @@ -444,6 +457,7 @@ exports[`ActionHeader should render an action header with title, enabled next bu isDisabled={true} isIconOnly={true} isReversed={false} + isSelectable={false} text="Terra.actionHeader.previous" type="button" variant="utility" @@ -461,6 +475,7 @@ exports[`ActionHeader should render an action header with title, enabled next bu isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.next" type="button" @@ -498,6 +513,7 @@ exports[`ActionHeader should render an action header with title, enabled previou isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onClick={[Function]} text="Terra.actionHeader.previous" type="button" @@ -516,6 +532,7 @@ exports[`ActionHeader should render an action header with title, enabled previou isDisabled={true} isIconOnly={true} isReversed={false} + isSelectable={false} text="Terra.actionHeader.next" type="button" variant="utility" diff --git a/packages/terra-alert/tests/jest/__snapshots__/Alert.test.jsx.snap b/packages/terra-alert/tests/jest/__snapshots__/Alert.test.jsx.snap index 4f73464c3b7..9556ae45764 100644 --- a/packages/terra-alert/tests/jest/__snapshots__/Alert.test.jsx.snap +++ b/packages/terra-alert/tests/jest/__snapshots__/Alert.test.jsx.snap @@ -317,6 +317,7 @@ exports[`Alert of type success with an action button text content should render isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onClick={[MockFunction]} text="Action" type="button" @@ -967,6 +968,7 @@ exports[`Dismissible Alert of type custom with action button, custom title and t isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onClick={[MockFunction]} text="Action" type="button" @@ -979,6 +981,7 @@ exports[`Dismissible Alert of type custom with action button, custom title and t isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onClick={[MockFunction]} text="Terra.alert.dismiss" type="button" @@ -1040,6 +1043,7 @@ exports[`Dismissible Alert that includes actions section should render an alert isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onClick={[MockFunction]} text="Terra.alert.dismiss" type="button" diff --git a/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroup.test.jsx.snap b/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroup.test.jsx.snap index f8ffb8e9d12..fb0e4b3177c 100644 --- a/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroup.test.jsx.snap +++ b/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroup.test.jsx.snap @@ -35,6 +35,7 @@ exports[`correctly applies the theme context className 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -88,6 +89,7 @@ exports[`correctly applies the theme context className 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} diff --git a/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroupButton.test.jsx.snap b/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroupButton.test.jsx.snap index 01a37b5abaf..0afc28da99a 100644 --- a/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroupButton.test.jsx.snap +++ b/packages/terra-button-group/tests/jest/__snapshots__/ButtonGroupButton.test.jsx.snap @@ -20,6 +20,7 @@ exports[`correctly applies the theme context className 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} @@ -65,6 +66,7 @@ exports[`should render a default component 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} @@ -84,6 +86,7 @@ exports[`should render as disabled 1`] = ` isDisabled={true} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} @@ -108,6 +111,7 @@ exports[`should render with icon only 1`] = ` isDisabled={false} isIconOnly={true} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} @@ -127,6 +131,7 @@ exports[`should render with onBlur 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} @@ -146,6 +151,7 @@ exports[`should render with onClick 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -166,6 +172,7 @@ exports[`should render with onFocus 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} @@ -185,6 +192,7 @@ exports[`should render with onKeyDown 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} @@ -204,6 +212,7 @@ exports[`should render with onKeyUp 1`] = ` isDisabled={false} isIconOnly={false} isReversed={false} + isSelectable={false} onBlur={[Function]} onFocus={[Function]} onKeyDown={[Function]} diff --git a/packages/terra-button/src/Button.jsx b/packages/terra-button/src/Button.jsx index 52ec953fdf0..f5865468869 100644 --- a/packages/terra-button/src/Button.jsx +++ b/packages/terra-button/src/Button.jsx @@ -101,6 +101,16 @@ const propTypes = { * Sets the button variant. One of `neutral`, `emphasis`, `ghost`, `de-emphasis`, `action` or `utility`. */ variant: PropTypes.oneOf([ButtonVariants.NEUTRAL, ButtonVariants.EMPHASIS, ButtonVariants.GHOST, ButtonVariants['DE-EMPHASIS'], ButtonVariants.ACTION, ButtonVariants.UTILITY]), + /** + * @private + * Whether or not the button can be selected (toggleable button). + */ + isSelectable: PropTypes.bool, + /** + * @private + * Callback function when the state changes. Parameters are (event, toggleState). + */ + onChange: PropTypes.func, }; const defaultProps = { @@ -113,18 +123,20 @@ const defaultProps = { title: undefined, type: ButtonTypes.BUTTON, variant: ButtonVariants.NEUTRAL, + isSelectable: false, }; class Button extends React.Component { constructor(props) { super(props); - this.state = { active: false, focused: false }; + this.state = { active: false, focused: false, isSelected: false }; this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.handleOnBlur = this.handleOnBlur.bind(this); this.handleFocus = this.handleFocus.bind(this); this.handleMouseDown = this.handleMouseDown.bind(this); this.handleClick = this.handleClick.bind(this); + this.handleOnChange = this.handleOnChange.bind(this); this.shouldShowFocus = true; } @@ -144,6 +156,12 @@ class Button extends React.Component { } } + handleOnChange(event) { + if (this.props.onChange && this.props.isSelectable) { + this.props.onChange(event, this.state.isSelected); + } + } + handleClick(event) { // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Button#Clicking_and_focus // Button on Firefox, Safari and IE running on OS X does not receive focus when clicked. @@ -154,6 +172,8 @@ class Button extends React.Component { this.shouldShowFocus = true; } + this.handleOnChange(event); + if (this.props.onClick) { this.props.onClick(event); } @@ -174,7 +194,7 @@ class Button extends React.Component { // Add focus styles for keyboard navigation if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE || event.nativeEvent.keyCode === KeyCode.KEY_RETURN) { - this.setState({ focused: true }); + this.setState(prevState => ({ focused: true, isSelected: !prevState.isSelected })); } if (this.props.onKeyDown) { @@ -212,6 +232,7 @@ class Button extends React.Component { if (this.props.onMouseDown) { this.props.onMouseDown(event); } + this.setState(prevState => ({ isSelected: !prevState.isSelected })); // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus#Notes // If you call HTMLElement.focus() from a mousedown event handler, you must call event.preventDefault() to keep the focus from leaving the HTMLElement. @@ -239,6 +260,8 @@ class Button extends React.Component { onKeyUp, refCallback, title, + isSelectable, + onChange, ...customProps } = this.props; @@ -255,6 +278,7 @@ class Button extends React.Component { { compact: isCompact }, { 'is-active': this.state.active && !isDisabled }, { 'is-focused': this.state.focused && !isDisabled }, + { 'is-selected': isSelectable && this.state.isSelected && !isDisabled }, theme.className, ]), customProps.className, @@ -306,6 +330,8 @@ class Button extends React.Component { if (href) { ComponentType = 'a'; customProps.role = 'button'; + } else if (isSelectable) { + customProps.role = 'checkbox'; } return ( @@ -326,6 +352,7 @@ class Button extends React.Component { onFocus={this.handleFocus} href={href} ref={refCallback} + aria-checked={isSelectable ? this.state.isSelected : undefined} > {buttonLabel} diff --git a/packages/terra-button/src/Button.module.scss b/packages/terra-button/src/Button.module.scss index 8469583e890..71e64537251 100644 --- a/packages/terra-button/src/Button.module.scss +++ b/packages/terra-button/src/Button.module.scss @@ -78,6 +78,35 @@ opacity: var(--terra-button-disabled-opacity, 0.3); pointer-events: none; // prevent pointer interaction for anchor elements } + + &.is-selected.is-focused { + background-color: var(--terra-button-selected-and-focus-background-color, #52585b); + border-color: var(--terra-button-selected-and-focus-border-color, #52585b); + box-shadow: var(--terra-button-selected-and-focus-box-shadow, 0 0 1px 3px rgba(76, 178, 233, 0.5), 0 0 7px 4px rgba(76, 178, 233, 0.35)); + color: var(--terra-button-selected-and-focus-color, #fff); + } + + &.is-selected { + background-color: var(--terra-button-selected-background-color, #52585b); + border-color: var(--terra-button-selected-border-color, #52585b); + box-shadow: var(--terra-button-selected-box-shadow); + color: var(--terra-button-selected-color, #fff); + } + + &:active.is-selected, + &:active { + background-color: var(--terra-button-active-background-color, #52585b); + border-color: var(--terra-button-active-border-color, #52585b); + box-shadow: var(--terra-button-active-box-shadow); + color: var(--terra-button-active-color, #fff); + } + + &:hover.is-selected { + background-color: var(--terra-button-selected-and-hover-background-color, #40474a); + border-color: var(--terra-button-selected-and-hover-border-color, #40474a); + box-shadow: var(--terra-button-selected-and-hover-box-shadow); + color: var(--terra-button-selected-and-hover-color, #fff); + } } .button-label-mac { diff --git a/packages/terra-button/src/clinical-lowlight-theme/Button.module.scss b/packages/terra-button/src/clinical-lowlight-theme/Button.module.scss index 780750b5203..99e0b06f466 100644 --- a/packages/terra-button/src/clinical-lowlight-theme/Button.module.scss +++ b/packages/terra-button/src/clinical-lowlight-theme/Button.module.scss @@ -190,5 +190,26 @@ --terra-button-hover-background-color-utility: transparent; --terra-button-hover-border-color-utility: transparent; --terra-button-hover-color-utility: #b2b5b6; + + // isSelected button + --terra-button-selected-background-color: #1a2023; + --terra-button-selected-border-color: #181b1d; + --terra-button-selected-box-shadow: none; + --terra-button-selected-color: #9b9b9b; + + --terra-button-selected-and-focus-background-color: #1a2023; + --terra-button-selected-and-focus-border-color: #181b1d; + --terra-button-selected-and-focus-box-shadow: 0 0 1px 3px rgba(76, 178, 233, 0.5), 0 0 7px 4px rgba(76, 178, 233, 0.35); + --terra-button-selected-and-focus-color: #9b9b9b; + + --terra-button-active-background-color: #1a2023; + --terra-button-active-border-color: #181b1d; + --terra-button-active-box-shadow: none; + --terra-button-active-color: #9b9b9b; + + --terra-button-selected-and-hover-background-color: #121517; + --terra-button-selected-and-hover-border-color: #181b1d; + --terra-button-selected-and-hover-box-shadow: none; + --terra-button-selected-and-hover-color: #9b9b9b; } } diff --git a/packages/terra-button/src/orion-fusion-theme/Button.module.scss b/packages/terra-button/src/orion-fusion-theme/Button.module.scss index 300172853cf..b78a499bc91 100644 --- a/packages/terra-button/src/orion-fusion-theme/Button.module.scss +++ b/packages/terra-button/src/orion-fusion-theme/Button.module.scss @@ -190,5 +190,26 @@ --terra-button-hover-background-color-utility: transparent; --terra-button-hover-border-color-utility: transparent; --terra-button-hover-color-utility: #004c76; + + // isSelected button + --terra-button-selected-and-hover-background-color: #52585b; + --terra-button-selected-and-hover-border-color: #6f7477; + --terra-button-selected-and-hover-box-shadow: none; + --terra-button-selected-and-hover-color: #fff; + + --terra-button-selected-and-focus-background-color: #52585b; + --terra-button-selected-and-focus-border-color: #6f7477; + --terra-button-selected-and-focus-box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.65), 0 0 2px 3px rgba(76, 178, 233, 0.5), 0 0 6px 4px rgba(76, 178, 233, 0.3); + --terra-button-selected-and-focus-color: #fff; + + --terra-button-selected-background-color: #52585b; + --terra-button-selected-border-color: #6f7477; + --terra-button-selected-box-shadow: inset 0 3rem 1.5rem -2rem #434a4d; + --terra-button-selected-color: #fff; + + --terra-button-active-background-color: #52585b; + --terra-button-active-border-color: #6f7477; + --terra-button-active-box-shadow: inset 0 3rem 1.5rem -2rem #434a4d; + --terra-button-active-color: #fff; } } diff --git a/packages/terra-button/tests/jest/Button.test.jsx b/packages/terra-button/tests/jest/Button.test.jsx index 2df2a4ad71b..57573a9cb81 100644 --- a/packages/terra-button/tests/jest/Button.test.jsx +++ b/packages/terra-button/tests/jest/Button.test.jsx @@ -263,4 +263,9 @@ it('correctly applies the theme context className', () => { ); expect(button).toMatchSnapshot(); }); + +it('should render a selectable button', () => { + const button = shallow( `; +exports[`should render a selectable button 1`] = ` + +`; + exports[`should render a utility button 1`] = `