Skip to content

Commit

Permalink
fix: Attribute editor form controls need to be programmatically group…
Browse files Browse the repository at this point in the history
…ed (#494)
  • Loading branch information
YueyingLu authored Nov 18, 2022
1 parent 52ac61a commit 3e13d44
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 36 deletions.
32 changes: 32 additions & 0 deletions src/attribute-editor/__tests__/attribute-editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import createWrapper, { AttributeEditorWrapper } from '../../../lib/components/test-utils/dom';
import AttributeEditor, { AttributeEditorProps } from '../../../lib/components/attribute-editor';
import Input from '../../../lib/components/input';

interface Item {
key: string;
Expand Down Expand Up @@ -322,4 +323,35 @@ describe('Attribute Editor', () => {
}
});
});

describe('a11y', () => {
test('row has role group and aria-labelledby referring to first control label and content', () => {
const wrapper = renderAttributeEditor({
...defaultProps,
definition: [
{
label: 'Key label',
info: 'Key info',
control: item => <Input value={item.key} />,
},
{
label: 'Value label',
info: 'Value info',
control: item => <Input value={item.value} />,
},
],
});
const [labelId, inputId] = wrapper
.findRow(1)!
.find('[role="group"]')!
.getElement()
.getAttribute('aria-labelledby')!
.split(' ');
const label =
wrapper.getElement().querySelector(`#${labelId}`)!.textContent +
' ' +
wrapper.getElement().querySelector(`#${inputId}`)!.getAttribute('value');
expect(label).toBe('Key label k1');
});
});
});
82 changes: 46 additions & 36 deletions src/attribute-editor/row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/e
import InternalGrid from '../grid/internal';
import { InternalButton } from '../button/internal';
import clsx from 'clsx';
import { generateUniqueId } from '../internal/hooks/use-unique-id';

const Divider = () => <InternalBox className={styles.divider} padding={{ top: 'l' }} />;

Expand Down Expand Up @@ -55,44 +56,53 @@ export const Row = React.memo(
fireNonCancelableEvent(onRemoveButtonClick, { itemIndex: index });
}, [onRemoveButtonClick, index]);

const firstControlId = generateUniqueId('first-control-id-');

return (
<InternalBox className={styles.row} margin={{ bottom: 's' }}>
<InternalGrid
__breakpoint={breakpoint}
gridDefinition={removable ? REMOVABLE_GRID_DEFINITION : GRID_DEFINITION}
>
<InternalColumnLayout className={styles['row-control']} columns={definition.length} __breakpoint={breakpoint}>
{definition.map(({ info, label, constraintText, errorText, control }, defIndex) => (
<InternalFormField
key={defIndex}
className={styles.field}
label={label}
info={info}
constraintText={render(item, index, constraintText)}
errorText={render(item, index, errorText)}
stretch={true}
i18nStrings={{ errorIconAriaLabel: i18nStrings.errorIconAriaLabel }}
__hideLabel={isWideViewport && index > 0}
>
{render(item, index, control)}
</InternalFormField>
))}
</InternalColumnLayout>
{removable && (
<ButtonContainer index={index} isNarrowViewport={isNarrowViewport}>
<InternalButton
className={styles['remove-button']}
formAction="none"
ref={ref => {
removeButtonRefs[index] = ref ?? undefined;
}}
onClick={handleRemoveClick}
>
{removeButtonText}
</InternalButton>
</ButtonContainer>
)}
</InternalGrid>
<div role="group" aria-labelledby={`${firstControlId}-label ${firstControlId}`}>
<InternalGrid
__breakpoint={breakpoint}
gridDefinition={removable ? REMOVABLE_GRID_DEFINITION : GRID_DEFINITION}
>
<InternalColumnLayout
className={styles['row-control']}
columns={definition.length}
__breakpoint={breakpoint}
>
{definition.map(({ info, label, constraintText, errorText, control }, defIndex) => (
<InternalFormField
key={defIndex}
className={styles.field}
label={label}
info={info}
constraintText={render(item, index, constraintText)}
errorText={render(item, index, errorText)}
stretch={true}
i18nStrings={{ errorIconAriaLabel: i18nStrings.errorIconAriaLabel }}
__hideLabel={isWideViewport && index > 0}
controlId={defIndex === 0 ? firstControlId : undefined}
>
{render(item, index, control)}
</InternalFormField>
))}
</InternalColumnLayout>
{removable && (
<ButtonContainer index={index} isNarrowViewport={isNarrowViewport}>
<InternalButton
className={styles['remove-button']}
formAction="none"
ref={ref => {
removeButtonRefs[index] = ref ?? undefined;
}}
onClick={handleRemoveClick}
>
{removeButtonText}
</InternalButton>
</ButtonContainer>
)}
</InternalGrid>
</div>
{isNarrowViewport && <Divider />}
</InternalBox>
);
Expand Down

0 comments on commit 3e13d44

Please sign in to comment.