Skip to content

Commit

Permalink
feat(Select): pass aria attributes to render functions (#1841)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Sep 11, 2024
1 parent a38b73b commit 9801d33
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 57 deletions.
7 changes: 3 additions & 4 deletions src/components/Select/__stories__/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ import React from 'react';
import type {Meta, StoryObj} from '@storybook/react';

import {Select} from '..';
import type {SelectProps} from '..';
import {Button} from '../../Button';

import {SelectPopupWidthShowcase} from './SelectPopupWidthShowcase';
import {SelectShowcase} from './SelectShowcase';
import {UseSelectOptionsShowcase} from './UseSelectOptionsShowcase';

const meta: Meta = {
const meta: Meta<typeof Select> = {
title: 'Components/Inputs/Select',
component: Select,
parameters: {
Expand All @@ -31,7 +30,7 @@ const meta: Meta = {

export default meta;

type Story = StoryObj<SelectProps>;
type Story = StoryObj<typeof Select>;

export const Default = {
render: (args) => (
Expand All @@ -45,7 +44,7 @@ export const Default = {
} satisfies Story;

export const Showcase = {
render: (args: SelectProps) => <SelectShowcase {...args} />,
render: (args) => <SelectShowcase {...args} />,
args: {
view: 'normal',
size: 'm',
Expand Down
34 changes: 24 additions & 10 deletions src/components/Select/__stories__/SelectShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,18 @@ export const SelectShowcase = (props: SelectProps) => {
const [matchCase, setMatchCase] = React.useState(false);
const [matchWholeWord, setMatchWholeWord] = React.useState(false);

const renderFilter: SelectProps['renderFilter'] = ({value, ref, onChange, onKeyDown}) => {
const renderFilter: SelectProps['renderFilter'] = ({
ref,
style,
inputProps: {value, onChange, onKeyDown, ...controlProps},
}) => {
return (
<div style={{display: 'flex', flexDirection: 'column', rowGap: 4}}>
<div style={{...style, display: 'flex', flexDirection: 'column', rowGap: 4}}>
<TextInput
controlRef={ref}
controlProps={{size: 1}}
controlProps={controlProps}
value={value}
onUpdate={onChange}
onChange={onChange}
onKeyDown={onKeyDown}
/>
<div style={{display: 'flex', columnGap: 2}}>
Expand Down Expand Up @@ -281,20 +285,26 @@ export const SelectShowcase = (props: SelectProps) => {
selectProps={{
...props,
className: b('user-control'),
renderControl: ({onClick, onKeyDown, ref, renderClear, disabled}) => {
renderControl: ({
ref,
renderClear,
triggerProps: {onClick, disabled, id, ...extraProps},
}) => {
return (
<Button
id={id}
ref={ref}
view="action"
onClick={onClick}
disabled={disabled}
extraProps={{
onKeyDown,
...extraProps,
'aria-label': extraProps['aria-label'] || 'User control',
}}
className={b({'has-clear': props.hasClear})}
>
<span className={b('text')}>User control</span>
{renderClear?.({
{renderClear({
renderIcon: () => (
<Icon data={TrashBin} className={b('user-clear-icon')} />
),
Expand All @@ -317,16 +327,20 @@ export const SelectShowcase = (props: SelectProps) => {
...props,
className: b('user-control-placement'),
popupPlacement: ['bottom'],
renderControl: ({onClick, onKeyDown, ref, disabled}) => {
renderControl: ({
ref,
triggerProps: {onClick, disabled, id, ...extraProps},
}) => {
return (
<Button
id={id}
ref={ref}
view="action"
onClick={onClick}
disabled={disabled}
extraProps={{
onKeyDown,
'aria-label': 'Add',
...extraProps,
'aria-label': extraProps['aria-label'] || 'Add',
}}
>
<Icon data={Plus} />
Expand Down
47 changes: 28 additions & 19 deletions src/components/Select/components/SelectControl/SelectControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import type {
SelectProps,
SelectRenderClearArgs,
SelectRenderControl,
SelectRenderControlProps,
SelectRenderCounter,
SelectRenderTriggerProps,
} from '../../types';
import {SelectClear} from '../SelectClear/SelectClear';
import {SelectCounter} from '../SelectCounter/SelectCounter';
Expand All @@ -38,12 +38,17 @@ type ControlProps = {
isErrorVisible?: boolean;
errorMessage?: SelectProps['errorMessage'];
disabled?: boolean;
value: SelectProps['value'];
value: NonNullable<SelectProps['value']>;
clearValue: () => void;
hasClear?: boolean;
hasCounter?: boolean;
title?: string;
} & Omit<SelectRenderControlProps, 'onClick' | 'onClear' | 'renderCounter'>;
onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void;
open: boolean;
popupId: string;
selectId: string;
activeIndex?: number;
};

export const SelectControl = React.forwardRef<HTMLButtonElement, ControlProps>((props, ref) => {
const {
Expand Down Expand Up @@ -128,16 +133,16 @@ export const SelectControl = React.forwardRef<HTMLButtonElement, ControlProps>((
if (!hasCounter) {
return null;
}
const count = Number(value?.length) || 0;
const count = value.length;
const counterComponent = <SelectCounter count={count} size={size} disabled={disabled} />;
return renderCounter
? renderCounter(counterComponent, {count, size, disabled})
: counterComponent;
};

const renderClearIcon = (args: SelectRenderClearArgs) => {
const hideOnEmpty = !value?.[0];
if (!hasClear || !clearValue || hideOnEmpty || disabled) {
const valueIsEmpty = value.length === 0;
if (!hasClear || valueIsEmpty || disabled) {
return null;
}
return (
Expand All @@ -151,20 +156,34 @@ export const SelectControl = React.forwardRef<HTMLButtonElement, ControlProps>((
);
};

const triggerProps: SelectRenderTriggerProps = {
id: selectId,
role: 'combobox',
'aria-controls': open ? popupId : undefined,
'aria-haspopup': 'listbox',
'aria-expanded': open,
'aria-activedescendant':
activeIndex === undefined ? undefined : `${popupId}-item-${activeIndex}`,
onClick: handleControlClick,
onKeyDown,
disabled,
};

if (renderControl) {
return renderControl(
{
onKeyDown,
onClear: clearValue,
onClick: handleControlClick,
renderClear: (arg) => renderClearIcon(arg),
renderClear: renderClearIcon,
renderCounter: renderCounterComponent,
ref,
open: Boolean(open),
open,
popupId,
selectId,
activeIndex,
disabled,
triggerProps,
},
{value},
);
Expand All @@ -174,23 +193,13 @@ export const SelectControl = React.forwardRef<HTMLButtonElement, ControlProps>((
<React.Fragment>
<div className={selectControlBlock(controlMods)} role="group">
<button
id={selectId}
ref={ref}
role="combobox"
aria-controls={open ? popupId : undefined}
className={selectControlButtonBlock(buttonMods, className)}
aria-haspopup="listbox"
aria-expanded={open}
aria-activedescendant={
activeIndex === undefined ? undefined : `${popupId}-item-${activeIndex}`
}
disabled={disabled}
onClick={handleControlClick}
onKeyDown={onKeyDown}
type="button"
data-qa={qa}
title={title}
tabIndex={0}
{...triggerProps}
>
{label && <span className={selectControlBlock('label')}>{label}</span>}
{showPlaceholder && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,5 @@ $block: '.#{variables.$ns}select-filter';

#{$block} {
// To prevent border blink when the focus is set frequently (in case of option click)
& &__input,
& &__input:hover,
& &__input:focus {
border-color: var(--g-color-line-generic-active);
}
--g-text-input-border-color: var(--g-color-line-generic-active);
}
31 changes: 24 additions & 7 deletions src/components/Select/components/SelectFilter/SelectFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import React from 'react';
import {TextInput} from '../../../controls';
import {block} from '../../../utils/cn';
import {SelectQa} from '../../constants';
import type {SelectProps} from '../../types';
import i18n from '../../i18n';
import type {SelectFilterInputProps, SelectProps} from '../../types';
import type {SelectFilterRef} from '../../types-misc';

import './SelectFilter.scss';
Expand Down Expand Up @@ -40,18 +41,34 @@ export const SelectFilter = React.forwardRef<SelectFilterRef, SelectFilterProps>
[],
);

return renderFilter ? (
renderFilter({onChange, onKeyDown, value, ref: inputRef, style})
) : (
const inputProps: SelectFilterInputProps = {
value,
placeholder,
size: 1,
onKeyDown,
onChange: (e) => {
onChange(e.target.value);
},
'aria-label': i18n('label_filter'),
'aria-controls': popupId,
'aria-activedescendant':
activeIndex === undefined ? undefined : `${popupId}-item-${activeIndex}`,
};

if (renderFilter) {
return renderFilter({onChange, onKeyDown, value, ref: inputRef, style, inputProps});
}

return (
<div className={b()} style={style}>
<TextInput
controlRef={inputRef}
controlProps={{
className: b('input'),
size: 1,
'aria-controls': popupId,
'aria-activedescendant':
activeIndex === undefined ? undefined : `${popupId}-item-${activeIndex}`,
'aria-label': inputProps['aria-label'],
'aria-controls': inputProps['aria-controls'],
'aria-activedescendant': inputProps['aria-activedescendant'],
}}
size={size}
value={value}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Select/i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"label_clear": "Clear",
"label_show-error-info": "Show popup with error info"
"label_show-error-info": "Show popup with error info",
"label_filter": "Filter"
}
3 changes: 2 additions & 1 deletion src/components/Select/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"label_clear": "Очистить",
"label_show-error-info": "Показать попап с информацей об ошибке"
"label_show-error-info": "Показать попап с информацей об ошибке",
"label_filter": "Фильтр"
}
58 changes: 48 additions & 10 deletions src/components/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,49 @@ import type React from 'react';
import type {PopperPlacement} from '../../hooks/private';
import type {UseOpenProps} from '../../hooks/useSelect/types';
import type {InputControlPin, InputControlSize, InputControlView} from '../controls';
import type {ControlGroupOption, QAProps} from '../types';
import type {AriaLabelingProps, ControlGroupOption, QAProps} from '../types';

import type {Option, OptionGroup} from './tech-components';

export type SelectRenderClearArgs = {
renderIcon?: () => React.ReactNode;
};

export type SelectRenderTriggerProps = AriaLabelingProps &
Pick<
React.ButtonHTMLAttributes<HTMLElement>,
| 'id'
| 'type'
| 'role'
| 'aria-controls'
| 'aria-haspopup'
| 'aria-expanded'
| 'aria-activedescendant'
| 'onClick'
| 'onKeyDown'
| 'disabled'
>;

export type SelectRenderControlProps = {
onClear: () => void;
/** @deprecated use triggerProps instead */
onClick: (e: React.MouseEvent<HTMLElement>) => void;
/** @deprecated use triggerProps instead */
onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void;
renderClear?: (args: SelectRenderClearArgs) => React.ReactNode;
renderCounter?: () => React.ReactNode;
renderClear: (args: SelectRenderClearArgs) => React.ReactNode;
renderCounter: () => React.ReactNode;
ref: React.Ref<HTMLElement>;
open: boolean;
/** @deprecated use triggerProps instead */
popupId: string;
/** @deprecated use triggerProps instead */
selectId: string;
/** @deprecated use triggerProps instead */
activeIndex?: number;
disabled?: boolean;
triggerProps: SelectRenderTriggerProps;
};

export type SelectRenderControlOptions = {
value: SelectProps['value'];
};
Expand Down Expand Up @@ -51,6 +73,28 @@ export type SelectRenderPopup = (popupItems: {
renderList: () => React.JSX.Element;
}) => React.ReactElement;

export type SelectFilterInputProps = {value: string} & Pick<
React.InputHTMLAttributes<HTMLInputElement>,
| 'placeholder'
| 'onKeyDown'
| 'onChange'
| 'size'
| 'aria-label'
| 'aria-controls'
| 'aria-activedescendant'
>;
export type SelectRenderFilter = (props: {
/** @deprecated use inputProps instead */
onChange: (filter: string) => void;
/** @deprecated use inputProps instead */
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
/** @deprecated use inputProps instead */
value: string;
ref: React.Ref<HTMLInputElement>;
style: React.CSSProperties;
inputProps: SelectFilterInputProps;
}) => React.ReactElement;

export type SelectSize = InputControlSize;

export type SelectRenderCounter = (
Expand All @@ -62,13 +106,7 @@ export type SelectProps<T = any> = QAProps &
UseOpenProps & {
onUpdate?: (value: string[]) => void;
renderControl?: SelectRenderControl;
renderFilter?: (props: {
onChange: (filter: string) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
value: string;
ref: React.Ref<HTMLInputElement>;
style: React.CSSProperties;
}) => React.ReactElement;
renderFilter?: SelectRenderFilter;
renderOption?: SelectRenderOption<T>;
renderOptionGroup?: SelectRenderOptionGroup<T>;
renderSelectedOption?: (option: SelectOption<T>, index: number) => React.ReactElement;
Expand Down

0 comments on commit 9801d33

Please sign in to comment.