Skip to content

Commit 4770339

Browse files
authored
[EuiMarkdownEditor][A11y] Fix invalid nested interactive elements (elastic#9625)
1 parent 939d45e commit 4770339

5 files changed

Lines changed: 92 additions & 7 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**Accessibility**
2+
3+
- Fixed invalid nested interactive elements in `EuiMarkdownEditor` by removing `role` from the drop zone wrapper.

packages/eui/src/components/markdown_editor/__snapshots__/markdown_editor.test.tsx.snap

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ exports[`EuiMarkdownEditor is rendered 1`] = `
210210
>
211211
<div
212212
class="euiMarkdownEditorDropZone emotion-euiMarkdownEditorDropZone"
213-
role="button"
214213
>
215214
<textarea
216215
aria-label="aria-label"
@@ -468,7 +467,6 @@ exports[`EuiMarkdownEditor props autoExpandPreview is rendered with false 1`] =
468467
>
469468
<div
470469
class="euiMarkdownEditorDropZone emotion-euiMarkdownEditorDropZone"
471-
role="button"
472470
>
473471
<textarea
474472
aria-label="aria-label"
@@ -725,7 +723,6 @@ exports[`EuiMarkdownEditor props height is rendered in full mode 1`] = `
725723
>
726724
<div
727725
class="euiMarkdownEditorDropZone emotion-euiMarkdownEditorDropZone"
728-
role="button"
729726
>
730727
<textarea
731728
aria-label="aria-label"
@@ -982,7 +979,6 @@ exports[`EuiMarkdownEditor props height is rendered with a custom size 1`] = `
982979
>
983980
<div
984981
class="euiMarkdownEditorDropZone emotion-euiMarkdownEditorDropZone"
985-
role="button"
986982
>
987983
<textarea
988984
aria-label="aria-label"
@@ -1239,7 +1235,6 @@ exports[`EuiMarkdownEditor props maxHeight is rendered with a custom size 1`] =
12391235
>
12401236
<div
12411237
class="euiMarkdownEditorDropZone emotion-euiMarkdownEditorDropZone"
1242-
role="button"
12431238
>
12441239
<textarea
12451240
aria-label="aria-label"

packages/eui/src/components/markdown_editor/markdown_editor.stories.tsx

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Side Public License, v 1.
77
*/
88

9-
import React from 'react';
9+
import React, { useEffect, useState } from 'react';
1010
import type { Meta, StoryObj } from '@storybook/react';
1111
import { action } from '@storybook/addon-actions';
1212

@@ -89,3 +89,58 @@ export const CustomToolbarContent: Story = {
8989
},
9090
},
9191
};
92+
93+
export const DropZone: Story = {
94+
parameters: {
95+
controls: { include: ['dropHandlers', 'value'] },
96+
loki: {
97+
// functional test story only
98+
skip: true,
99+
},
100+
},
101+
args: {
102+
value: initialContent,
103+
dropHandlers: [
104+
{
105+
supportedFiles: ['image/png', 'image/jpeg'],
106+
accepts: (type: string) => type.startsWith('image/'),
107+
getFormattingForItem: (file: File) =>
108+
new Promise((resolve) => {
109+
const reader = new FileReader();
110+
reader.onload = () =>
111+
resolve({
112+
text: `![${file.name}](${reader.result})`,
113+
config: { block: true },
114+
});
115+
reader.readAsDataURL(file);
116+
}),
117+
},
118+
],
119+
},
120+
render: (args: EuiMarkdownEditorProps) => (
121+
<StatefulMarkdownEditor {...args} />
122+
),
123+
};
124+
125+
const StatefulMarkdownEditor = ({
126+
value: _value,
127+
onChange,
128+
...rest
129+
}: EuiMarkdownEditorProps) => {
130+
const [value, setValue] = useState(_value);
131+
132+
useEffect(() => {
133+
setValue(_value);
134+
}, [_value]);
135+
136+
return (
137+
<EuiMarkdownEditor
138+
{...rest}
139+
value={value}
140+
onChange={(v) => {
141+
setValue(v);
142+
onChange?.(v);
143+
}}
144+
/>
145+
);
146+
};

packages/eui/src/components/markdown_editor/markdown_editor.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,34 @@ describe('EuiMarkdownEditor', () => {
121121
expect(container.firstChild).toMatchSnapshot();
122122
});
123123
});
124+
125+
describe('dropHandlers', () => {
126+
// A11y: ensure interactive elements aren't nested
127+
it('does not have role="button" when dropHandlers are provided', () => {
128+
const { container } = render(
129+
<EuiMarkdownEditor
130+
editorId="editorId"
131+
value=""
132+
onChange={() => null}
133+
dropHandlers={[
134+
{
135+
supportedFiles: ['image/png'],
136+
accepts: (type: string) => type === 'image/png',
137+
getFormattingForItem: (file: File) =>
138+
Promise.resolve({
139+
text: `![${file.name}]()`,
140+
config: { block: true },
141+
}),
142+
},
143+
]}
144+
{...requiredProps}
145+
/>
146+
);
147+
148+
const dropZone = container.querySelector('.euiMarkdownEditorDropZone');
149+
expect(dropZone).not.toHaveAttribute('role', 'button');
150+
});
151+
});
124152
});
125153

126154
test('is preview rendered', () => {

packages/eui/src/components/markdown_editor/markdown_editor_drop_zone.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,11 @@ export const EuiMarkdownEditorDropZone: FunctionComponent<
214214
});
215215

216216
const rootProps = { ...getRootProps() };
217-
if (readOnly) rootProps.role = undefined; // Unset the default `role="button"` attribute which sets a misleading pointer icon
217+
/* Unset the default `role="button"` attribute to prevent invalid nested interactive elements.
218+
The dropzone has `noClick=true` and `noKeyboard=true` set via `useDropzone`.
219+
Keyboard drop interactions expect the drop zone element to have `role="button` but we're supporting
220+
adding files via a standalone footer button instead." */
221+
rootProps.role = undefined;
218222

219223
return (
220224
<div {...rootProps} css={cssStyles} className={classes}>

0 commit comments

Comments
 (0)