import React, { createRef, useState } from 'react' import classnames from 'classnames' import { FieldMessage } from '~components/FieldMessage' import { Icon } from '~components/Icon' import { Text } from '~components/Text' import { type ColorSchema, type Scale, type ScaleItem, type ScaleValue } from './types' import determineSelectionFromKeyPress from './utils/determineSelectionFromKeyPress' import styles from './LikertScale.module.scss' type ItemRefs = { value: ScaleValue ref: { current: null | HTMLDivElement } }[] export type LikertScaleProps = { 'labelId': string 'scale': Scale 'selectedItem': ScaleItem | null /** * @deprecated Please use data-testid instead */ 'automationId'?: string 'data-testid'?: string 'reversed'?: boolean 'colorSchema'?: ColorSchema | 'classical' 'validationMessage'?: string 'status'?: 'default' | 'error' /** * Sets aria-required value on radiogroup for assistive technologies. Validation must still be handled. */ 'isRequired'?: boolean 'onSelect': (value: ScaleItem | null) => void } const SelectedItemIcon = (): JSX.Element => ( ) /** * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3082060201/Likert+Scale Guidance} | * {@link https://cultureamp.design/?path=/docs/components-likertscale--docs Storybook} */ export const LikertScale = ({ scale, selectedItem, reversed, colorSchema = 'classical', 'data-testid': dataTestId, onSelect, validationMessage, status, labelId, isRequired, }: LikertScaleProps): JSX.Element => { const [hoveredItem, setHoveredItem] = useState(null) const itemRefs: ItemRefs = scale.map((s) => ({ value: s.value, ref: createRef(), })) const handleRadioClick = (item: ScaleItem): void => { // Is this a click on the item that is currently selected? const isClickOnSelectedItem = selectedItem?.value === item.value // Grab "Not rated" state item from the scale, its value is -1 const notYetRated = scale.find((s) => s.value === -1) ?? null // Clear or set new selection const newItem = isClickOnSelectedItem ? notYetRated : item onSelect(newItem) setHoveredItem(null) } /** * Because the radios have been built with divs, we need to add the keyboard functionality manually */ const handleKeyDown = ( event: React.KeyboardEvent, focusedItem: ScaleItem, ): void => { const newPosition = determineSelectionFromKeyPress(event.keyCode, selectedItem, focusedItem) if (newPosition) { event.preventDefault() onSelect(scale.find((s) => s.value === newPosition) ?? null) // Update focus const itemRef = itemRefs.find((item) => item.value === newPosition) itemRef?.ref?.current?.focus() } } const legend = hoveredItem?.label ?? selectedItem?.label ?? 'Not rated' const shouldDisplayValidationMessage = status !== 'default' && validationMessage !== undefined const validationMessageId = shouldDisplayValidationMessage ? `${labelId}-field-validation-message` : undefined const isRated = selectedItem && selectedItem.value > 0 return (
{legend}
{scale.map((item: ScaleItem) => { if (item.value <= 0) { return } const isSelectedItem = selectedItem?.value === item.value const itemRef = itemRefs.find((i) => item.value === i.value) // Make control tabbable let tabIndex = 0 // Unless.. there's an item selected and it's not this one if (selectedItem && selectedItem.value > 0 && selectedItem.value !== item.value) { tabIndex = -1 } const isSelected = selectedItem && item.value <= selectedItem?.value && !hoveredItem const isSuggested = hoveredItem && hoveredItem.value >= item.value const isUnselected = selectedItem && selectedItem.value < item.value return (
handleRadioClick(item)} onMouseEnter={(): void => setHoveredItem(item)} onMouseLeave={(): void => setHoveredItem(null)} onKeyDown={(event): void => handleKeyDown(event, item)} onFocus={(): void => setHoveredItem(item)} onBlur={(): void => setHoveredItem(null)} role="radio" aria-label={item.label} aria-checked={isSelectedItem} aria-posinset={item.value} aria-setsize={5} tabIndex={tabIndex} ref={itemRef?.ref} >
{isSelectedItem ? : null}
) })}
{shouldDisplayValidationMessage && ( )}
) }