diff --git a/src/components/CheckBox/CheckBox.stories.tsx b/src/components/CheckBox/CheckBox.stories.tsx index bcd7bc91b..7739e0336 100644 --- a/src/components/CheckBox/CheckBox.stories.tsx +++ b/src/components/CheckBox/CheckBox.stories.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Stories } from '@storybook/addon-docs'; import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { useArgs } from '@storybook/client-api'; import { CheckBox, CheckBoxGroup, @@ -8,6 +9,8 @@ import { LabelAlign, LabelPosition, SelectorSize, + SelectorVariant, + SelectorWidth, } from './'; export default { @@ -99,18 +102,44 @@ export default { ], control: { type: 'radio' }, }, + variant: { + options: [SelectorVariant.Default, SelectorVariant.Pill], + control: { type: 'inline-radio' }, + }, + selectorWidth: { + options: [SelectorWidth.fitContent, SelectorWidth.fill], + control: { type: 'inline-radio' }, + }, }, } as ComponentMeta; -const CheckBox_Story: ComponentStory = (args) => ( - -); +const CheckBox_Story: ComponentStory = (args) => { + const [_, updateArgs] = useArgs(); + const onSelectionChange = (event: React.ChangeEvent) => { + updateArgs({ + ...args, + checked: event.currentTarget.checked, + indeterminate: false, + }); + }; + return ; +}; -const CheckBox_Long_text_Story: ComponentStory = (args) => ( -
- -
-); +const CheckBox_Long_text_Story: ComponentStory = (args) => { + const [_, updateArgs] = useArgs(); + const onSelectionChange = (event: React.ChangeEvent) => { + updateArgs({ + ...args, + checked: event.currentTarget.checked, + indeterminate: false, + }); + }; + return ( +
+ +
+ ); +}; const CheckBoxGroup_Story: ComponentStory = (args) => { const [selected, setSelected] = useState([]); @@ -127,6 +156,7 @@ const CheckBoxGroup_Story: ComponentStory = (args) => { }; export const Check_Box = CheckBox_Story.bind({}); +export const Check_Box_Pill = CheckBox_Story.bind({}); export const Check_Box_Long_Text = CheckBox_Long_text_Story.bind({}); export const Check_Box_Group = CheckBoxGroup_Story.bind({}); @@ -135,6 +165,7 @@ export const Check_Box_Group = CheckBoxGroup_Story.bind({}); // See https://www.npmjs.com/package/babel-plugin-named-exports-order export const __namedExportsOrder = [ 'Check_Box', + 'Check_Box_Pill', 'Check_Box_Long_Text', 'Check_Box_Group', ]; @@ -142,8 +173,10 @@ export const __namedExportsOrder = [ const checkBoxArgs: Object = { allowDisabledFocus: false, ariaLabel: 'Label', + checked: true, classNames: 'my-checkbox-class', disabled: false, + indeterminate: false, name: 'myCheckBoxName', value: 'label', label: 'Label', @@ -151,14 +184,21 @@ const checkBoxArgs: Object = { labelAlign: LabelAlign.Center, id: 'myCheckBoxId', defaultChecked: false, + selectorWidth: SelectorWidth.fitContent, size: SelectorSize.Medium, toggle: false, + variant: SelectorVariant.Default, }; Check_Box.args = { ...checkBoxArgs, }; +Check_Box_Pill.args = { + ...checkBoxArgs, + variant: SelectorVariant.Pill, +}; + Check_Box_Long_Text.args = { ...checkBoxArgs, label: @@ -191,5 +231,7 @@ Check_Box_Group.args = { }, ], layout: 'vertical', + selectorWidth: SelectorWidth.fitContent, size: SelectorSize.Medium, + variant: SelectorVariant.Default, }; diff --git a/src/components/CheckBox/CheckBox.test.tsx b/src/components/CheckBox/CheckBox.test.tsx index 345bd2db9..771d191d8 100644 --- a/src/components/CheckBox/CheckBox.test.tsx +++ b/src/components/CheckBox/CheckBox.test.tsx @@ -2,7 +2,13 @@ import React from 'react'; import Enzyme, { mount } from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import MatchMediaMock from 'jest-matchmedia-mock'; -import { CheckBox, CheckBoxGroup, SelectorSize } from './'; +import { + CheckBox, + CheckBoxGroup, + SelectorSize, + SelectorWidth, + SelectorVariant, +} from './'; Enzyme.configure({ adapter: new Adapter() }); @@ -29,11 +35,34 @@ describe('CheckBox', () => { expect(wrapper.find('.toggle')).toBeTruthy(); }); - test('simulate disabled CheckBox', () => { + test('Simulate disabled CheckBox', () => { const wrapper = mount(); wrapper.find('input').html().includes('disabled=""'); }); + test('Simulate indeterminate CheckBox', () => { + const wrapper = mount(); + wrapper.find('input').html().includes('indeterminate'); + }); + + test('Checkbox is pill', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.selector-pill')).toBeTruthy(); + }); + + test('Checkbox is fill pill', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.selector-pill-stretch')).toBeTruthy(); + }); + test('CheckBox is large', () => { const wrapper = mount( diff --git a/src/components/CheckBox/CheckBox.tsx b/src/components/CheckBox/CheckBox.tsx index 7e70bdfd7..6198bf512 100644 --- a/src/components/CheckBox/CheckBox.tsx +++ b/src/components/CheckBox/CheckBox.tsx @@ -2,10 +2,18 @@ import React, { FC, Ref, useContext, useEffect, useRef, useState } from 'react'; import DisabledContext, { Disabled } from '../ConfigProvider/DisabledContext'; import { SizeContext, Size } from '../ConfigProvider'; import { generateId, mergeClasses } from '../../shared/utilities'; -import { CheckboxProps, LabelAlign, LabelPosition, SelectorSize } from './'; +import { + CheckboxProps, + LabelAlign, + LabelPosition, + SelectorSize, + SelectorVariant, + SelectorWidth, +} from './'; import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia'; import { FormItemInputContext } from '../Form/Context'; import { useCanvasDirection } from '../../hooks/useCanvasDirection'; +import { useMergedRefs } from '../../hooks/useMergedRefs'; import styles from './checkbox.module.scss'; @@ -24,15 +32,18 @@ export const CheckBox: FC = React.forwardRef( disabled = false, formItemInput = false, id, + indeterminate = false, label, labelPosition = LabelPosition.End, labelAlign = LabelAlign.Center, name, onChange, + selectorWidth = SelectorWidth.fitContent, size = SelectorSize.Medium, style, toggle = false, value, + variant = SelectorVariant.Default, 'data-test-id': dataTestId, }, ref: Ref @@ -44,10 +55,20 @@ export const CheckBox: FC = React.forwardRef( const htmlDir: string = useCanvasDirection(); + const internalRef: React.MutableRefObject = + useRef(null); + + const mergedRef: (node: HTMLInputElement) => void = useMergedRefs( + internalRef, + ref + ); + const checkBoxId = useRef(id || generateId()); const [isChecked, setIsChecked] = useState( defaultChecked || checked ); + const [isIndeterminate, setIsIndeterminate] = + useState(indeterminate); const { isFormItemInput } = useContext(FormItemInputContext); const mergedFormItemInput: boolean = isFormItemInput || formItemInput; @@ -62,12 +83,35 @@ export const CheckBox: FC = React.forwardRef( ? size : contextuallySized || size; - useEffect(() => { + useEffect((): void => { setIsChecked(checked); }, [checked]); + useEffect((): void => { + setIsIndeterminate(indeterminate); + if (internalRef.current) { + internalRef.current.indeterminate = indeterminate; + } + }, [indeterminate]); + const checkboxWrapperClassNames: string = mergeClasses([ styles.selector, + { + [styles.selectorPill]: variant === SelectorVariant.Pill, + }, + { + [styles.selectorPillActive]: + variant === SelectorVariant.Pill && isChecked, + }, + { + [styles.selectorPillIndeterminate]: + variant === SelectorVariant.Pill && isIndeterminate, + }, + { + [styles.selectorPillStretch]: + variant === SelectorVariant.Pill && + selectorWidth === SelectorWidth.fill, + }, { [styles.selectorSmall]: mergedSize === SelectorSize.Flex && largeScreenActive, @@ -126,7 +170,7 @@ export const CheckBox: FC = React.forwardRef( data-test-id={dataTestId} > = React.forwardRef( labelAlign = LabelAlign.Center, layout = 'vertical', onChange, + selectorWidth = SelectorWidth.fitContent, size = SelectorSize.Medium, style, value, + variant = SelectorVariant.Default, ...rest }, ref: Ref @@ -118,7 +122,9 @@ export const CheckBoxGroup: FC = React.forwardRef( onChange?.(newValue); } }} + selectorWidth={selectorWidth} size={mergedSize} + variant={variant} /> ))} diff --git a/src/components/CheckBox/Checkbox.types.ts b/src/components/CheckBox/Checkbox.types.ts index 1ccd5aeb7..e0ae686b9 100644 --- a/src/components/CheckBox/Checkbox.types.ts +++ b/src/components/CheckBox/Checkbox.types.ts @@ -22,6 +22,16 @@ export enum SelectorSize { Small = 'small', } +export enum SelectorWidth { + fitContent = 'fitContent', + fill = 'fill', +} + +export enum SelectorVariant { + Default = 'default', + Pill = 'pill', +} + export interface CheckboxProps extends OcBaseProps { /** * Allows focus on the checkbox when it's disabled. @@ -53,6 +63,10 @@ export interface CheckboxProps extends OcBaseProps { * @default false */ formItemInput?: boolean; + /** + * Whether or not the checkbox state is indeterminate. + */ + indeterminate?: boolean; /** * The checkbox input name. */ @@ -75,6 +89,12 @@ export interface CheckboxProps extends OcBaseProps { * The checkbox onChange event handler. */ onChange?: React.ChangeEventHandler; + /** + * The checkbox width type + * Use when variant is `SelectorVariant.Pill` + * @default fitContent + */ + selectorWidth?: SelectorWidth; /** * The checkbox size. * @default SelectorSize.Medium @@ -89,6 +109,11 @@ export interface CheckboxProps extends OcBaseProps { * The checkbox value. */ value?: CheckboxValueType; + /** + * Determines the checkbox variant. + * @default SelectorVariant.Default + */ + variant?: SelectorVariant; } export interface CheckboxGroupProps @@ -139,6 +164,12 @@ export interface CheckboxGroupProps * @param checkedValue */ onChange?: (checkedValue: CheckboxValueType[]) => void; + /** + * The checkbox group width type + * Use when variant is `SelectorVariant.Pill` + * @default fitContent + */ + selectorWidth?: SelectorWidth; /** * The checkbox size. * @default SelectorSize.Medium @@ -148,4 +179,9 @@ export interface CheckboxGroupProps * The checkbox value. */ value?: CheckboxValueType[]; + /** + * Determines the checkbox group variant. + * @default SelectorVariant.Default + */ + variant?: SelectorVariant; } diff --git a/src/components/CheckBox/checkbox.module.scss b/src/components/CheckBox/checkbox.module.scss index 5cb420aeb..3eb233ddf 100644 --- a/src/components/CheckBox/checkbox.module.scss +++ b/src/components/CheckBox/checkbox.module.scss @@ -142,9 +142,6 @@ } & + label { - transition: all $motion-duration-extra-fast $motion-ease-out-back - $motion-delay-s; - .checkmark { background: var(--check-box-checked-background-color); border: var(--check-box-checked-border); @@ -220,6 +217,46 @@ } } + input:indeterminate { + &[disabled] { + cursor: not-allowed; + } + + & + label { + .checkmark { + background: var(--check-box-checked-background-color); + border: var(--check-box-checked-border); + + &:after { + border-width: 1px; + height: 1px; + left: $space-xxxs; + opacity: 1; + top: 5px; + transform: none; + transition: opacity $motion-duration-extra-fast $motion-ease-out-back + $motion-delay-s; + width: $space-xs; + } + } + } + + &:not(.disabled):not([disabled]):active + label { + .checkmark { + transform: scale(0.98); + background: var(--check-box-checked-active-background-color); + border: var(--check-box-checked-active-border); + } + } + + &:not(.disabled):not([disabled]):hover + label { + .checkmark { + background: var(--check-box-checked-hover-background-color); + border: var(--check-box-checked-hover-border); + } + } + } + label { display: flex; align-items: center; @@ -239,6 +276,151 @@ } } + &-pill { + background: var(--check-box-pill-container-background-color); + border-color: var(--check-box-pill-container-border-color); + border-radius: var(--check-box-pill-container-border-radius); + border-style: var(--check-box-pill-container-border-style); + border-width: var(--check-box-pill-container-border-width); + color: var(--check-box-pill-container-text-color); + transition: all $motion-duration-extra-fast $motion-easing-easeinout 0s; + + &-active, + &-indeterminate { + background: var(--check-box-pill-container-active-background-color); + border-color: var(--check-box-pill-container-active-border-color); + color: var(--check-box-pill-container-active-text-color); + } + + label { + height: auto; + min-height: 36px; + padding: $selector-padding-vertical-medium + $selector-padding-horizontal-medium; + } + + input { + & + label { + .checkmark { + background: var(--check-box-in-pill-background-color); + border: var(--check-box-in-pill-border); + + &:after { + border-color: var(--check-box-in-pill-mark-color); + } + } + } + + &:not(.disabled):not([disabled]):active + label { + .checkmark { + background: var(--check-box-in-pill-active-background-color); + border: var(--check-box-in-pill-active-border); + } + } + + &:not(.disabled):not([disabled]):hover + label { + .checkmark { + background: var(--check-box-in-pill-hover-background-color); + border: var(--check-box-in-pill-hover-border); + } + } + } + + input:checked { + & + label { + .checkmark { + background: var(--check-box-in-pill-checked-background-color); + border: var(--check-box-in-pill-checked-border); + } + } + + &:not(.disabled):not([disabled]):active + label { + .checkmark { + background: var(--check-box-in-pill-checked-active-background-color); + border: var(--check-box-in-pill-checked-active-border); + } + } + + &:not(.disabled):not([disabled]):hover + label { + .checkmark { + background: var(--check-box-in-pill-checked-hover-background-color); + border: var(--check-box-in-pill-checked-hover-border); + } + } + } + + input:indeterminate { + & + label { + .checkmark { + background: var(--check-box-in-pill-checked-background-color); + border: var(--check-box-in-pill-checked-border); + } + } + + &:not(.disabled):not([disabled]):active + label { + .checkmark { + background: var(--check-box-in-pill-checked-active-background-color); + border: var(--check-box-in-pill-checked-active-border); + } + } + + &:not(.disabled):not([disabled]):hover + label { + .checkmark { + background: var(--check-box-in-pill-checked-hover-background-color); + border: var(--check-box-in-pill-checked-hover-border); + } + } + } + + &:hover:not([disabled]) { + background: var(--check-box-pill-container-hover-background-color); + border-color: var(--check-box-pill-container-hover-border-color); + color: var(--check-box-pill-container-hover-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .checkmark { + background: var(--check-box-in-pill-hover-background-color); + border: var(--check-box-in-pill-hover-border); + } + } + } + } + + &:active:not([disabled]) { + background: var(--check-box-pill-container-active-background-color); + border-color: var(--check-box-pill-container-active-border-color); + color: var(--check-box-pill-container-active-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .checkmark { + background: var(--check-box-in-pill-active-background-color); + border: var(--check-box-in-pill-active-border); + } + } + } + } + + &:disabled, + &.disabled { + opacity: $disabled-alpha-value; + cursor: not-allowed; + + input { + &[disabled] { + & + label { + opacity: 1; + } + } + } + } + + &-stretch { + width: 100%; + } + } + &-large { input { & + label { @@ -289,6 +471,18 @@ } } + input:indeterminate { + & + label { + .checkmark { + &:after { + left: 3px; + top: 7px; + width: 10px; + } + } + } + } + .selector-label { font-size: $text-font-size-3; @@ -300,6 +494,14 @@ margin-right: $space-m; } } + + &.selector-pill { + label { + min-height: 44px; + padding: $selector-padding-vertical-large + $selector-padding-horizontal-large; + } + } } &-medium { @@ -352,6 +554,18 @@ } } + input:indeterminate { + & + label { + .checkmark { + &:after { + left: $space-xxxs; + top: 5px; + width: $space-xs; + } + } + } + } + .selector-label { font-size: $text-font-size-2; @@ -363,6 +577,14 @@ margin-right: $space-xs; } } + + &.selector-pill { + label { + min-height: 36px; + padding: $selector-padding-vertical-medium + $selector-padding-horizontal-medium; + } + } } &-small { @@ -411,6 +633,18 @@ } } + input:indeterminate { + & + label { + .checkmark { + &:after { + left: $space-xxxs; + top: $space-xxs; + width: 6px; + } + } + } + } + .selector-label { font-size: $text-font-size-1; @@ -422,6 +656,14 @@ margin-right: $space-xxs; } } + + &.selector-pill { + label { + min-height: 28px; + padding: $selector-padding-vertical-small + $selector-padding-horizontal-small; + } + } } &.disabled { @@ -439,7 +681,8 @@ } } - input:checked { + input:checked, + input:indeterminate { cursor: not-allowed; } } @@ -517,6 +760,15 @@ } } + &:indeterminate { + &:focus-visible + label { + .checkmark { + outline: var(--focus-visible-outline); + outline-offset: $selector-outline-offset; + } + } + } + &:focus-visible + label { .checkmark { border: var(--check-box-focus-visible-border); @@ -553,6 +805,14 @@ } } + &:indeterminate { + &:focus-visible + label { + .checkmark { + outline: none; + } + } + } + &:focus-visible + label { .checkmark { outline: none; @@ -587,6 +847,14 @@ } } + &:indeterminate { + &:focus-visible + label { + .checkmark { + outline: none; + } + } + } + &:focus-visible + label { .checkmark { outline: none; @@ -606,6 +874,98 @@ } } } + + .selector-pill:has(:focus-visible) { + &:focus-within { + background: var(--check-box-pill-container-active-background-color); + box-shadow: var(--focus-visible-box-shadow); + color: var(--check-box-pill-container-active-text-color); + + input[type='checkbox'] { + &:checked { + &:focus-visible + label { + .checkmark { + outline: none; + } + } + } + + &:indeterminate { + &:focus-visible + label { + .checkmark { + outline: none; + } + } + } + + &:focus-visible + label { + .checkmark { + border: var(--check-box-in-pill-focus-visible-border); + outline: none; + + &:after { + border-color: var(--check-box-in-pill-focus-visible-mark-color); + } + } + } + + &[disabled] { + &:checked { + &:focus-visible + label { + .checkmark { + outline: none; + } + } + } + + &:indeterminate { + &:focus-visible + label { + .checkmark { + outline: none; + } + } + } + + &:focus-visible + label { + .checkmark { + outline: none; + border: var(--check-box-in-pill-border); + } + } + } + } + + &:hover:not([disabled]) { + background: var(--check-box-pill-container-hover-background-color); + border-color: var(--check-box-pill-container-hover-border-color); + color: var(--check-box-pill-container-hover-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .checkmark { + background: var(--check-box-in-pill-hover-background-color); + border: var(--check-box-in-pill-hover-border); + } + } + } + } + + &:active:not([disabled]) { + background: var(--check-box-pill-container-active-background-color); + border-color: var(--check-box-pill-container-active-border-color); + color: var(--check-box-pill-container-active-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .checkmark { + background: var(--check-box-in-pill-active-background-color); + border: var(--check-box-in-pill-active-border); + } + } + } + } + } + } } .checkbox-group { diff --git a/src/components/Progress/Internal/OcCircle.tsx b/src/components/Progress/Internal/OcCircle.tsx index 069ae07e5..03af56d97 100644 --- a/src/components/Progress/Internal/OcCircle.tsx +++ b/src/components/Progress/Internal/OcCircle.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import { MAX_PERCENT, useTransitionDuration } from './Common'; import type { OcProgressProps } from './OcProgress.types'; -import { mergeClasses, uniqueId } from '../../../shared/utilities'; +import { uniqueId } from '../../../shared/utilities'; import styles from '../progress.module.scss'; function stripPercentToNumber(percent: string): number { diff --git a/src/components/RadioButton/Radio.types.ts b/src/components/RadioButton/Radio.types.ts index 7430b26bd..f6166e371 100644 --- a/src/components/RadioButton/Radio.types.ts +++ b/src/components/RadioButton/Radio.types.ts @@ -1,6 +1,12 @@ import React from 'react'; import { OcBaseProps } from '../OcBase'; -import { LabelAlign, LabelPosition, SelectorSize } from '../CheckBox'; +import { + LabelAlign, + LabelPosition, + SelectorSize, + SelectorWidth, + SelectorVariant, +} from '../CheckBox'; import { ConfigContextProps, Size } from '../ConfigProvider'; export type RadioButtonValue = string | number; @@ -67,6 +73,12 @@ export interface RadioButtonProps extends OcBaseProps { * The radio button onChange event handler. */ onChange?: React.ChangeEventHandler; + /** + * The radio button width type + * Use when variant is `SelectorVariant.Pill` + * @default fitContent + */ + selectorWidth?: SelectorWidth; /** * The radio button size. * @default SelectorSize.Medium @@ -76,6 +88,11 @@ export interface RadioButtonProps extends OcBaseProps { * The value of the input. */ value?: RadioButtonValue; + /** + * Determines the radio button variant. + * @default SelectorVariant.Default + */ + variant?: SelectorVariant; } export interface RadioGroupProps extends OcBaseProps { @@ -123,6 +140,12 @@ export interface RadioGroupProps extends OcBaseProps { * The radio button onChange event handler. */ onChange?: React.ChangeEventHandler; + /** + * The radio group width type + * Use when variant is `SelectorVariant.Pill` + * @default fitContent + */ + selectorWidth?: SelectorWidth; /** * The radio group size. * @default SelectorSize.Medium @@ -132,4 +155,9 @@ export interface RadioGroupProps extends OcBaseProps { * The input radio default selected value. */ value?: RadioButtonValue; + /** + * Determines the radio group variant. + * @default SelectorVariant.Default + */ + variant?: SelectorVariant; } diff --git a/src/components/RadioButton/RadioButton.stories.tsx b/src/components/RadioButton/RadioButton.stories.tsx index 49ab06de3..28838e7f3 100644 --- a/src/components/RadioButton/RadioButton.stories.tsx +++ b/src/components/RadioButton/RadioButton.stories.tsx @@ -3,7 +3,13 @@ import { Stories } from '@storybook/addon-docs'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { Label } from '../Label'; import { RadioButton, RadioButtonValue, RadioGroup } from './'; -import { LabelAlign, LabelPosition, SelectorSize } from '../CheckBox'; +import { + LabelAlign, + LabelPosition, + SelectorSize, + SelectorVariant, + SelectorWidth, +} from '../CheckBox'; import { Stack } from '../Stack'; export default { @@ -112,6 +118,14 @@ export default { ], control: { type: 'radio' }, }, + variant: { + options: [SelectorVariant.Default, SelectorVariant.Pill], + control: { type: 'inline-radio' }, + }, + selectorWidth: { + options: [SelectorWidth.fitContent, SelectorWidth.fill], + control: { type: 'inline-radio' }, + }, }, } as ComponentMeta; @@ -294,6 +308,7 @@ const RadioGroup_With_Custom_Label_Story: ComponentStory = ( }; export const Radio_Button = RadioButton_Story.bind({}); +export const Radio_Button_Pill = RadioButton_Story.bind({}); export const Radio_Button_Long_Text = RadioButtonLongText_Story.bind({}); export const Radio_Group = RadioGroup_Story.bind({}); export const Bespoke_Radio_Group = Bespoke_RadioGroup_Story.bind({}); @@ -307,6 +322,7 @@ export const RadioGroup_With_Custom_Label = // See https://www.npmjs.com/package/babel-plugin-named-exports-order export const __namedExportsOrder = [ 'Radio_Button', + 'Radio_Button_Pill', 'Radio_Button_Long_Text', 'Radio_Group', 'Bespoke_Radio_Group', @@ -325,14 +341,21 @@ const radioButtonArgs: Object = { labelPosition: LabelPosition.End, labelAlign: LabelAlign.Center, name: 'myRadioButtonName', + selectorWidth: SelectorWidth.fitContent, size: SelectorSize.Medium, value: 'Label1', + variant: SelectorVariant.Default, }; Radio_Button.args = { ...radioButtonArgs, }; +Radio_Button_Pill.args = { + ...radioButtonArgs, + variant: SelectorVariant.Pill, +}; + Radio_Button_Long_Text.args = { ...radioButtonArgs, label: @@ -351,7 +374,9 @@ Radio_Group.args = { value: `Radio${i}`, })), layout: 'vertical', + selectorWidth: SelectorWidth.fitContent, size: SelectorSize.Medium, + variant: SelectorVariant.Default, value: 'Radio1', }; @@ -393,6 +418,8 @@ RadioGroup_With_Custom_Label.args = { value: `Radio${i}`, })), layout: 'vertical', + selectorWidth: SelectorWidth.fitContent, size: SelectorSize.Medium, + variant: SelectorVariant.Default, value: 'Radio1', }; diff --git a/src/components/RadioButton/RadioButton.test.tsx b/src/components/RadioButton/RadioButton.test.tsx index 622cfc535..91a5b543f 100644 --- a/src/components/RadioButton/RadioButton.test.tsx +++ b/src/components/RadioButton/RadioButton.test.tsx @@ -3,7 +3,7 @@ import Enzyme, { mount } from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import MatchMediaMock from 'jest-matchmedia-mock'; import { RadioButton } from './'; -import { SelectorSize } from '../CheckBox'; +import { SelectorSize, SelectorWidth, SelectorVariant } from '../CheckBox'; Enzyme.configure({ adapter: new Adapter() }); @@ -18,33 +18,51 @@ describe('RadioButton', () => { matchMedia.clear(); }); - it('Radio button renders', () => { + test('Radio button renders', () => { const wrapper = mount(); expect( wrapper.containsMatchingElement() ).toEqual(true); }); - it('simulate disabled RadioButton', () => { + test('simulate disabled RadioButton', () => { const wrapper = mount(); wrapper.find('input').html().includes('disabled=""'); }); - it('RadioButton is large', () => { + test('RadioButton is pill', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.selector-pill')).toBeTruthy(); + }); + + test('RadioButton is fill pill', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.selector-pill-stretch')).toBeTruthy(); + }); + + test('RadioButton is large', () => { const wrapper = mount( ); expect(wrapper.find('.selector-large')).toBeTruthy(); }); - it('RadioButton is medium', () => { + test('RadioButton is medium', () => { const wrapper = mount( ); expect(wrapper.find('.selector-medium')).toBeTruthy(); }); - it('RadioButton is small', () => { + test('RadioButton is small', () => { const wrapper = mount( ); diff --git a/src/components/RadioButton/RadioButton.tsx b/src/components/RadioButton/RadioButton.tsx index 02b475153..d43f36668 100644 --- a/src/components/RadioButton/RadioButton.tsx +++ b/src/components/RadioButton/RadioButton.tsx @@ -2,7 +2,13 @@ import React, { FC, Ref, useContext, useEffect, useRef, useState } from 'react'; import DisabledContext, { Disabled } from '../ConfigProvider/DisabledContext'; import { SizeContext, Size } from '../ConfigProvider'; import { RadioButtonProps, RadioButtonValue } from './'; -import { LabelAlign, LabelPosition, SelectorSize } from '../CheckBox'; +import { + LabelAlign, + LabelPosition, + SelectorSize, + SelectorVariant, + SelectorWidth, +} from '../CheckBox'; import { mergeClasses, generateId } from '../../shared/utilities'; import { useRadioGroup } from './RadioGroup.context'; import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia'; @@ -30,9 +36,11 @@ export const RadioButton: FC = React.forwardRef( labelPosition = LabelPosition.End, labelAlign = LabelAlign.Center, onChange, + selectorWidth = SelectorWidth.fitContent, size = SelectorSize.Medium, style, value = '', + variant = SelectorVariant.Default, 'data-test-id': dataTestId, }, ref: Ref @@ -68,6 +76,19 @@ export const RadioButton: FC = React.forwardRef( const selectorClassNames: string = mergeClasses([ styles.selector, + { + [styles.selectorPill]: variant === SelectorVariant.Pill, + }, + { + [styles.selectorPillActive]: + variant === SelectorVariant.Pill && + (radioGroupContext ? isActive : selectedValue === value && checked), + }, + { + [styles.selectorPillStretch]: + variant === SelectorVariant.Pill && + selectorWidth === SelectorWidth.fill, + }, { [styles.selectorSmall]: mergedSize === SelectorSize.Flex && largeScreenActive, diff --git a/src/components/RadioButton/RadioGroup.tsx b/src/components/RadioButton/RadioGroup.tsx index e8423a69c..a9e8761e5 100644 --- a/src/components/RadioButton/RadioGroup.tsx +++ b/src/components/RadioButton/RadioGroup.tsx @@ -2,7 +2,13 @@ import React, { FC, Ref, useContext } from 'react'; import DisabledContext, { Disabled } from '../ConfigProvider/DisabledContext'; import { SizeContext, Size } from '../ConfigProvider'; import { RadioButtonProps, RadioGroupProps, RadioButton } from './'; -import { LabelAlign, LabelPosition, SelectorSize } from '../CheckBox'; +import { + LabelAlign, + LabelPosition, + SelectorSize, + SelectorVariant, + SelectorWidth, +} from '../CheckBox'; import { RadioGroupProvider } from './RadioGroup.context'; import { mergeClasses } from '../../shared/utilities'; import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia'; @@ -28,9 +34,11 @@ export const RadioGroup: FC = React.forwardRef( labelAlign = LabelAlign.Center, layout = 'vertical', onChange, + selectorWidth = SelectorWidth.fitContent, size = SelectorSize.Medium, style, value, + variant = SelectorVariant.Default, ...rest }, ref: Ref @@ -99,7 +107,9 @@ export const RadioGroup: FC = React.forwardRef( {...item} labelPosition={labelPosition} labelAlign={labelAlign} + selectorWidth={selectorWidth} size={mergedSize} + variant={variant} /> ))} diff --git a/src/components/RadioButton/radio.module.scss b/src/components/RadioButton/radio.module.scss index ffe870487..e1d06bd59 100644 --- a/src/components/RadioButton/radio.module.scss +++ b/src/components/RadioButton/radio.module.scss @@ -143,6 +143,132 @@ } } + &-pill { + background: var(--radio-button-pill-container-background-color); + border-color: var(--radio-button-pill-container-border-color); + border-radius: var(--radio-button-pill-container-border-radius); + border-style: var(--radio-button-pill-container-border-style); + border-width: var(--radio-button-pill-container-border-width); + color: var(--radio-button-pill-container-text-color); + transition: all $motion-duration-extra-fast $motion-easing-easeinout 0s; + + &-active, + &-indeterminate { + background: var(--radio-button-pill-container-active-background-color); + border-color: var(--radio-button-pill-container-active-border-color); + color: var(--radio-button-pill-container-active-text-color); + } + + label { + height: auto; + min-height: 36px; + padding: $selector-padding-vertical-medium + $selector-padding-horizontal-medium; + } + + input { + & + label { + .radio-button { + background: var(--radio-button-in-pill-background-color); + border: var(--radio-button-in-pill-border); + + &:after { + border-color: var(--radio-button-in-pill-pip-color); + } + } + } + + &:not(.disabled):not([disabled]):active + label { + .radio-button { + background: var(--radio-button-in-pill-active-background-color); + border: var(--radio-button-in-pill-active-border); + } + } + + &:not(.disabled):not([disabled]):hover + label { + .radio-button { + background: var(--radio-button-in-pill-hover-background-color); + border: var(--radio-button-in-pill-hover-border); + } + } + } + + input:checked { + & + label { + .radio-button { + background: var(--radio-button-in-pill-checked-background-color); + border: var(--radio-button-in-pill-checked-border); + } + } + + &:not(.disabled):not([disabled]):active + label { + .radio-button { + background: var( + --radio-button-in-pill-checked-active-background-color + ); + border: var(--radio-button-in-pill-checked-active-border); + } + } + + &:not(.disabled):not([disabled]):hover + label { + .radio-button { + background: var( + --radio-button-in-pill-checked-hover-background-color + ); + border: var(--radio-button-in-pill-checked-hover-border); + } + } + } + + &:hover:not([disabled]) { + background: var(--radio-button-pill-container-hover-background-color); + border-color: var(--radio-button-pill-container-hover-border-color); + color: var(--radio-button-pill-container-hover-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .checkmark { + background: var(--radio-button-in-pill-hover-background-color); + border: var(--radio-button-in-pill-hover-border); + } + } + } + } + + &:active:not([disabled]) { + background: var(--radio-button-pill-container-active-background-color); + border-color: var(--radio-button-pill-container-active-border-color); + color: var(--radio-button-pill-container-active-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .radio-button { + background: var(--radio-button-in-pill-active-background-color); + border: var(--radio-button-in-pill-active-border); + } + } + } + } + + &:disabled, + &.disabled { + opacity: $disabled-alpha-value; + cursor: not-allowed; + + input { + &[disabled] { + & + label { + opacity: 1; + } + } + } + } + + &-stretch { + width: 100%; + } + } + &-large { input { & + label { @@ -181,6 +307,14 @@ margin-right: $space-m; } } + + &.selector-pill { + label { + min-height: 44px; + padding: $selector-padding-vertical-large + $selector-padding-horizontal-large; + } + } } &-medium { @@ -221,6 +355,14 @@ margin-right: $space-xs; } } + + &.selector-pill { + label { + min-height: 36px; + padding: $selector-padding-vertical-medium + $selector-padding-horizontal-medium; + } + } } &-small { @@ -261,6 +403,14 @@ margin-right: $space-xxs; } } + + &.selector-pill { + label { + min-height: 28px; + padding: $selector-padding-vertical-small + $selector-padding-horizontal-small; + } + } } &.disabled { @@ -395,6 +545,82 @@ } } } + + .selector-pill:has(:focus-visible) { + &:focus-within { + background: var(--radio-button-pill-container-active-background-color); + box-shadow: var(--focus-visible-box-shadow); + color: var(--radio-button-pill-container-active-text-color); + + input[type='radio'] { + &:checked { + &:focus-visible + label { + .radio-button { + outline: none; + } + } + } + + &:focus-visible + label { + .radio-button { + border: var(--radio-button-in-pill-focus-visible-border); + outline: none; + + &:after { + border-color: var(--radio-button-in-pill-focus-visible-pip-color); + } + } + } + + &[disabled] { + &:checked { + &:focus-visible + label { + .radio-button { + outline: none; + } + } + } + + &:focus-visible + label { + .radio-button { + outline: none; + border: var(--radio-button-in-pill-border); + } + } + } + } + + &:hover:not([disabled]) { + background: var(--radio-button-pill-container-hover-background-color); + border-color: var(--radio-button-pill-container-hover-border-color); + color: var(--radio-button-pill-container-hover-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .radio-button { + background: var(--radio-button-in-pill-hover-background-color); + border: var(--radio-button-in-pill-hover-border); + } + } + } + } + + &:active:not([disabled]) { + background: var(--radio-button-pill-container-active-background-color); + border-color: var(--radio-button-pill-container-active-border-color); + color: var(--radio-button-pill-container-active-text-color); + + input { + &:not(.disabled):not([disabled]) + label { + .radio-button { + background: var(--radio-button-in-pill-active-background-color); + border: var(--radio-button-in-pill-active-border); + } + } + } + } + } + } } .radio-group { diff --git a/src/octuple.ts b/src/octuple.ts index e29993adb..7744e5a82 100644 --- a/src/octuple.ts +++ b/src/octuple.ts @@ -44,6 +44,8 @@ import { LabelPosition, LabelAlign, SelectorSize, + SelectorVariant, + SelectorWidth, } from './components/CheckBox'; import { @@ -365,6 +367,8 @@ export { SelectShape, SelectSize, SelectorSize, + SelectorVariant, + SelectorWidth, SearchBox, SecondaryButton, Shape, diff --git a/src/styles/themes/_default-theme.scss b/src/styles/themes/_default-theme.scss index 0596e8166..dff2728e8 100644 --- a/src/styles/themes/_default-theme.scss +++ b/src/styles/themes/_default-theme.scss @@ -1123,6 +1123,76 @@ --check-box-mark-color: var(--primary-secondary-color); --check-box-focus-visible-mark-color: var(--primary-color); --check-box-text-color: var(--primary-secondary-color); + + --check-box-pill-container-background-color: var(--grey-background1-color); + --check-box-pill-container-active-background-color: var( + --accent-background2-color + ); + --check-box-pill-container-hover-background-color: var( + --accent-background1-color + ); + --check-box-pill-container-border-color: var(--accent-background2-color); + --check-box-pill-container-active-border-color: var( + --accent-background2-color + ); + --check-box-pill-container-hover-border-color: var( + --accent-background1-color + ); + --check-box-pill-container-border-radius: var(--border-radius-xl); + --check-box-pill-container-border-style: solid; + --check-box-pill-container-border-width: 0; + --check-box-pill-container-text-color: var(--text-secondary-color); + --check-box-pill-container-active-text-color: var(--primary-color); + --check-box-pill-container-hover-text-color: var(--primary-color); + --check-box-in-pill-background-color: transparent; + --check-box-in-pill-active-background-color: transparent; + --check-box-in-pill-hover-background-color: transparent; + --check-box-in-pill-checked-background-color: transparent; + --check-box-in-pill-checked-active-background-color: transparent; + --check-box-in-pill-checked-hover-background-color: transparent; + --check-box-in-pill-border-color: var(--grey-secondary-color); + --check-box-in-pill-active-border-color: var(--primary-secondary-color); + --check-box-in-pill-hover-border-color: var(--primary-secondary-color); + --check-box-in-pill-checked-border-color: var(--primary-secondary-color); + --check-box-in-pill-checked-active-border-color: var( + --primary-secondary-color + ); + --check-box-in-pill-checked-hover-border-color: var( + --primary-secondary-color + ); + --check-box-in-pill-focus-visible-border-color: var( + --primary-secondary-color + ); + --check-box-in-pill-border-width: 2px; + --check-box-in-pill-border-style: solid; + --check-box-in-pill-border: var(--check-box-in-pill-border-width) + var(--check-box-in-pill-border-style) var(--check-box-in-pill-border-color); + --check-box-in-pill-active-border: var(--check-box-in-pill-border-width) + var(--check-box-in-pill-border-style) + var(--check-box-in-pill-active-border-color); + --check-box-in-pill-hover-border: var(--check-box-in-pill-border-width) + var(--check-box-in-pill-border-style) + var(--check-box-in-pill-hover-border-color); + --check-box-in-pill-checked-border: var(--check-box-in-pill-border-width) + var(--check-box-in-pill-border-style) + var(--check-box-in-pill-checked-border-color); + --check-box-in-pill-checked-active-border: var( + --check-box-in-pill-border-width + ) + var(--check-box-in-pill-border-style) + var(--check-box-in-pill-checked-active-border-color); + --check-box-in-pill-checked-hover-border: var( + --check-box-in-pill-border-width + ) + var(--check-box-in-pill-border-style) + var(--check-box-in-pill-checked-hover-border-color); + --check-box-in-pill-focus-visible-border: var( + --check-box-in-pill-border-width + ) + var(--check-box-in-pill-border-style) + var(--check-box-in-pill-focus-visible-border-color); + --check-box-in-pill-mark-color: var(--primary-secondary-color); + --check-box-in-pill-focus-visible-mark-color: var(--primary-secondary-color); // ------ Check Box theme ------ // ------ Toggle Switch theme ------ @@ -1212,6 +1282,81 @@ --radio-button-pip-color: var(--primary-secondary-color); --radio-button-focus-visible-pip-color: var(--primary-color); --radio-button-text-color: var(--primary-secondary-color); + + --radio-button-pill-container-background-color: var(--grey-background1-color); + --radio-button-pill-container-active-background-color: var( + --accent-background2-color + ); + --radio-button-pill-container-hover-background-color: var( + --accent-background1-color + ); + --radio-button-pill-container-border-color: var(--accent-background2-color); + --radio-button-pill-container-active-border-color: var( + --accent-background2-color + ); + --radio-button-pill-container-hover-border-color: var( + --accent-background1-color + ); + --radio-button-pill-container-border-radius: var(--border-radius-xl); + --radio-button-pill-container-border-style: solid; + --radio-button-pill-container-border-width: 0; + --radio-button-pill-container-text-color: var(--text-secondary-color); + --radio-button-pill-container-active-text-color: var(--primary-color); + --radio-button-pill-container-hover-text-color: var(--primary-color); + --radio-button-in-pill-background-color: transparent; + --radio-button-in-pill-active-background-color: transparent; + --radio-button-in-pill-hover-background-color: transparent; + --radio-button-in-pill-checked-background-color: transparent; + --radio-button-in-pill-checked-active-background-color: transparent; + --radio-button-in-pill-checked-hover-background-color: transparent; + --radio-button-in-pill-border-color: var(--grey-secondary-color); + --radio-button-in-pill-active-border-color: var(--primary-secondary-color); + --radio-button-in-pill-hover-border-color: var(--primary-secondary-color); + --radio-button-in-pill-checked-border-color: var(--primary-secondary-color); + --radio-button-in-pill-checked-active-border-color: var( + --primary-secondary-color + ); + --radio-button-in-pill-checked-hover-border-color: var( + --primary-secondary-color + ); + --radio-button-in-pill-focus-visible-border-color: var( + --primary-secondary-color + ); + --radio-button-in-pill-border-width: 2px; + --radio-button-in-pill-border-style: solid; + --radio-button-in-pill-border: var(--radio-button-in-pill-border-width) + var(--radio-button-in-pill-border-style) + var(--radio-button-in-pill-border-color); + --radio-button-in-pill-active-border: var(--radio-button-in-pill-border-width) + var(--radio-button-in-pill-border-style) + var(--radio-button-in-pill-active-border-color); + --radio-button-in-pill-hover-border: var(--radio-button-in-pill-border-width) + var(--radio-button-in-pill-border-style) + var(--radio-button-in-pill-hover-border-color); + --radio-button-in-pill-checked-border: var( + --radio-button-in-pill-border-width + ) + var(--radio-button-in-pill-border-style) + var(--radio-button-in-pill-checked-border-color); + --radio-button-in-pill-checked-active-border: var( + --radio-button-in-pill-border-width + ) + var(--radio-button-in-pill-border-style) + var(--radio-button-in-pill-checked-active-border-color); + --radio-button-in-pill-checked-hover-border: var( + --radio-button-in-pill-border-width + ) + var(--radio-button-in-pill-border-style) + var(--radio-button-in-pill-checked-hover-border-color); + --radio-button-in-pill-focus-visible-border: var( + --radio-button-in-pill-border-width + ) + var(--radio-button-in-pill-border-style) + var(--radio-button-in-pill-focus-visible-border-color); + --radio-button-in-pill-pip-color: var(--primary-secondary-color); + --radio-button-in-pill-focus-visible-pip-color: var( + --primary-secondary-color + ); // ------ Radio Button theme ------ // ------ Persistent Bar theme ------ diff --git a/src/styles/themes/_definitions.scss b/src/styles/themes/_definitions.scss index 932edf7c9..2eed7d957 100644 --- a/src/styles/themes/_definitions.scss +++ b/src/styles/themes/_definitions.scss @@ -150,6 +150,15 @@ $button-padding-vertical-small: 6px; $button-padding-horizontal-small: 10px; $button-spacer-small: 4px; +$selector-padding-vertical-large: 10px; +$selector-padding-horizontal-large: 14px; + +$selector-padding-vertical-medium: 8px; +$selector-padding-horizontal-medium: 12px; + +$selector-padding-vertical-small: 6px; +$selector-padding-horizontal-small: 10px; + $selector-outline-offset: 1px; $label-no-value-margin-bottom: 15px;