diff --git a/src/__snapshots__/storybook.test.js.snap b/src/__snapshots__/storybook.test.js.snap index d15bf901b..54e5acbd2 100644 --- a/src/__snapshots__/storybook.test.js.snap +++ b/src/__snapshots__/storybook.test.js.snap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf0180ea24811204ec5253f5fd38916e9c78eb90599c2b8488a96c7bc1ac45a8 -size 240639 +oid sha256:67628020d300930c65c449e49022c8750bdf8e88f94e297eb80a0de01db28788 +size 243707 diff --git a/src/components/ConfigProvider/ConfigProvider.stories.tsx b/src/components/ConfigProvider/ConfigProvider.stories.tsx index 135877e04..543ce3048 100644 --- a/src/components/ConfigProvider/ConfigProvider.stories.tsx +++ b/src/components/ConfigProvider/ConfigProvider.stories.tsx @@ -14,6 +14,7 @@ import { ConfigProvider, OcThemeNames, useConfig } from './'; import { MatchScore } from '../MatchScore'; import { Spinner } from '../Spinner'; import { Stack } from '../Stack'; +import { CheckBoxGroup, CheckboxValueType, RadioGroup } from '../Selectors'; const theme: OcThemeNames[] = [ 'red', @@ -206,6 +207,44 @@ const ThemedComponents: FC = () => { + + ({ + value: `Radio${i}`, + label: `Radio${i}`, + name: 'group', + id: `oea2exk-${i}`, + })), + }} + /> ); }; diff --git a/src/components/Inputs/input.module.scss b/src/components/Inputs/input.module.scss index b7590b41c..d9716830b 100644 --- a/src/components/Inputs/input.module.scss +++ b/src/components/Inputs/input.module.scss @@ -286,7 +286,7 @@ .text-area { background-color: var(--background-color); - border: 1px solid --border-color; + border: 1px solid var(--border-color); border-radius: $corner-radius-s; box-sizing: border-box; color: var(--grey-color-60); diff --git a/src/components/Selectors/CheckBox/CheckBox.stories.tsx b/src/components/Selectors/CheckBox/CheckBox.stories.tsx index 2fb5ff002..ae806aed2 100644 --- a/src/components/Selectors/CheckBox/CheckBox.stories.tsx +++ b/src/components/Selectors/CheckBox/CheckBox.stories.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Stories } from '@storybook/addon-docs'; import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { CheckBox, CheckBoxGroup } from '../index'; +import { CheckBox, CheckBoxGroup } from '../'; +import { CheckboxValueType } from './Checkbox.types'; export default { title: 'Check Box', @@ -74,10 +75,6 @@ export default { defaultValue: true, control: { type: 'boolean' }, }, - defaultChecked: { - defaultValue: false, - control: { type: 'boolean' }, - }, }, } as ComponentMeta; @@ -87,9 +84,16 @@ const CheckBox_Story: ComponentStory = (args) => ( export const Check_Box = CheckBox_Story.bind({}); -const CheckBoxGroup_Story: ComponentStory = (args) => ( - -); +const CheckBoxGroup_Story: ComponentStory = (args) => { + const [selected, setSelected] = useState([]); + return ( + setSelected([...newSelected])} + /> + ); +}; export const Check_Box_Group = CheckBoxGroup_Story.bind({}); @@ -99,8 +103,10 @@ const checkBoxArgs: Object = { classNames: 'my-checkbox-class', disabled: false, name: 'myCheckBoxName', - value: 'Label', + value: 'label', + label: 'Label', id: 'myCheckBoxId', + defaultChecked: false, }; Check_Box.args = { @@ -108,24 +114,25 @@ Check_Box.args = { }; Check_Box_Group.args = { - ...checkBoxArgs, + value: ['First'], + defaultChecked: ['First'], items: [ { - checked: true, name: 'group', value: 'First', + label: 'First', id: 'test-1', }, { - checked: true, name: 'group', value: 'Second', + label: 'Second', id: 'test-2', }, { - checked: true, name: 'group', value: 'Third', + label: 'Third', id: 'test-3', }, ], diff --git a/src/components/Selectors/CheckBox/CheckBox.tsx b/src/components/Selectors/CheckBox/CheckBox.tsx index 4c3942dd5..846e19d57 100644 --- a/src/components/Selectors/CheckBox/CheckBox.tsx +++ b/src/components/Selectors/CheckBox/CheckBox.tsx @@ -1,58 +1,79 @@ -import React, { FC, useState } from 'react'; -import { CheckBoxProps } from '../'; -import { mergeClasses, generateId } from '../../../shared/utilities'; +import React, { FC, Ref, useEffect, useRef, useState } from 'react'; +import { generateId, mergeClasses } from '../../../shared/utilities'; +import { CheckboxProps } from './Checkbox.types'; import styles from './checkbox.module.scss'; -export const CheckBox: FC = ({ - ariaLabel, - checked = false, - defaultChecked, - disabled = false, - name, - value = '', - id, - onChange, -}) => { - const [checkBoxId] = useState(id || generateId()); - const [isChecked, setIsChecked] = useState(checked); - - const checkBoxCheckClassNames: string = mergeClasses([ - styles.checkmark, - { [styles.disabled]: disabled }, - ]); - - const toggleChecked = (): void => { - if (!disabled) setIsChecked(!isChecked); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key !== 'Tab') event.preventDefault(); - if (event.key === 'Enter' || event.key === ' ') toggleChecked(); - }; - - return ( -
- -
+ ); + } +); diff --git a/src/components/Selectors/CheckBox/CheckBoxGroup.tsx b/src/components/Selectors/CheckBox/CheckBoxGroup.tsx index 68f3d640e..8afda5a33 100644 --- a/src/components/Selectors/CheckBox/CheckBoxGroup.tsx +++ b/src/components/Selectors/CheckBox/CheckBoxGroup.tsx @@ -1,26 +1,47 @@ -import React, { FC } from 'react'; -import { CheckBoxProps } from '../'; +import React, { FC, Ref } from 'react'; import { CheckBox } from './CheckBox'; -import { generateId } from '../../../shared/utilities'; +import { mergeClasses } from '../../../shared/utilities'; +import { CheckboxGroupProps } from './Checkbox.types'; -export const CheckBoxGroup: FC = ({ - defaultChecked = true, - items, - onChange, -}) => { - return ( - <> - {items.map((item, index) => ( - - ))} - - ); -}; +import styles from './checkbox.module.scss'; + +export const CheckBoxGroup: FC = React.forwardRef( + ( + { items = [], onChange, value, classNames, style, ariaLabel, ...rest }, + ref: Ref + ) => { + const checkboxGroupClassNames = mergeClasses([ + styles.checkboxGroup, + classNames, + ]); + + return ( +
+ {items.map((item) => ( + { + const optionIndex = value?.indexOf(item.value); + const newValue = [...value]; + if (optionIndex === -1) { + newValue.push(item.value); + } else { + newValue.splice(optionIndex, 1); + } + onChange?.(newValue); + }} + /> + ))} +
+ ); + } +); diff --git a/src/components/Selectors/CheckBox/Checkbox.types.ts b/src/components/Selectors/CheckBox/Checkbox.types.ts new file mode 100644 index 000000000..b45556325 --- /dev/null +++ b/src/components/Selectors/CheckBox/Checkbox.types.ts @@ -0,0 +1,65 @@ +import React from 'react'; +import { OcBaseProps } from '../../OcBase'; + +export type CheckboxValueType = string | number; + +export interface CheckboxProps extends OcBaseProps { + /** + * Allows focus on the checkbox when it's disabled. + */ + allowDisabledFocus?: boolean; + /** + * The input checkbox aria-label text. + */ + ariaLabel?: string; + /** + * The input checkbox checked value. + */ + checked?: boolean; + /** + * The input checkbox default checked value. + */ + defaultChecked?: boolean; + /** + * The checkbox disabled state. + * @default false + */ + disabled?: boolean; + /** + * The checkbox input name. + */ + name?: string; + /** + * The checkbox value. + */ + value?: CheckboxValueType; + /** + * The label of the checkbox + */ + label?: string | React.ReactNode; + /** + * The checkbox onChange event handler. + */ + onChange?: React.ChangeEventHandler; +} + +export interface CheckboxGroupProps + extends Omit, 'defaultChecked' | 'onChange'> { + /** + * Aria label for the checkbox group + */ + ariaLabel?: string; + /** + * The array of items for the radio group. + */ + items?: CheckboxProps[]; + /** + * The checkbox value. + */ + value?: CheckboxValueType[]; + /** + * Callback fired when any item in the checkboxGroup changes + * @param checkedValue + */ + onChange?: (checkedValue: CheckboxValueType[]) => void; +} diff --git a/src/components/Selectors/CheckBox/checkbox.module.scss b/src/components/Selectors/CheckBox/checkbox.module.scss index ab0dbb8c4..1ab4f5e9e 100644 --- a/src/components/Selectors/CheckBox/checkbox.module.scss +++ b/src/components/Selectors/CheckBox/checkbox.module.scss @@ -1,141 +1,188 @@ .selector { - margin-bottom: $selector-margin-bottom; - flex-shrink: 1; - position: relative; display: flex; - text-overflow: elipses; - vertical-align: baseline; + position: relative; + width: fit-content; input { position: absolute; - background: none; opacity: 0; - height: $selector-input-width; - width: $selector-input-height; - top: $space-xxxs; - left: $space-xxxs; + height: 100%; + width: 100%; cursor: pointer; - } + z-index: 1; - input:checked + label { - .checkmark { - background-color: var(--primary-color); - border: $space-xxxs solid var(--primary-color); + &[disabled] { + cursor: default; - &.disabled { - opacity: 50%; - - &:hover { - background-color: var(--primary-color); - border: $space-xxxs solid var(--primary-color); + & + label { + .checkmark { + opacity: 50%; } + } - &:focus, - &:focus-visible { - outline: none; + &:hover + label { + .checkmark { + border: $space-xxxs solid var(--grey-color-70); } + } - &:active { - transform: none; - background-color: var(--primary-color); - border: $space-xxxs solid var(--primary-color); + &:active + label { + .checkmark { + border: $space-xxxs solid var(--grey-color-70); } } + } - &:hover { - background-color: var(--primary-color-60); - border: $space-xxxs solid var(--primary-color-60); + & + label { + .checkmark { + height: $checkmark-height; + width: $checkmark-width; + position: relative; + top: 0; + left: 0; + border-radius: $corner-radius-s; + border: $space-xxxs solid var(--grey-color-70); + transition: all $motion-duration-extra-fast $motion-ease-in-back + $motion-delay-s; + + &:after { + content: ''; + position: absolute; + left: $checkmark-after-left; + top: $checkmark-after-top; + width: $checkmark-after-width; + height: $checkmark-after-height; + border: solid white; + border-width: 0 $space-xxxs $space-xxxs 0; + opacity: 0; + transform: rotate(45deg) scale(0); + display: block; + transition: all $motion-duration-extra-fast + $motion-ease-in-back $motion-delay-s; + } } + } - &:focus, - &:focus-visible { - outline: $space-xxxs solid var(--primary-color-50); - outline-offset: $selector-outline-offset; + &:hover + label { + .checkmark { + border: $space-xxxs solid var(--primary-color-60); } + } - &:active { - transform: scale(0.98); - background-color: var(--primary-color-80); + &:active + label { + .checkmark { border: $space-xxxs solid var(--primary-color-80); } - - &:after { - left: $checkmark-after-left; - top: $checkmark-after-top; - width: $checkmark-after-width; - height: $icon-font-size-material-xs; - border: solid white; - border-width: 0 $space-xxxs $space-xxxs 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - display: block; - } } } - .checkmark { - height: $checkmark-height; - width: $checkmark-width; - position: absolute; - left: 0; - border-radius: $corner-radius-s; - cursor: pointer; - border: $space-xxxs solid var(--grey-color-70); - - &.disabled { - opacity: 50%; + input:checked { + &[disabled] { + cursor: default; - &:hover { - border: $space-xxxs solid var(--grey-color-70); - } - - &:focus, - &:focus-visible { - outline: none; - border: $space-xxxs solid var(--grey-color-70); + &:hover + label { + .checkmark { + background-color: var(--primary-color); + border: $space-xxxs solid var(--primary-color); + } } - &:active { - border: $space-xxxs solid var(--grey-color-70); + &:active + label { + .checkmark { + transform: none; + background-color: var(--primary-color); + border: $space-xxxs solid var(--primary-color); + } } } - &:hover { - border: $space-xxxs solid var(--primary-color-60); - } + & + label { + transition: all $motion-duration-extra-fast $motion-ease-out-back + $motion-delay-s; + + .checkmark { + background-color: var(--primary-color); + border: $space-xxxs solid var(--primary-color); + } - &:focus, - &:focus-visible { - border: $space-xxxs solid var(--primary-color); - outline: $space-xxxs solid var(--primary-color-50); - outline-offset: $selector-outline-offset; + .checkmark:after { + opacity: 1; + transform: rotate(45deg) scale(1); + transition: all $motion-duration-extra-fast + $motion-ease-out-back $motion-delay-s; + } } - &:active { - border: $space-xxxs solid var(--primary-color-80); + &:active + label { + .checkmark { + transform: scale(0.98); + background-color: var(--primary-color-80); + border: $space-xxxs solid var(--primary-color-80); + } } - &:after { - content: ''; - position: absolute; - display: none; + &:hover + label { + .checkmark { + background-color: var(--primary-color-60); + border: $space-xxxs solid var(--primary-color-60); + } } } label { display: flex; - align-items: flex-start; - cursor: pointer; - position: relative; - user-select: none; - vertical-align: baseline; + align-items: center; + margin-bottom: 0; + font-weight: $text-font-weight-regular; } .selector-label { - margin-left: $selector-label-margin-left; - font-size: medium; - margin-top: $space-xxxs; + margin-left: $space-xs; + font-size: $text-font-weight-regular; + } +} + +:global(.focus-visible) { + input[type='checkbox'] { + &:checked { + &:focus-visible + label { + .checkmark { + outline: $space-xxxs solid var(--primary-color-50); + outline-offset: $selector-outline-offset; + } + } + } + + &:focus-visible + label { + .checkmark { + border: $space-xxxs solid var(--primary-color); + outline: $space-xxxs solid var(--primary-color-50); + outline-offset: $selector-outline-offset; + } + } + + &[disabled] { + &:checked { + &:focus-visible + label { + .checkmark { + outline: none; + } + } + } + + &:focus-visible + label { + .checkmark { + outline: none; + border: $space-xxxs solid var(--grey-color-70); + } + } + } + } +} + +.checkbox-group { + .selector { + margin-bottom: $space-m; } } diff --git a/src/components/Selectors/RadioButton/Radio.types.ts b/src/components/Selectors/RadioButton/Radio.types.ts new file mode 100644 index 000000000..ae42ee4e1 --- /dev/null +++ b/src/components/Selectors/RadioButton/Radio.types.ts @@ -0,0 +1,65 @@ +import React from 'react'; +import { OcBaseProps } from '../../OcBase'; + +export type RadioButtonValue = string | number; + +export interface RadioGroupContextProps { + children: React.ReactNode; + onChange: React.ChangeEventHandler; + value?: RadioButtonValue; +} + +export interface IRadioButtonsContext { + value: RadioButtonValue; + onChange: React.ChangeEventHandler; +} + +export interface RadioButtonProps extends OcBaseProps { + /** + * The input aria label text. + */ + ariaLabel?: string; + /** + * The input icon button checked value. + */ + checked?: boolean; + /** + * The boolean for disabling the radio button. + */ + disabled?: boolean; + /** + * The name of the radio button group. + */ + name?: string; + /** + * The value of the input. + */ + value?: RadioButtonValue; + /** + * Label of the radio button + */ + label?: string | React.ReactNode; + /** + * The radio button onChange event handler. + */ + onChange?: React.ChangeEventHandler; +} + +export interface RadioGroupProps extends OcBaseProps { + /** + * The group aria label text. + */ + ariaLabel?: string; + /** + * The input radio default selected value. + */ + value?: RadioButtonValue; + /** + * The array of items for the radio group. + */ + items?: RadioButtonProps[]; + /** + * The radio button onChange event handler. + */ + onChange?: React.ChangeEventHandler; +} diff --git a/src/components/Selectors/RadioButton/RadioButton.stories.tsx b/src/components/Selectors/RadioButton/RadioButton.stories.tsx index 056c0453d..3fa465f4c 100644 --- a/src/components/Selectors/RadioButton/RadioButton.stories.tsx +++ b/src/components/Selectors/RadioButton/RadioButton.stories.tsx @@ -1,8 +1,12 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Stories } from '@storybook/addon-docs'; import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { RadioButton, RadioGroup } from '../index'; -import { RadioButtonChecked } from '../Selectors.types'; +import { + CheckboxValueType, + RadioButton, + RadioButtonValue, + RadioGroup, +} from '../index'; export default { title: 'Radio Button', @@ -109,16 +113,26 @@ const RadioButton_Story: ComponentStory = (args) => ( export const Radio_Button = RadioButton_Story.bind({}); -const RadioGroup_Story: ComponentStory = (args) => ( - -); +const RadioGroup_Story: ComponentStory = (args) => { + const [selected, setSelected] = useState(args.value); + return ( + { + setSelected(e.target.value); + }} + /> + ); +}; export const Radio_Group = RadioGroup_Story.bind({}); const radioButtonArgs: Object = { allowDisabledFocus: false, ariaLabel: 'Label', - checked: true, + label: 'Label', + checked: false, classNames: 'my-radiobutton-class', disabled: false, name: 'myRadioButtonName', @@ -131,10 +145,11 @@ Radio_Button.args = { }; Radio_Group.args = { - ...radioButtonArgs, - activeRadioButton: 'Radio1', - radioGroupItems: [1, 2, 3].map((i) => ({ + ariaLabel: 'Radio Group', + value: 'Radio1', + items: [1, 2, 3].map((i) => ({ value: `Radio${i}`, + label: `Radio${i}`, name: 'group', id: `oea2exk-${i}`, })), diff --git a/src/components/Selectors/RadioButton/RadioButton.tsx b/src/components/Selectors/RadioButton/RadioButton.tsx index 98aa28044..7eddd4dc9 100644 --- a/src/components/Selectors/RadioButton/RadioButton.tsx +++ b/src/components/Selectors/RadioButton/RadioButton.tsx @@ -1,64 +1,83 @@ -import React, { FC, useEffect, useState } from 'react'; -import { RadioButtonProps } from '../'; +import React, { FC, Ref, useEffect, useRef, useState } from 'react'; +import { RadioButtonProps } from './Radio.types'; import { mergeClasses, generateId } from '../../../shared/utilities'; import { useRadioGroup } from './RadioGroup.context'; import styles from './radio.module.scss'; -export const RadioButton: FC = ({ - ariaLabel, - activeRadioButton, - checked = false, - disabled = false, - name, - value = '', - id, - setActiveRadioButton = () => {}, -}) => { - const [radioButtonId] = useState(id || generateId()); - const { onRadioButtonClick, currentRadioButton } = useRadioGroup(); - const isActive: boolean = value === currentRadioButton; +export const RadioButton: FC = React.forwardRef( + ( + { + ariaLabel, + checked = false, + disabled = false, + name, + value = '', + id, + onChange, + label, + style, + 'data-test-id': dataTestId, + }, + ref: Ref + ) => { + const radioButtonId = useRef(id || generateId()); + const radioGroupContext = useRadioGroup(); + const [isActive, setIsActive] = useState( + radioGroupContext?.value === value || checked + ); - const updateActiveRadioButton = (value: string | number) => { - setActiveRadioButton(value); - }; - useEffect(() => { - if (onRadioButtonClick) onRadioButtonClick(activeRadioButton, null); - }, [activeRadioButton]); + const radioButtonClassNames: string = mergeClasses([ + styles.radioButton, + ]); - const radioButtonClassNames: string = mergeClasses([ - styles.radioButton, - { [styles.disabled]: disabled }, - ]); + const labelClassNames: string = mergeClasses([ + { [styles.labelNoValue]: value === '' }, + ]); - return ( -
- { - updateActiveRadioButton(value); - onRadioButtonClick?.(value, e); - }} - readOnly - /> -
+ ); + } +); diff --git a/src/components/Selectors/RadioButton/RadioGroup.context.tsx b/src/components/Selectors/RadioButton/RadioGroup.context.tsx index 928f32d23..cca071d7c 100644 --- a/src/components/Selectors/RadioButton/RadioGroup.context.tsx +++ b/src/components/Selectors/RadioButton/RadioGroup.context.tsx @@ -1,44 +1,24 @@ -import React, { createContext, useState } from 'react'; +import React, { createContext, useEffect, useState } from 'react'; import { - RadioGroupContextProps, IRadioButtonsContext, - RadioButtonChecked, - SelectRadioButtonEvent, -} from '../Selectors.types'; + RadioButtonValue, + RadioGroupContextProps, +} from './Radio.types'; -const RadioGroupContext = createContext>({}); +const RadioGroupContext = createContext>(null); const RadioGroupProvider = ({ children, onChange, - activeRadioButton, + value, }: RadioGroupContextProps) => { - const [currentRadioButton, setCurrentRadioButton] = - useState(activeRadioButton); - - const onRadioButtonClick = ( - value: RadioButtonChecked, - e: SelectRadioButtonEvent - ) => { - setCurrentRadioButton(value); - onChange?.(value, e); - }; - return ( - + {children} ); }; -const useRadioGroup = () => { - const context = React.useContext(RadioGroupContext); - if (context === undefined) { - throw new Error('RadioButton component must be used within RadioGroup'); - } - return context; -}; +const useRadioGroup = () => React.useContext(RadioGroupContext); export { RadioGroupProvider, useRadioGroup }; diff --git a/src/components/Selectors/RadioButton/RadioGroup.tsx b/src/components/Selectors/RadioButton/RadioGroup.tsx index c81cf8882..74b59ac0b 100644 --- a/src/components/Selectors/RadioButton/RadioGroup.tsx +++ b/src/components/Selectors/RadioButton/RadioGroup.tsx @@ -1,78 +1,36 @@ -import React, { FC, useEffect, useState } from 'react'; -import { RadioButton, RadioButtonProps } from '../'; +import React, { FC, Ref } from 'react'; +import { RadioButton } from '../'; import { RadioGroupProvider } from './RadioGroup.context'; -import { generateId } from '../../../shared/utilities'; +import { mergeClasses } from '../../../shared/utilities'; +import { RadioButtonProps, RadioGroupProps } from './Radio.types'; -export const RadioGroup: FC = (props) => { - let { activeRadioButton, onChange, radioGroupItems } = props; - const [radioGroupValues, setRadioGroupValues] = useState([]); - const [radioIndex, setRadioIndex] = useState(0); +import styles from './radio.module.scss'; - const handleKeyDown = (event: React.KeyboardEvent) => { - let indexOfRadio = radioIndex; - if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { - if (indexOfRadio + 2 > radioGroupValues.length) indexOfRadio = 0; - else indexOfRadio++; - activeRadioButton = radioGroupValues[indexOfRadio]; - } - if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { - if (indexOfRadio - 1 < 0) - indexOfRadio = radioGroupValues.length - 1; - else indexOfRadio--; - activeRadioButton = radioGroupValues[indexOfRadio]; - } - setRadioIndex(indexOfRadio); - const radioOnFocus = document.getElementById( - `${radioGroupItems[indexOfRadio].id}-custom-radio` - ); - radioOnFocus.focus(); - radioOnFocus.tabIndex = 0; - radioGroupItems.forEach((item: RadioButtonProps, idx: number) => { - if (idx !== indexOfRadio) { - const currentRadio = document.getElementById( - `${item.id}-custom-radio` - ); - currentRadio.tabIndex = -1; - } - }); - }; - - const getRadioGroupValues = () => { - let radioGroupValues: (string | number)[] = []; - radioGroupItems.map((item: RadioButtonProps) => { - radioGroupValues.push(item.value); - item.id = item.id || generateId(); - }); - return radioGroupValues; - }; - - const setActiveRadioButton = (value: string | number) => { - activeRadioButton = value; - const index = radioGroupValues.indexOf(activeRadioButton); - setRadioIndex(index >= 0 ? index : 0); - }; +export const RadioGroup: FC = React.forwardRef( + ( + { onChange, items, classNames, style, ariaLabel, value, ...rest }, + ref: Ref + ) => { + const radioGroupClasses: string = mergeClasses([ + styles.radioGroup, + classNames, + ]); - useEffect(() => { - const index = radioGroupValues.indexOf(activeRadioButton); - setRadioGroupValues(getRadioGroupValues()); - setRadioIndex(index >= 0 ? index : 0); - }, [activeRadioButton]); - - return ( - -
- {radioGroupItems.map((item: RadioButtonProps) => ( - - ))} -
-
- ); -}; + return ( + +
+ {items.map((item: RadioButtonProps) => ( + + ))} +
+
+ ); + } +); diff --git a/src/components/Selectors/RadioButton/radio.module.scss b/src/components/Selectors/RadioButton/radio.module.scss index e36e13a81..3978d6bdb 100644 --- a/src/components/Selectors/RadioButton/radio.module.scss +++ b/src/components/Selectors/RadioButton/radio.module.scss @@ -1,138 +1,186 @@ .selector { - margin-bottom: $selector-margin-bottom; - flex-shrink: 1; - position: relative; display: flex; - text-overflow: elipses; - vertical-align: baseline; + position: relative; + width: fit-content; input { position: absolute; - background: none; opacity: 0; - height: $selector-input-width; - width: $selector-input-height; - top: $space-xxxs; - left: $space-xxxs; + height: 100%; + width: 100%; cursor: pointer; - } + z-index: 1; - input:checked + label { - .radio-button { - background-color: var(--primary-color); - border: $space-xxxs solid var(--primary-color); + &[disabled] { + cursor: default; - &.disabled { - opacity: 50%; + & + label { + .radio-button { + opacity: 50%; + } + } - &:hover { - background-color: var(--primary-color); - border: $space-xxxs solid var(--primary-color); + &:hover + label { + .radio-button { + border: $space-xxxs solid var(--grey-color-70); } + } - &:focus, - &:focus-visible { - outline: none; + &:active + label { + .radio-button { + border: $space-xxxs solid var(--grey-color-70); } + } + } - &:active { - transform: none; + & + label { + .radio-button { + position: relative; + height: $radio-height; + width: $radio-width; + border-radius: 50%; + background-color: var(--white-color); + border: $space-xxxs solid var(--grey-color-70); + transition: all $motion-duration-extra-fast $motion-ease-in-back + $motion-delay-s; + + &:after { + top: $radio-after-top; + left: $radio-after-left; + width: $radio-after-width; + height: $radio-after-height; + border-radius: 50%; background-color: var(--primary-color); - border: $space-xxxs solid var(--primary-color); + content: ''; + position: absolute; + transform: scale(0); + opacity: 0; + display: block; + transition: all $motion-duration-extra-fast + $motion-ease-in-back $motion-delay-s; } } + } - &:hover { - background-color: var(--primary-color-60); + &:hover + label { + .radio-button { border: $space-xxxs solid var(--primary-color-60); } + } - &:focus, - &:focus-visible { - outline: $space-xxxs solid var(--primary-color-50); - } - - &:active { - transform: scale(0.98); - background-color: var(--primary-color-80); + &:active + label { + .radio-button { border: $space-xxxs solid var(--primary-color-80); } - - &:after { - display: block; - background: white; - } } } - .radio-button { - position: absolute; - left: 0; - height: $radio-height; - width: $radio-width; - border-radius: 50%; - background-color: white; - cursor: pointer; - border: $space-xxxs solid var(--grey-color-70); - - &.disabled { - opacity: 50%; + input:checked { + &[disabled] { + cursor: default; - &:hover { - border: $space-xxxs solid var(--grey-color-70); - } - - &:focus, - &:focus-visible { - outline: none; - border: $space-xxxs solid var(--grey-color-70); + &:hover + label { + .radio-button { + background-color: var(--primary-color); + border: $space-xxxs solid var(--primary-color); + } } - &:active { - border: $space-xxxs solid var(--grey-color-70); + &:active + label { + .radio-button { + transform: none; + background-color: var(--primary-color); + border: $space-xxxs solid var(--primary-color); + } } } - &:hover { - border: $space-xxxs solid var(--primary-color-60); - } + & + label { + transition: all $motion-duration-extra-fast $motion-ease-out-back + $motion-delay-s; - &:focus, - &:focus-visible { - outline: $space-xxxs solid var(--primary-color-50); - border: $space-xxxs solid var(--primary-color); + .radio-button { + background-color: var(--primary-color); + border: $space-xxxs solid var(--primary-color); + } + + .radio-button:after { + opacity: 1; + background: var(--white-color); + transform: scale(1); + transition: all $motion-duration-extra-fast + $motion-ease-out-back $motion-delay-s; + } } - &:active { - border: $space-xxxs solid var(--primary-color-80); + &:hover + label { + .radio-button { + background-color: var(--primary-color-60); + border: $space-xxxs solid var(--primary-color-60); + } } - &:after { - top: $radio-after-top; - left: $radio-after-left; - width: $radio-after-width; - height: $radio-after-height; - border-radius: 50%; - background-color: var(--primary-color); - content: ''; - position: absolute; - display: none; + &:active + label { + .radio-button { + transform: scale(0.98); + background-color: var(--primary-color-80); + border: $space-xxxs solid var(--primary-color-80); + } } } label { display: flex; - align-items: flex-start; - cursor: pointer; - position: relative; - user-select: none; - vertical-align: baseline; + align-items: center; + margin-bottom: 0; + font-weight: $text-font-weight-regular; } .selector-label { - margin-left: $selector-label-margin-left; - font-size: medium; - margin-top: $space-xxxs; + margin-left: $space-xs; + font-size: $text-font-weight-regular; + } +} + +:global(.focus-visible) { + input[type='radio'] { + &:checked { + &:focus-visible + label { + .radio-button { + outline: $space-xxxs solid var(--primary-color-50); + } + } + } + + &:focus-visible + label { + .radio-button { + outline: $space-xxxs solid var(--primary-color-50); + border: $space-xxxs solid var(--primary-color); + } + } + + &[disabled] { + &:checked { + &:focus-visible + label { + .radio-button { + outline: none; + } + } + } + + &:focus-visible + label { + .radio-button { + outline: none; + border: $space-xxxs solid var(--grey-color-70); + } + } + } + } +} + +.radio-group { + .selector { + margin-bottom: $space-m; } } diff --git a/src/components/Selectors/Selectors.types.ts b/src/components/Selectors/Selectors.types.ts deleted file mode 100644 index 06d04d99d..000000000 --- a/src/components/Selectors/Selectors.types.ts +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import { OcBaseProps } from '../OcBase'; - -export type SelectRadioButtonEvent = - | React.MouseEvent - | React.KeyboardEvent; - -export type RadioButtonChecked = string | number; - -export type OnChangeHandler = ( - value: RadioButtonChecked, - event: SelectRadioButtonEvent -) => void; - -export interface RadioGroupContextProps { - children: React.ReactNode; - onChange: OnChangeHandler; - activeRadioButton?: RadioButtonChecked; -} - -export interface IRadioButtonsContext { - currentRadioButton: RadioButtonChecked; - onRadioButtonClick: OnChangeHandler; -} - -export interface CheckBoxProps extends OcBaseProps { - /** - * Allows focus on the checkbox when it's disabled. - */ - allowDisabledFocus?: boolean; - /** - * The input checkbox aria-label text. - */ - ariaLabel?: string; - /** - * The input checkbox checked value. - */ - checked?: boolean; - /** - * The input checkbox default checked value. - */ - defaultChecked?: boolean; - /** - * The checkbox disabled state. - * @default false - */ - disabled?: boolean; - /** - * The checkbox id. - */ - id?: string; - /** - * The array of items for the radio group. - */ - items?: Array; - /** - * The checkbox input name. - */ - name?: string; - /** - * The check box value. - */ - value?: string; - /** - * The checkbox onChange event handler. - */ - onChange?: React.ChangeEventHandler; -} - -export interface RadioButtonProps - extends Omit, 'onChange'> { - /** - * The default radio button to select - */ - activeRadioButton?: RadioButtonChecked | string; - /** - * The input aria label text. - */ - ariaLabel?: string; - /** - * The input icon button checked value. - */ - checked?: boolean; - /** - * The input radio default selected value. - */ - defaultSelected?: boolean; - /** - * The boolean for disabling the radio button. - */ - disabled?: boolean; - /** - * The radio button id. - */ - id?: string; - /** - * The radio button index. - */ - index?: number; - /** - * The array of items for the radio group. - */ - items?: Array; - /** - * The boolean for the radio button being part of a group. - */ - forRadioGroup?: boolean; - /** - * The name of the radio button group. - */ - name?: string; - /** - * The value of the input. - */ - value?: string | number; - /** - * The radio button onChange event handler. - */ - onChange?: OnChangeHandler; - /** - * The radio buttons in the radio group. - */ - radioGroupItems?: any; - setActiveRadioButton?: Function; -} diff --git a/src/components/Selectors/index.ts b/src/components/Selectors/index.ts index a794fce43..63a01926c 100644 --- a/src/components/Selectors/index.ts +++ b/src/components/Selectors/index.ts @@ -1,5 +1,6 @@ -export * from './Selectors.types'; +export * from './CheckBox/Checkbox.types'; export * from './CheckBox/CheckBox'; export * from './CheckBox/CheckBoxGroup'; +export * from './RadioButton/Radio.types'; export * from './RadioButton/RadioButton'; export * from './RadioButton/RadioGroup'; diff --git a/src/shared/utilities.ts b/src/shared/utilities.ts index 179e9aee7..6adf97a7f 100644 --- a/src/shared/utilities.ts +++ b/src/shared/utilities.ts @@ -110,7 +110,6 @@ export const stopPropagation = (e: React.MouseEvent) => /** * Get unique id */ -export const generateId = ((): ((prefix?: string) => string) => { - return (prefix?: string): string => - `${prefix}${Math.random().toString(36).substring(2, 9)}`; -})(); +export const generateId = (prefix?: string) => { + return `${prefix ?? ''}${Math.random().toString(36).substring(2, 9)}`; +}; diff --git a/src/styles/abstracts/_mixins.scss b/src/styles/abstracts/_mixins.scss index 1c47c4049..d14912dbb 100644 --- a/src/styles/abstracts/_mixins.scss +++ b/src/styles/abstracts/_mixins.scss @@ -21,3 +21,9 @@ height: 100%; opacity: 0; } + +@mixin text-overflow { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/styles/themes/_definitions-light.scss b/src/styles/themes/_definitions-light.scss index 34b6e1890..946766d92 100644 --- a/src/styles/themes/_definitions-light.scss +++ b/src/styles/themes/_definitions-light.scss @@ -283,10 +283,8 @@ $button-spacer-small: 4px; $button-padding-vertical-default-small: 6px; $button-padding-horizontal-default-small: 2px; -$selector-margin-bottom: 15px; $selector-outline-offset: 1px; $label-no-value-margin-bottom: 15px; -$selector-label-margin-left: 28px; // Corner-radius Definitions @@ -329,6 +327,8 @@ $motion-easing-easeinout: cubic-bezier( 0.58, 1 ); // The primary easing curve. Used for any core animation not entering/exiting the screen or a component +$motion-ease-in-back: cubic-bezier(0.12, 0.4, 0.29, 1.46); +$motion-ease-out-back: cubic-bezier(0.71, -0.46, 0.88, 0.6); // Movement Definitions @@ -367,20 +367,21 @@ $all-backdrops: rgba(0, 0, 0, 0.5); $selector-input-width: 18px; $selector-input-height: 18px; -$checkmark-after-width: 7px; -$checkmark-height: 22px; -$checkmark-width: 22px; -$radio-height: 22px; -$radio-width: 22px; +$checkmark-after-width: 6px; +$checkmark-after-height: 12px; +$checkmark-height: 18px; +$checkmark-width: 18px; +$radio-height: 20px; +$radio-width: 20px; $radio-after-width: 6px; $radio-after-height: 6px; // Positioning -$checkmark-after-left: 5px; +$checkmark-after-left: 4px; $checkmark-after-top: 0px; -$radio-after-top: 6px; -$radio-after-left: 6px; +$radio-after-top: 5px; +$radio-after-left: 5px; // END: CONSTANTS