Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Bug fixes**

- Fixed EuiDataGrid height issue when in full-screen mode and with scrolling content ([#5557](https://github.com/elastic/eui/pull/5557))
- Fixed an accessibility issue in custom and interactive Drag and Drop patterns ([#5568](https://github.com/elastic/eui/pull/5568))
- Fixed a focus bug in `EuiDataGrid` when clicking another cell header with an already-open cell header popover ([#5556](https://github.com/elastic/eui/pull/5556))

## [`46.1.0`](https://github.com/elastic/eui/tree/v46.1.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default () => {
spacing="l"
style={{ flex: '1 0 50%' }}
disableInteractiveElementBlocking // Allows button to be drag handle
hasInteractiveChildren={true}
>
{(provided) => (
<EuiPanel color="subdued" paddingSize="s">
Expand Down
14 changes: 10 additions & 4 deletions src-docs/src/views/drag_and_drop/drag_and_drop_custom_handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,20 @@ export default () => {
index={idx}
draggableId={id}
customDragHandle={true}
hasInteractiveChildren={true}
>
{(provided) => (
<EuiPanel className="custom" paddingSize="m">
<EuiFlexGroup>
<EuiPanel paddingSize="s">
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<div {...provided.dragHandleProps} aria-label="Drag Handle">
<EuiPanel
color="transparent"
paddingSize="s"
{...provided.dragHandleProps}
aria-label="Drag Handle"
>
<EuiIcon type="grab" />
</div>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>{content}</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
EuiDragDropContext,
EuiDraggable,
EuiDroppable,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
euiDragDropReorder,
} from '../../../../src/components';
import { htmlIdGenerator } from '../../../../src/services';
Expand All @@ -30,22 +34,40 @@ export default () => {
return (
<EuiDragDropContext onDragEnd={onDragEnd}>
<EuiDroppable
droppableId="DROPPABLE_AREA"
droppableId="CUSTOM_HANDLE_DROPPABLE_AREA"
spacing="m"
withPanel
grow={false}
>
{list.map(({ content, id }, idx) => (
<EuiDraggable
spacing="m"
key={id}
index={idx}
draggableId={id}
disableInteractiveElementBlocking
customDragHandle={true}
hasInteractiveChildren={true}
>
<EuiButton fullWidth onClick={() => {}}>
{content}
</EuiButton>
{(provided) => (
<EuiPanel paddingSize="s">
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiPanel
color="transparent"
paddingSize="s"
{...provided.dragHandleProps}
aria-label="Drag Handle"
>
<EuiIcon type="grab" />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiButton fullWidth onClick={() => {}}>
{content}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
)}
</EuiDraggable>
))}
</EuiDroppable>
Expand Down
17 changes: 10 additions & 7 deletions src-docs/src/views/drag_and_drop/drag_and_drop_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,9 @@ export const DragAndDropExample = {
<React.Fragment>
<p>
By default the entire element surface can initiate a drag. To
specify a certain element within as the handle, set
<EuiCode>customDragHandle=true</EuiCode> on the{' '}
specify an element within as the handle and create a containing
group, set <EuiCode>customDragHandle=true</EuiCode> and{' '}
<EuiCode>hasInteractiveChildren=true</EuiCode> on the{' '}
<strong>EuiDraggable</strong>.
</p>
<p>
Expand All @@ -226,11 +227,13 @@ export const DragAndDropExample = {
text: (
<React.Fragment>
<p>
<strong>EuiDraggable</strong> elements can contain interactive
elements such as buttons and form fields by adding the
<EuiCode>disableInteractiveElementBlocking</EuiCode> prop. This will
keep drag functionality while also enabling click, etc., events on
the interactive child elements.
<strong>EuiDraggable</strong> can contain interactive elements such
as buttons and form fields. Interactive elements require{' '}
<EuiCode>customDragHandle=true</EuiCode> and{' '}
<EuiCode>hasInteractiveChildren=true</EuiCode> on the{' '}
<strong>EuiDraggable</strong>. These props will maintain drag
functionality and accessibility, while enabling click, keypress,
etc., events on the interactive child elements.
</p>
</React.Fragment>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,40 @@ exports[`EuiDraggable can be given ReactElement children 1`] = `

exports[`EuiDraggable can be given ReactElement children 2`] = `undefined`;

exports[`EuiDraggable hasInteractiveChildren renders with role="group" and no tabIndex 1`] = `
<div>
<div
class="euiDroppable euiDroppable--noGrow"
data-rbd-droppable-context-id="0"
data-rbd-droppable-id="testDroppable"
data-test-subj="droppable"
>
<div
aria-describedby="rbd-hidden-text-0-hidden-text-0"
class="euiDraggable"
data-rbd-drag-handle-context-id="0"
data-rbd-drag-handle-draggable-id="testDraggable"
data-rbd-draggable-context-id="0"
data-rbd-draggable-id="testDraggable"
data-test-subj="draggable"
draggable="false"
role="group"
>
<div
class="euiDraggable__item"
>
Hello
</div>
</div>
<div
class="euiDroppable__placeholder"
/>
</div>
</div>
`;

exports[`EuiDraggable hasInteractiveChildren renders with role="group" and no tabIndex 2`] = `undefined`;

exports[`EuiDraggable is rendered 1`] = `
<div>
<div
Expand Down
20 changes: 20 additions & 0 deletions src/components/drag_and_drop/draggable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,24 @@ describe('EuiDraggable', () => {

expect(takeSnapshot(appDiv)).toMatchSnapshot();
});

test('hasInteractiveChildren renders with role="group" and no tabIndex', () => {
const handler = jest.fn();
ReactDOM.render(
<EuiDragDropContext onDragEnd={handler} {...requiredProps}>
<EuiDroppable droppableId="testDroppable">
<EuiDraggable
hasInteractiveChildren={true}
draggableId="testDraggable"
index={0}
>
<div>Hello</div>
</EuiDraggable>
</EuiDroppable>
</EuiDragDropContext>,
appDiv
);

expect(takeSnapshot(appDiv)).toMatchSnapshot();
});
});
21 changes: 21 additions & 0 deletions src/components/drag_and_drop/draggable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export interface EuiDraggableProps
* Whether the `children` will provide and set up its own drag handle
*/
customDragHandle?: boolean;
/**
* Whether the container has interactive children and should have `role="group"` instead of `"button"`.
* Setting this flag ensures your drag & drop container is keyboard and screen reader accessible.
*/
hasInteractiveChildren?: boolean;
/**
* Whether the item is currently in a position to be removed
*/
Expand All @@ -55,6 +60,7 @@ export const EuiDraggable: FunctionComponent<EuiDraggableProps> = ({
customDragHandle = false,
draggableId,
isDragDisabled = false,
hasInteractiveChildren = false,
isRemovable = false,
index,
children,
Expand Down Expand Up @@ -104,6 +110,21 @@ export const EuiDraggable: FunctionComponent<EuiDraggableProps> = ({
data-test-subj={dataTestSubj}
className={classes}
style={{ ...style, ...provided.draggableProps.style }}
// We use [role="group"] instead of [role="button"] when we expect a nested
// interactive element. Screen readers will cue users that this is a container
// and has one or more elements inside that are part of a related group.
role={
hasInteractiveChildren
? 'group'
: provided.dragHandleProps?.role
}
// If the container includes an interactive element, we remove the tabindex=0
// because [role="group"] does not permit or warrant a tab stop
tabIndex={
hasInteractiveChildren
? undefined
: provided.dragHandleProps?.tabIndex
}
>
{cloneElement(DraggableElement, {
className: classNames(
Expand Down