diff --git a/.buildkite/pipelines/pipeline_pull_request_test.yml b/.buildkite/pipelines/pipeline_pull_request_test.yml
index 74e3c9587f4..58b07441730 100644
--- a/.buildkite/pipelines/pipeline_pull_request_test.yml
+++ b/.buildkite/pipelines/pipeline_pull_request_test.yml
@@ -73,3 +73,14 @@ steps:
artifact_paths:
- "cypress/screenshots/**/*.png"
- "cypress/videos/**/*.mp4"
+
+ - command: .buildkite/scripts/pipeline_test.sh
+ label: ":axe: Cypress accessibility (a11y) tests on React 18"
+ agents:
+ provider: "gcp"
+ env:
+ TEST_TYPE: 'cypress:a11y'
+ if: build.branch != "main"
+ artifact_paths:
+ - "cypress/screenshots/**/*.png"
+ - "cypress/videos/**/*.mp4"
diff --git a/.buildkite/scripts/pipelines/pipeline_test.sh b/.buildkite/scripts/pipelines/pipeline_test.sh
index 7ff953c49ca..b8cd1d0c9be 100644
--- a/.buildkite/scripts/pipelines/pipeline_test.sh
+++ b/.buildkite/scripts/pipelines/pipeline_test.sh
@@ -54,6 +54,11 @@ case $TEST_TYPE in
DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn cypress install && yarn test-cypress --node-options=--max_old_space_size=2048")
;;
+ cypress:a11y)
+ echo "[TASK]: Running Cypress accessibility tests against React 18"
+ DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn cypress install && yarn run test-cypress-a11y --node-options=--max_old_space_size=2048")
+ ;;
+
*)
echo "[ERROR]: Unknown task"
echo "Exit code: 1"
diff --git a/.github/workflows/add_issues_to_project.yml b/.github/workflows/add_issues_to_project.yml
deleted file mode 100644
index c276c51c9fd..00000000000
--- a/.github/workflows/add_issues_to_project.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: Adds issues to project board
-
-on:
- issues:
- types:
- - opened
-
-jobs:
- add-to-project:
- name: Add issue to project
- runs-on: ubuntu-latest
- steps:
- - uses: actions/add-to-project@v0.4.0
- with:
- project-url: https://github.com/orgs/elastic/projects/1079
- github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
diff --git a/changelogs/upcoming/7352.md b/changelogs/upcoming/7352.md
new file mode 100644
index 00000000000..0ae57cf1474
--- /dev/null
+++ b/changelogs/upcoming/7352.md
@@ -0,0 +1,2 @@
+- Updated `EuiListGroupItem` to render an external icon and screen reader affordance for links with `target` set to to `_blank`
+- Updated `EuiListGroupItem` with a new `external` prop, which allows enabling or disabling the new external link icon
diff --git a/changelogs/upcoming/7360.md b/changelogs/upcoming/7360.md
new file mode 100644
index 00000000000..18056dd00c7
--- /dev/null
+++ b/changelogs/upcoming/7360.md
@@ -0,0 +1 @@
+- Updated `EuiText` to no longer set any opinionated styles on child ` ` tags - use `EuiImage` for image display within text instead
diff --git a/changelogs/upcoming/7361.md b/changelogs/upcoming/7361.md
new file mode 100644
index 00000000000..d33f9a1974c
--- /dev/null
+++ b/changelogs/upcoming/7361.md
@@ -0,0 +1,5 @@
+- Improved `EuiBasicTable`/`EuiInMemoryTable's mobile UI for custom actions
+
+**Accessibility**
+
+- Fixed custom `EuiBasicTable`/`EuiInMemoryTable` rendering nested interactive custom actions
diff --git a/scripts/test-a11y-docker.js b/scripts/test-a11y-docker.js
index b30e0fd2b48..65cc57244bf 100644
--- a/scripts/test-a11y-docker.js
+++ b/scripts/test-a11y-docker.js
@@ -5,15 +5,16 @@ execSync('docker pull docker.elastic.co/eui/ci:5.6', {
});
/* eslint-disable-next-line no-multi-str */
execSync(
- 'docker run \
- -i --rm --cap-add=SYS_ADMIN --volume=$(pwd):/app --workdir=/app \
+ "docker run \
+ -i --rm --cap-add=SYS_ADMIN --volume=$(pwd):/app --workdir=/app --platform=linux/amd64 \
-e GIT_COMMITTER_NAME=test -e GIT_COMMITTER_EMAIL=test -e HOME=/tmp \
--user=$(id -u):$(id -g) \
docker.elastic.co/eui/ci:5.6 \
- bash -c \'npm config set spin false \
- && /opt/yarn*/bin/yarn \
+ bash -c '/opt/yarn*/bin/yarn \
&& yarn cypress install \
- && NODE_OPTIONS="--max-old-space-size=2048" npm run test-cypress-a11y\'',
+ && yarn run test-cypress-a11y \
+ --node-options=--max_old_space_size=2048 \
+ --skip-css '", // Skipping CSS because compiling has a tendency to hang on Apple Silicon
{
stdio: 'inherit',
}
diff --git a/src-docs/src/views/list_group/list_group_example.js b/src-docs/src/views/list_group/list_group_example.js
index b34bad0618f..76a23f0b8d0 100644
--- a/src-docs/src/views/list_group/list_group_example.js
+++ b/src-docs/src/views/list_group/list_group_example.js
@@ -79,6 +79,17 @@ export const ListGroupExample = {
isActive and isDisabled {' '}
properties.
+
+ If your link is external or will open in a new tab, you can manually{' '}
+ set the external property. However, just like{' '}
+ with the{' '}
+
+ EuiLink
+ {' '}
+ component, setting{' '}
+ {'target="_blank"'} defaults to{' '}
+ {'external={true}'} .
+
As is done in this example, the EuiListGroup {' '}
component can also accept an array of items via the{' '}
diff --git a/src-docs/src/views/list_group/list_group_links.tsx b/src-docs/src/views/list_group/list_group_links.tsx
index 34fb9abeaee..283c4597e2d 100644
--- a/src-docs/src/views/list_group/list_group_links.tsx
+++ b/src-docs/src/views/list_group/list_group_links.tsx
@@ -26,9 +26,10 @@ const myContent = [
iconType: 'copyClipboard',
},
{
- label: 'Fifth link',
- href: '#/display/list-group',
+ label: 'Fifth link will open in new tab',
+ href: 'http://www.elastic.co',
iconType: 'crosshairs',
+ target: '_blank',
},
];
diff --git a/src-docs/src/views/tables/actions/actions.tsx b/src-docs/src/views/tables/actions/actions.tsx
index 86ac089a0af..a233c872ac1 100644
--- a/src-docs/src/views/tables/actions/actions.tsx
+++ b/src-docs/src/views/tables/actions/actions.tsx
@@ -171,6 +171,11 @@ export default () => {
);
},
},
+ {
+ render: () => {
+ return {}}>Edit ;
+ },
+ },
...actions,
];
}
diff --git a/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap b/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap
index f6ff384aa19..7d351bcfc26 100644
--- a/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap
+++ b/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap
@@ -1,6 +1,219 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`CollapsedItemActions render 1`] = `
+exports[`CollapsedItemActions custom actions 1`] = `
+
+
+
+
+`;
+
+exports[`CollapsedItemActions default actions 1`] = `
+
+
+
+
+`;
+
+exports[`CollapsedItemActions renders 1`] = `
`;
-
-exports[`CollapsedItemActions render with href and _target provided 1`] = `
-
- [Function]
-
- }
- closePopover={[Function]}
- display="inline-block"
- hasArrow={true}
- id="id-actions"
- isOpen={true}
- ownFocus={true}
- panelPaddingSize="none"
- popoverRef={[Function]}
- repositionToCrossAxis={true}
->
-
- default1
- ,
-
-
- ,
-
- default2
- ,
- ]
- }
- />
-
-`;
diff --git a/src/components/basic_table/__snapshots__/custom_item_action.test.tsx.snap b/src/components/basic_table/__snapshots__/custom_item_action.test.tsx.snap
index 578fe7defdf..f8b1953b4e4 100644
--- a/src/components/basic_table/__snapshots__/custom_item_action.test.tsx.snap
+++ b/src/components/basic_table/__snapshots__/custom_item_action.test.tsx.snap
@@ -4,10 +4,7 @@ exports[`CustomItemAction render 1`] = `
-
+
test
diff --git a/src/components/basic_table/action_types.ts b/src/components/basic_table/action_types.ts
index cc527d97930..09fb84d0109 100644
--- a/src/components/basic_table/action_types.ts
+++ b/src/components/basic_table/action_types.ts
@@ -72,7 +72,7 @@ export type DefaultItemAction = ExclusiveUnion<
export interface CustomItemAction {
/**
- * The function that renders the action. Note that the returned node is expected to have `onFocus` and `onBlur` functions
+ * Allows rendering a totally custom action
*/
render: (item: T, enabled: boolean) => ReactElement;
/**
diff --git a/src/components/basic_table/basic_table.a11y.tsx b/src/components/basic_table/basic_table.a11y.tsx
index 7a04e284fe7..45db1e36c4d 100644
--- a/src/components/basic_table/basic_table.a11y.tsx
+++ b/src/components/basic_table/basic_table.a11y.tsx
@@ -231,8 +231,7 @@ describe('EuiTable', () => {
describe('Keyboard accessibility', () => {
it('has zero violations after expanding a row', () => {
- cy.repeatRealPress('Tab');
- cy.get('button#1').should('have.focus');
+ cy.get('button#1').focus();
cy.realPress('Enter');
cy.get('tr.euiTableRow-isExpandedRow div.euiTableCellContent').should(
'exist'
diff --git a/src/components/basic_table/collapsed_item_actions.test.tsx b/src/components/basic_table/collapsed_item_actions.test.tsx
index 591a03d5d3e..f369e459f9f 100644
--- a/src/components/basic_table/collapsed_item_actions.test.tsx
+++ b/src/components/basic_table/collapsed_item_actions.test.tsx
@@ -6,14 +6,18 @@
* Side Public License, v 1.
*/
-import React, { FocusEvent } from 'react';
-import { shallow } from 'enzyme';
-import { render } from '../../test/rtl';
+import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import {
+ render,
+ waitForEuiPopoverOpen,
+ waitForEuiPopoverClose,
+} from '../../test/rtl';
+
import { CollapsedItemActions } from './collapsed_item_actions';
-import { Action } from './action_types';
describe('CollapsedItemActions', () => {
- test('render', () => {
+ it('renders', () => {
const props = {
actions: [
{
@@ -29,9 +33,7 @@ describe('CollapsedItemActions', () => {
],
itemId: 'id',
item: { id: '1' },
- actionEnabled: (_: Action<{ id: string }>) => true,
- onFocus: (_: FocusEvent) => {},
- onBlur: () => {},
+ actionEnabled: () => true,
};
const { container } = render( );
@@ -39,18 +41,14 @@ describe('CollapsedItemActions', () => {
expect(container.firstChild).toMatchSnapshot();
});
- test('render with href and _target provided', () => {
+ test('default actions', async () => {
const props = {
actions: [
{
name: 'default1',
description: 'default 1',
onClick: () => {},
- },
- {
- name: 'custom1',
- description: 'custom 1',
- render: () =>
,
+ 'data-test-subj': 'defaultAction',
},
{
name: 'default2',
@@ -61,14 +59,45 @@ describe('CollapsedItemActions', () => {
],
itemId: 'id',
item: { id: 'xyz' },
- actionEnabled: (_: Action<{ id: string }>) => true,
- onFocus: (_: FocusEvent) => {},
- onBlur: () => {},
+ actionEnabled: () => true,
+ };
+
+ const { getByTestSubject, baseElement } = render(
+
+ );
+ fireEvent.click(getByTestSubject('euiCollapsedItemActionsButton'));
+ await waitForEuiPopoverOpen();
+
+ expect(baseElement).toMatchSnapshot();
+
+ fireEvent.click(getByTestSubject('defaultAction'));
+ await waitForEuiPopoverClose();
+ });
+
+ test('custom actions', async () => {
+ const props = {
+ actions: [
+ { render: () => hello },
+ { render: () => world },
+ ],
+ itemId: 'id',
+ item: { id: 'xyz' },
+ actionEnabled: () => true,
};
- const component = shallow( );
- component.setState({ popoverOpen: true });
+ const { getByTestSubject, baseElement } = render(
+
+ );
+ fireEvent.click(getByTestSubject('euiCollapsedItemActionsButton'));
+ await waitForEuiPopoverOpen();
+
+ expect(
+ baseElement.querySelector('.euiBasicTable__collapsedCustomAction')
+ ?.nodeName
+ ).toEqual('DIV');
+ expect(baseElement).toMatchSnapshot();
- expect(component).toMatchSnapshot();
+ fireEvent.click(getByTestSubject('customAction'));
+ await waitForEuiPopoverClose();
});
});
diff --git a/src/components/basic_table/collapsed_item_actions.tsx b/src/components/basic_table/collapsed_item_actions.tsx
index 4e63e31dc1c..bc93d2ce296 100644
--- a/src/components/basic_table/collapsed_item_actions.tsx
+++ b/src/components/basic_table/collapsed_item_actions.tsx
@@ -6,200 +6,150 @@
* Side Public License, v 1.
*/
-import React, { Component, FocusEvent, ReactNode, ReactElement } from 'react';
+import React, {
+ useState,
+ useCallback,
+ useMemo,
+ ReactNode,
+ ReactElement,
+} from 'react';
+
import { isString } from '../../services/predicate';
import { EuiContextMenuItem, EuiContextMenuPanel } from '../context_menu';
import { EuiPopover } from '../popover';
import { EuiButtonIcon } from '../button';
import { EuiToolTip } from '../tool_tip';
import { EuiI18n } from '../i18n';
+
import { Action, CustomItemAction } from './action_types';
import { ItemIdResolved } from './table_types';
-export interface CollapsedItemActionsProps {
+export interface CollapsedItemActionsProps {
actions: Array>;
item: T;
itemId: ItemIdResolved;
actionEnabled: (action: Action) => boolean;
className?: string;
- onFocus?: (event: FocusEvent) => void;
- onBlur?: () => void;
-}
-
-interface CollapsedItemActionsState {
- popoverOpen: boolean;
}
-function actionIsCustomItemAction(
+const actionIsCustomItemAction = (
action: Action
-): action is CustomItemAction {
- return action.hasOwnProperty('render');
-}
-
-export class CollapsedItemActions extends Component<
- CollapsedItemActionsProps,
- CollapsedItemActionsState
-> {
- private popoverDiv: HTMLDivElement | null = null;
-
- state = { popoverOpen: false };
-
- togglePopover = () => {
- this.setState((prevState) => ({ popoverOpen: !prevState.popoverOpen }));
- };
-
- closePopover = () => {
- this.setState({ popoverOpen: false });
- };
-
- onPopoverBlur = () => {
- // you must be asking... WTF? I know... but this timeout is
- // required to make sure we process the onBlur events after the initial
- // event cycle. Reference:
- // https://medium.com/@jessebeach/dealing-with-focus-and-blur-in-a-composite-widget-in-react-90d3c3b49a9b
- window.requestAnimationFrame(() => {
- if (
- !this.popoverDiv!.contains(document.activeElement) &&
- this.props.onBlur
- ) {
- this.props.onBlur();
- }
- });
- };
-
- registerPopoverDiv = (popoverDiv: HTMLDivElement | null) => {
- if (!this.popoverDiv) {
- this.popoverDiv = popoverDiv;
- this.popoverDiv &&
- this.popoverDiv.addEventListener('focusout', this.onPopoverBlur);
- }
- };
-
- componentWillUnmount() {
- if (this.popoverDiv) {
- this.popoverDiv.removeEventListener('focusout', this.onPopoverBlur);
- }
- }
-
- onClickItem = (onClickAction: (() => void) | undefined) => {
- this.closePopover();
- if (onClickAction) {
- onClickAction();
- }
- };
-
- render() {
- const { actions, itemId, item, actionEnabled, onFocus, className } =
- this.props;
-
- const isOpen = this.state.popoverOpen;
-
- let allDisabled = true;
- const controls = actions.reduce(
- (controls, action, index) => {
- const key = `action_${itemId}_${index}`;
- const available = action.available ? action.available(item) : true;
- if (!available) {
- return controls;
- }
- const enabled = actionEnabled(action);
- allDisabled = allDisabled && !enabled;
- if (actionIsCustomItemAction(action)) {
- const customAction = action as CustomItemAction;
- const actionControl = customAction.render(item, enabled);
- const actionControlOnClick =
- actionControl && actionControl.props && actionControl.props.onClick;
- controls.push(
-
- this.onClickItem(
- actionControlOnClick
- ? () => actionControlOnClick(item)
- : undefined
- )
- }
- >
- {actionControl}
-
- );
- } else {
- const {
- onClick,
- name,
- href,
- target,
- 'data-test-subj': dataTestSubj,
- } = action;
-
- const buttonIcon = action.icon;
- let icon;
- if (buttonIcon) {
- icon = isString(buttonIcon) ? buttonIcon : buttonIcon(item);
- }
- const buttonContent = typeof name === 'function' ? name(item) : name;
-
- controls.push(
-
- this.onClickItem(onClick ? () => onClick(item) : undefined)
- }
- >
- {buttonContent}
-
- );
+): action is CustomItemAction => action.hasOwnProperty('render');
+
+export const CollapsedItemActions = ({
+ actions,
+ itemId,
+ item,
+ actionEnabled,
+ className,
+}: CollapsedItemActionsProps) => {
+ const [popoverOpen, setPopoverOpen] = useState(false);
+ const [allDisabled, setAllDisabled] = useState(true);
+
+ const onClickItem = useCallback((onClickAction?: () => void) => {
+ setPopoverOpen(false);
+ onClickAction?.();
+ }, []);
+
+ const controls = useMemo(() => {
+ return actions.reduce((controls, action, index) => {
+ const available = action.available?.(item) ?? true;
+ if (!available) return controls;
+
+ const enabled = actionEnabled(action);
+ if (enabled) setAllDisabled(false);
+
+ if (actionIsCustomItemAction(action)) {
+ const customAction = action as CustomItemAction;
+ const actionControl = customAction.render(item, enabled);
+ controls.push(
+ // Do not put the `onClick` on the EuiContextMenuItem itself - otherwise
+ // it renders a tag instead of a , and we end up with nested
+ // interactive elements
+
+ onClickItem()}>{actionControl}
+
+ );
+ } else {
+ const {
+ onClick,
+ name,
+ href,
+ target,
+ 'data-test-subj': dataTestSubj,
+ } = action;
+
+ const buttonIcon = action.icon;
+ let icon;
+ if (buttonIcon) {
+ icon = isString(buttonIcon) ? buttonIcon : buttonIcon(item);
}
- return controls;
- },
- []
- );
-
- const popoverButton = (
-
- {(allActions: string) => (
-
- )}
-
- );
-
- const withTooltip = !allDisabled && (
-
- {(allActions: ReactNode) => (
-
- {popoverButton}
-
- )}
-
- );
-
- return (
-
-
-
- );
- }
-}
+ const buttonContent = typeof name === 'function' ? name(item) : name;
+
+ controls.push(
+
+ onClickItem(onClick ? () => onClick(item) : undefined)
+ }
+ >
+ {buttonContent}
+
+ );
+ }
+ return controls;
+ }, []);
+ }, [actions, actionEnabled, item, onClickItem]);
+
+ const popoverButton = (
+
+ {(allActions: string) => (
+ setPopoverOpen((isOpen) => !isOpen)}
+ data-test-subj="euiCollapsedItemActionsButton"
+ />
+ )}
+
+ );
+
+ const withTooltip = !allDisabled && (
+
+ {(allActions: ReactNode) => (
+
+ {popoverButton}
+
+ )}
+
+ );
+
+ return (
+
setPopoverOpen(false)}
+ panelPaddingSize="none"
+ anchorPosition="leftCenter"
+ >
+
+
+ );
+};
diff --git a/src/components/basic_table/custom_item_action.tsx b/src/components/basic_table/custom_item_action.tsx
index 9236e9ac693..91351d5efbd 100644
--- a/src/components/basic_table/custom_item_action.tsx
+++ b/src/components/basic_table/custom_item_action.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import React, { Component, cloneElement } from 'react';
+import React from 'react';
import { CustomItemAction as Action } from './action_types';
export interface CustomItemActionProps
{
@@ -17,66 +17,11 @@ export interface CustomItemActionProps {
index?: number;
}
-interface CustomItemActionState {
- hasFocus: boolean;
-}
-
-export class CustomItemAction extends Component<
- CustomItemActionProps,
- CustomItemActionState
-> {
- private mounted: boolean;
-
- constructor(props: CustomItemActionProps) {
- super(props);
- this.state = { hasFocus: false };
-
- // while generally considered an anti-pattern, here we require
- // to do that as the onFocus/onBlur events of the action controls
- // may trigger while this component is unmounted. An alternative
- // (at least the workarounds suggested by react is to unregister
- // the onFocus/onBlur listeners from the action controls... this
- // unfortunately will lead to unnecessarily complex code... so we'll
- // stick to this approach for now)
- this.mounted = false;
- }
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- onFocus = () => {
- if (this.mounted) {
- this.setState({ hasFocus: true });
- }
- };
-
- onBlur = () => {
- if (this.mounted) {
- this.setState({ hasFocus: false });
- }
- };
-
- hasFocus = () => {
- return this.state.hasFocus;
- };
-
- render() {
- const { action, enabled, item, className } = this.props;
- const tool = action.render(item, enabled);
- const clonedTool = cloneElement(tool, {
- onFocus: this.onFocus,
- onBlur: this.onBlur,
- });
- const style = this.hasFocus() ? { opacity: 1 } : undefined;
- return (
-
- {clonedTool}
-
- );
- }
-}
+export const CustomItemAction = ({
+ action,
+ enabled,
+ item,
+ className,
+}: CustomItemActionProps) => (
+ {action.render(item, enabled)}
+);
diff --git a/src/components/basic_table/in_memory_table.a11y.tsx b/src/components/basic_table/in_memory_table.a11y.tsx
index 8f4e8de11d7..23156386dc7 100644
--- a/src/components/basic_table/in_memory_table.a11y.tsx
+++ b/src/components/basic_table/in_memory_table.a11y.tsx
@@ -163,36 +163,34 @@ describe('EuiInMemoryTable', () => {
});
it('has zero violations after sorting on a column', () => {
- cy.realPress('Tab');
cy.get('button[data-test-subj="tableHeaderSortButton"]')
.first()
- .should('have.focus');
- cy.realPress('Enter');
+ .focus();
+ cy.realPress('{enter}');
cy.checkAxe();
});
it('has zero violations when number of rows is increased by keyboard', () => {
- cy.repeatRealPress('Tab', 14);
cy.get('button[data-test-subj="tablePaginationPopoverButton"]')
- .should('have.focus')
- .realPress('Space');
- cy.get('div[data-popover-open="true"]').should('exist');
- cy.get('div[data-popover-open="true"]').should('have.focus');
- cy.repeatRealPress('Tab'); // Switched to Tab from ArrowDown because of flaky test runs
- cy.get('button[data-test-subj="tablePagination-25-rows"]').realPress(
- 'Space'
+ .focus()
+ .realPress('{enter}');
+ cy.get('div[data-popover-open="true"]', { timeout: 1000 }).should(
+ 'exist'
);
- cy.get('table.euiTable')
+ cy.repeatRealPress('Tab'); // Switched to Tab from ArrowDown because of flaky test runs
+ cy.get('button[data-test-subj="tablePagination-25-rows"]', {
+ timeout: 1000,
+ }).realPress('{enter}');
+ cy.get('table.euiTable', { timeout: 1000 })
.find('tr.euiTableRow')
.should('have.length', 20);
cy.checkAxe();
});
it('has zero violations when pagination is pressed', () => {
- cy.repeatRealPress('Tab', 15);
cy.get('a[data-test-subj="pagination-button-1"]')
- .should('have.focus')
- .realPress('Enter');
+ .focus()
+ .realPress('{enter}');
cy.get('button[data-test-subj="pagination-button-1"]').should(
'be.disabled'
);
diff --git a/src/components/card/card.a11y.tsx b/src/components/card/card.a11y.tsx
index ff51930e264..f04e8e26474 100644
--- a/src/components/card/card.a11y.tsx
+++ b/src/components/card/card.a11y.tsx
@@ -41,6 +41,7 @@ const Card = () => {
}
title="Sketch"
description="Example of a short card description."
@@ -125,24 +126,14 @@ describe('EuiCard', () => {
});
it('has zero violations after keyboard interaction', () => {
- cy.repeatRealPress('Tab');
- cy.realPress('Enter');
- cy.focused().should('have.attr', 'aria-checked', 'true');
- cy.repeatRealPress('Tab');
- cy.realPress('Enter');
- cy.focused().should('have.attr', 'aria-checked', 'true');
- cy.repeatRealPress('Tab');
- cy.realPress('Enter');
- cy.focused().should('have.attr', 'aria-checked', 'true');
- cy.checkAxe();
- cy.realPress('Enter');
- cy.focused().should('have.attr', 'aria-checked', 'false');
- cy.repeatRealPress(['Shift', 'Tab']);
- cy.realPress('Enter');
- cy.focused().should('have.attr', 'aria-checked', 'false');
- cy.repeatRealPress(['Shift', 'Tab']);
- cy.realPress('Enter');
- cy.focused().should('have.attr', 'aria-checked', 'false');
+ cy.get('div[data-test-subj="cy-card-1"]')
+ .find('button.euiButtonEmpty')
+ .focus();
+ cy.realPress('Tab');
+ cy.realPress('{enter}');
+ cy.get('div[data-test-subj="cy-card-1"]')
+ .find('button.euiButton')
+ .should('have.attr', 'aria-checked', 'true');
cy.checkAxe();
});
});
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_link.test.tsx.snap b/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_link.test.tsx.snap
index caea9f2b9c8..3b583eae7e1 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_link.test.tsx.snap
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_link.test.tsx.snap
@@ -20,13 +20,13 @@ exports[`EuiCollapsibleNavLink renders a link 1`] = `
>
Link
External link
(opens in a new tab or window)
diff --git a/src/components/error_boundary/error_boundary.a11y.tsx b/src/components/error_boundary/error_boundary.a11y.tsx
index 17626e67d14..a566946073d 100644
--- a/src/components/error_boundary/error_boundary.a11y.tsx
+++ b/src/components/error_boundary/error_boundary.a11y.tsx
@@ -58,7 +58,7 @@ describe('EuiErrorBoundary', () => {
});
it('has zero violations and accepts focus when the button is pressed', () => {
- cy.realPress('Tab');
+ cy.get('button').first().focus();
cy.realPress('Enter');
cy.get('pre.euiCodeBlock__pre').should('have.focus');
cy.checkAxe();
diff --git a/src/components/expression/expression.a11y.tsx b/src/components/expression/expression.a11y.tsx
index 3bc59c8fe88..0196888c3fe 100644
--- a/src/components/expression/expression.a11y.tsx
+++ b/src/components/expression/expression.a11y.tsx
@@ -225,10 +225,9 @@ describe('EuiExpression', () => {
});
it('has zero violations when first popover is interacted with by keyboard', () => {
- cy.realPress('Tab');
cy.get('button')
.contains(/When count\(\)/i)
- .should('have.focus');
+ .focus();
cy.realPress('Enter');
cy.get('div[data-test-subj="cy-expression-popover-1"]').should('exist');
cy.realPress('Tab');
@@ -241,10 +240,9 @@ describe('EuiExpression', () => {
});
it('has zero violations when second popover is interacted with by keyboard', () => {
- cy.repeatRealPress('Tab');
cy.get('button')
.contains(/Is above 100/i)
- .should('have.focus');
+ .focus();
cy.realPress('Enter');
cy.get('div[data-test-subj="cy-expression-popover-2"]').should('exist');
cy.realPress('Tab');
diff --git a/src/components/focus_trap/focus_trap.a11y.tsx b/src/components/focus_trap/focus_trap.a11y.tsx
index cc77255e321..eb6623b3ce8 100644
--- a/src/components/focus_trap/focus_trap.a11y.tsx
+++ b/src/components/focus_trap/focus_trap.a11y.tsx
@@ -118,15 +118,13 @@ describe('EuiFocusTrap', () => {
describe('Accessibility checks for keyboard navigation', () => {
it('allows the button outside the FocusTrap to receive focus', () => {
- cy.repeatRealPress('Tab', 5);
- cy.get('button[data-test-subj="cy-external-button"]').should(
- 'have.focus'
- );
+ cy.get('button[data-test-subj="cy-external-button"]').focus();
cy.checkAxe();
});
it('prevents the button outside the FocusTrap from receiving focus', () => {
- cy.repeatRealPress('Tab', 4);
+ cy.get('input[data-test-subj="cy-input-one"]').focus();
+ cy.repeatRealPress('Tab', 3);
cy.realPress('Enter');
cy.realPress('Tab');
cy.get('input[data-test-subj="cy-input-one"]').should('have.focus');
diff --git a/src/components/form/range/range.a11y.tsx b/src/components/form/range/range.a11y.tsx
index b43403bacf0..88f4ebcccca 100644
--- a/src/components/form/range/range.a11y.tsx
+++ b/src/components/form/range/range.a11y.tsx
@@ -53,16 +53,14 @@ describe('Single EuiRange', () => {
describe('Keyboard accessibility check', () => {
it('has zero violations when the range slider is increased', () => {
- cy.realPress('Tab');
- cy.get('input#cy-range-single').should('have.focus');
+ cy.get('input#cy-range-single').focus();
cy.repeatRealPress('ArrowRight', 10);
cy.get('output.euiRangeTooltip__value').contains('100 - 110');
cy.checkAxe();
});
it('has zero violations when the range slider is decreased', () => {
- cy.realPress('Tab');
- cy.get('input#cy-range-single').should('have.focus');
+ cy.get('input#cy-range-single').focus();
cy.repeatRealPress('ArrowLeft', 10);
cy.get('output.euiRangeTooltip__value').contains('100 - 100');
cy.checkAxe();
@@ -109,8 +107,7 @@ describe('Dual EuiRange', () => {
describe('Keyboard accessibility check', () => {
it('has zero violations when the range sliders are adjusted', () => {
- cy.realPress('Tab');
- cy.get('div[role="slider"]').first().should('have.focus');
+ cy.get('div[role="slider"]').first().focus();
cy.repeatRealPress('ArrowLeft', 3);
cy.get('div[role="slider"]')
.first()
@@ -164,8 +161,7 @@ describe('Highlight Area EuiRange', () => {
describe('Keyboard accessibility check', () => {
it('has zero violations when the highlight area is adjusted using arrow keys', () => {
- cy.realPress('Tab');
- cy.get('div[role="slider"]').first().should('have.focus');
+ cy.get('div[role="slider"]').first().focus();
cy.repeatRealPress('ArrowLeft', 3);
cy.get('div[role="slider"]')
.first()
@@ -216,8 +212,7 @@ describe('Highlight Area EuiRange', () => {
describe('Keyboard accessibility check', () => {
it('updates the range value using arrow keys with input[type="number"]', () => {
- cy.realPress('Tab');
- cy.get('input#cy-range-in-dropdown').should('have.focus');
+ cy.get('input#cy-range-in-dropdown').focus();
cy.repeatRealPress('ArrowUp', 10);
cy.get('input[type="range"]')
.first()
diff --git a/src/components/header/header.a11y.tsx b/src/components/header/header.a11y.tsx
index fe8ee58e1f1..25a44f0732f 100644
--- a/src/components/header/header.a11y.tsx
+++ b/src/components/header/header.a11y.tsx
@@ -404,7 +404,7 @@ describe('EuiHeader', () => {
});
it('has zero violations when a hidden breadcrumb is expanded', () => {
- cy.get('button[aria-label="See collapsed breadcrumbs"]').realClick();
+ cy.get('button[title="See collapsed breadcrumbs"]').realClick();
cy.get('a[data-test-subj="cy-breadcrumb-hidden"]').should('exist');
cy.checkAxe();
});
diff --git a/src/components/image/image.a11y.tsx b/src/components/image/image.a11y.tsx
index d4938872ebd..4c1b4f856c1 100644
--- a/src/components/image/image.a11y.tsx
+++ b/src/components/image/image.a11y.tsx
@@ -49,10 +49,7 @@ describe('EuiImage', () => {
});
it('has zero violations on keyboard interaction', () => {
- cy.realPress('Tab');
- cy.get('button[data-test-subj="activateFullScreenButton"]').should(
- 'have.focus'
- );
+ cy.get('button[data-test-subj="activateFullScreenButton"]').focus();
cy.realPress('Enter');
cy.get('button[data-test-subj="deactivateFullScreenButton"]').should(
'exist'
diff --git a/src/components/key_pad_menu/key_pad_menu.a11y.tsx b/src/components/key_pad_menu/key_pad_menu.a11y.tsx
index 8a35400dc43..769520aa095 100644
--- a/src/components/key_pad_menu/key_pad_menu.a11y.tsx
+++ b/src/components/key_pad_menu/key_pad_menu.a11y.tsx
@@ -122,8 +122,9 @@ describe('EuiKeyPadMenu', () => {
it('has zero violations on item click', () => {
cy.get('a[data-test-subj="cy-keypad-link-2"]').realClick();
cy.get('a[data-test-subj="cy-keypad-link-2"]').should(
- 'have.class',
- 'euiKeyPadMenuItem-isSelected'
+ 'have.attr',
+ 'aria-current',
+ 'true'
);
cy.checkAxe();
});
@@ -135,23 +136,24 @@ describe('EuiKeyPadMenu', () => {
);
cy.realPress('Space');
cy.get('button[data-test-subj="cy-keypad-button-3"]').should(
- 'have.class',
- 'euiKeyPadMenuItem-isSelected'
+ 'have.attr',
+ 'aria-pressed',
+ 'true'
);
cy.checkAxe();
cy.realPress(['Shift', 'Tab']);
cy.get('button[data-test-subj="cy-keypad-button-2"]').should(
- 'have.focus'
+ 'have.attr',
+ 'aria-pressed',
+ 'false'
);
cy.realPress('Space');
- cy.get('button[data-test-subj="cy-keypad-button-2"]').should(
- 'have.class',
- 'euiKeyPadMenuItem-isSelected'
- );
- cy.get('button[data-test-subj="cy-keypad-button-3"]').should(
- 'not.have.class',
- 'euiKeyPadMenuItem-isSelected'
- );
+ cy.get('button[data-test-subj="cy-keypad-button-2"]')
+ .invoke('attr', 'aria-pressed')
+ .should('equal', 'true');
+ cy.get('button[data-test-subj="cy-keypad-button-3"]')
+ .invoke('attr', 'aria-pressed')
+ .should('equal', 'false');
cy.checkAxe();
});
});
diff --git a/src/components/link/__snapshots__/link.test.tsx.snap b/src/components/link/__snapshots__/link.test.tsx.snap
index 0cbb30de8b8..226cb0a1f84 100644
--- a/src/components/link/__snapshots__/link.test.tsx.snap
+++ b/src/components/link/__snapshots__/link.test.tsx.snap
@@ -7,21 +7,6 @@ exports[`EuiLink accent is rendered 1`] = `
/>
`;
-exports[`EuiLink allows for target and external to be controlled independently 1`] = `
-
-
- (opens in a new tab or window)
-
-
-`;
-
exports[`EuiLink button respects the type property 1`] = `
External link
@@ -147,13 +132,13 @@ exports[`EuiLink supports target 1`] = `
target="_blank"
>
External link
(opens in a new tab or window)
diff --git a/src/components/link/external_link_icon.test.tsx b/src/components/link/external_link_icon.test.tsx
new file mode 100644
index 00000000000..5a16c7e9d8c
--- /dev/null
+++ b/src/components/link/external_link_icon.test.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { render } from '../../test/rtl';
+import { shouldRenderCustomStyles } from '../../test/internal';
+
+import { EuiExternalLinkIcon } from './external_link_icon';
+
+// Note - the icon is not actually text, but it's mocked as such
+describe('EuiExternalLinkIcon', () => {
+ shouldRenderCustomStyles( );
+
+ it('always renders the icon if `external` is true', () => {
+ const { container } = render( );
+ expect(container).toMatchInlineSnapshot(`
+
+
+ External link
+
+
+ `);
+ });
+
+ describe('target="_blank"', () => {
+ it('renders the icon by default, along with screen reader text', () => {
+ const { container } = render( );
+ expect(container).toMatchInlineSnapshot(`
+
+
+ External link
+
+
+ (opens in a new tab or window)
+
+
+ `);
+ });
+
+ it('hides the icon if `external` is false, but still shows the screen reader text', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchInlineSnapshot(`
+
+
+ (opens in a new tab or window)
+
+
+ `);
+ });
+ });
+
+ it('renders nothing if neither external nor target="_blank" are set', () => {
+ const { container } = render( );
+ expect(container).toMatchInlineSnapshot(`
`);
+ });
+
+ it('allows configuring the icon props', () => {
+ const { getByTestSubject } = render(
+
+ );
+ expect(getByTestSubject('test')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/link/external_link_icon.tsx b/src/components/link/external_link_icon.tsx
new file mode 100644
index 00000000000..f74093c46c2
--- /dev/null
+++ b/src/components/link/external_link_icon.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { FunctionComponent, AnchorHTMLAttributes } from 'react';
+
+import { useEuiTheme } from '../../services';
+import { logicalStyle } from '../../global_styling';
+import { EuiIcon, EuiIconProps } from '../icon';
+import { EuiI18n, useEuiI18n } from '../i18n';
+import { EuiScreenReaderOnly } from '../accessibility';
+
+/**
+ * DRY util for indicating external links both via icon and to
+ * screen readers. Used internally by at EuiLink and EuiListGroupItem
+ */
+
+export type EuiExternalLinkIconProps = {
+ target?: AnchorHTMLAttributes['target'];
+ /**
+ * Set to true to show an icon indicating that it is an external link;
+ * Defaults to true if `target="_blank"`
+ */
+ external?: boolean;
+};
+
+export const EuiExternalLinkIcon: FunctionComponent<
+ EuiExternalLinkIconProps & Partial
+> = ({ target, external, ...rest }) => {
+ const { euiTheme } = useEuiTheme();
+
+ const showExternalLinkIcon =
+ (target === '_blank' && external !== false) || external === true;
+
+ const iconAriaLabel = useEuiI18n(
+ 'euiExternalLinkIcon.ariaLabel',
+ 'External link'
+ );
+
+ return (
+ <>
+ {showExternalLinkIcon && (
+
+ )}
+ {target === '_blank' && (
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/src/components/link/link.styles.ts b/src/components/link/link.styles.ts
index 3abe2ab8a24..7dd64046b26 100644
--- a/src/components/link/link.styles.ts
+++ b/src/components/link/link.styles.ts
@@ -8,11 +8,7 @@
import { css } from '@emotion/react';
import { UseEuiTheme } from '../../services';
-import {
- euiFocusRing,
- logicalCSS,
- logicalTextAlignCSS,
-} from '../../global_styling';
+import { euiFocusRing, logicalTextAlignCSS } from '../../global_styling';
const _colorCSS = (color: string) => {
return `
@@ -87,13 +83,5 @@ export const euiLinkStyles = (euiThemeContext: UseEuiTheme) => {
warning: css(_colorCSS(euiTheme.colors.warningText)),
ghost: css(_colorCSS(euiTheme.colors.ghost)),
text: css(_colorCSS(euiTheme.colors.text)),
-
- // Children
- euiLink__screenReaderText: css`
- ${logicalCSS('left', '0px')}
- `,
- euiLink__externalIcon: css`
- ${logicalCSS('margin-left', euiTheme.size.xs)}
- `,
};
};
diff --git a/src/components/link/link.test.tsx b/src/components/link/link.test.tsx
index db039cef184..220d0613047 100644
--- a/src/components/link/link.test.tsx
+++ b/src/components/link/link.test.tsx
@@ -59,13 +59,6 @@ describe('EuiLink', () => {
expect(container.firstChild).toMatchSnapshot();
});
- test('allows for target and external to be controlled independently', () => {
- const { container } = render(
-
- );
- expect(container.firstChild).toMatchSnapshot();
- });
-
test('supports rel', () => {
const { container } = render( );
expect(container.firstChild).toMatchSnapshot();
diff --git a/src/components/link/link.tsx b/src/components/link/link.tsx
index 434352b3641..dc7c84da32c 100644
--- a/src/components/link/link.tsx
+++ b/src/components/link/link.tsx
@@ -13,14 +13,14 @@ import React, {
MouseEventHandler,
} from 'react';
import classNames from 'classnames';
+
import { getSecureRelForTarget, useEuiTheme } from '../../services';
-import { euiLinkStyles } from './link.styles';
-import { EuiIcon } from '../icon';
-import { EuiI18n, useEuiI18n } from '../i18n';
import { CommonProps, ExclusiveUnion } from '../common';
-import { EuiScreenReaderOnly } from '../accessibility';
import { validateHref } from '../../services/security/href_validator';
+import { EuiExternalLinkIcon } from './external_link_icon';
+import { euiLinkStyles } from './link.styles';
+
export type EuiLinkType = 'button' | 'reset' | 'submit';
export const COLORS = [
@@ -95,32 +95,10 @@ const EuiLink = forwardRef(
const euiTheme = useEuiTheme();
const styles = euiLinkStyles(euiTheme);
const cssStyles = [styles.euiLink];
- const cssScreenReaderTextStyles = [styles.euiLink__screenReaderText];
- const cssExternalLinkIconStyles = [styles.euiLink__externalIcon];
const isHrefValid = !href || validateHref(href);
const disabled = _disabled || !isHrefValid;
- const newTargetScreenreaderText = (
-
-
-
-
-
- );
-
- const externalLinkIcon = (
-
- );
-
if (href === undefined || !isHrefValid) {
const buttonProps = {
className: classNames('euiLink', className),
@@ -152,8 +130,6 @@ const EuiLink = forwardRef(
onClick,
...rest,
};
- const showExternalLinkIcon =
- (target === '_blank' && external !== false) || external === true;
return (
(
{...(anchorProps as EuiLinkAnchorProps)}
>
{children}
- {showExternalLinkIcon && externalLinkIcon}
- {target === '_blank' && newTargetScreenreaderText}
+
);
}
diff --git a/src/components/list_group/list_group_item.styles.ts b/src/components/list_group/list_group_item.styles.ts
index 47204d84e58..d68dba8675f 100644
--- a/src/components/list_group/list_group_item.styles.ts
+++ b/src/components/list_group/list_group_item.styles.ts
@@ -163,6 +163,9 @@ export const euiListGroupItemInnerStyles = (euiThemeContext: UseEuiTheme) => {
text-decoration: underline;
}
`,
+ externalIcon: css`
+ ${logicalCSS('margin-left', euiTheme.size.xs)}
+ `,
};
};
diff --git a/src/components/list_group/list_group_item.test.tsx b/src/components/list_group/list_group_item.test.tsx
index 4230910b515..c41cb0f5dfd 100644
--- a/src/components/list_group/list_group_item.test.tsx
+++ b/src/components/list_group/list_group_item.test.tsx
@@ -7,6 +7,7 @@
*/
import React from 'react';
+
import { fireEvent } from '@testing-library/react';
import { shouldRenderCustomStyles } from '../../test/internal';
import { requiredProps } from '../../test/required_props';
@@ -205,6 +206,22 @@ describe('EuiListGroupItem', () => {
});
});
+ describe('external', () => {
+ test('is rendered', () => {
+ const { getByText } = render(
+
+ );
+ expect(getByText('External link')).toBeInTheDocument();
+ });
+
+ test('target `_blank` renders external icon', () => {
+ const { getByText } = render(
+
+ );
+ expect(getByText('External link')).toBeInTheDocument();
+ });
+ });
+
describe('onClick', () => {
test('is rendered', () => {
const { container } = render(
diff --git a/src/components/list_group/list_group_item.tsx b/src/components/list_group/list_group_item.tsx
index 4ee2d2cc93d..f28301bdce9 100644
--- a/src/components/list_group/list_group_item.tsx
+++ b/src/components/list_group/list_group_item.tsx
@@ -33,6 +33,7 @@ import {
cloneElementWithCss,
} from '../../services';
import { validateHref } from '../../services/security/href_validator';
+import { EuiExternalLinkIcon } from '../link/external_link_icon';
import {
euiListGroupItemStyles,
@@ -89,10 +90,13 @@ export type EuiListGroupItemProps = CommonProps &
* While permitted, `href` and `onClick` should not be used together in most cases and may create problems.
*/
href?: string;
-
- target?: string;
-
rel?: string;
+ target?: string;
+ /**
+ * Set to true to show an icon indicating that it is an external link;
+ * Defaults to true if `target="_blank"`
+ */
+ external?: boolean;
/**
* Adds `EuiIcon` of `EuiIcon.type`
@@ -157,8 +161,9 @@ export const EuiListGroupItem: FunctionComponent = ({
isActive = false,
isDisabled: _isDisabled = false,
href,
- target,
rel,
+ target,
+ external,
className,
css: customCss,
style,
@@ -289,6 +294,7 @@ export const EuiListGroupItem: FunctionComponent = ({
>
{iconNode}
{labelContent}
+
);
} else if ((href && isDisabled) || onClick) {
diff --git a/src/components/notification/notification_event.a11y.tsx b/src/components/notification/notification_event.a11y.tsx
index 9fe561b8867..9078045967a 100644
--- a/src/components/notification/notification_event.a11y.tsx
+++ b/src/components/notification/notification_event.a11y.tsx
@@ -97,10 +97,9 @@ describe('EuiNotificationEvent', () => {
describe('Keyboard accessibility', () => {
it('has zero violations when the popover is opened by keyboard', () => {
- cy.repeatRealPress('Tab');
cy.get(
'button[data-test-subj="cy-eui-notification-1-notificationEventMetaButton"]'
- ).should('have.focus');
+ ).focus();
cy.realPress('Enter');
cy.get('div.euiPopover__panel').should('exist');
cy.checkAxe();
diff --git a/src/components/resizable_container/resizable_container.a11y.tsx b/src/components/resizable_container/resizable_container.a11y.tsx
index 9ac295ee168..968910a06b2 100644
--- a/src/components/resizable_container/resizable_container.a11y.tsx
+++ b/src/components/resizable_container/resizable_container.a11y.tsx
@@ -61,24 +61,21 @@ describe('Horizontal EuiResizableContainer', () => {
describe('Keyboard accessibility check', () => {
it('has zero violations when first panel is scrolled vertically', () => {
- cy.realPress('Tab');
- cy.get('div.euiPanel').first().should('have.focus');
+ cy.get('div.euiPanel').first().focus();
cy.realPress('End');
cy.get('a[data-test-subj="hello-world-link"]').should('be.visible');
cy.checkAxe();
});
it('has zero violations when second panel is scrolled vertically', () => {
- cy.repeatRealPress('Tab', 4);
- cy.get('div.euiPanel').last().should('have.focus');
+ cy.get('div.euiPanel').last().focus();
cy.realPress('End');
cy.realPress('Home');
cy.checkAxe();
});
it('has zero violations when the horizontal panels are resized', () => {
- cy.repeatRealPress('Tab', 3);
- cy.get('button.euiResizableButton').should('have.focus');
+ cy.get('button.euiResizableButton').focus();
cy.repeatRealPress('ArrowRight', 10);
cy.checkAxe();
});
@@ -120,8 +117,7 @@ describe('Vertical EuiResizableContainer', () => {
});
it('has zero violations when the vertical panels are resized', () => {
- cy.repeatRealPress('Tab');
- cy.get('button.euiResizableButton').should('have.focus');
+ cy.get('button.euiResizableButton').focus();
cy.repeatRealPress('ArrowDown', 10);
cy.checkAxe();
});
diff --git a/src/components/search_bar/search_bar.a11y.tsx b/src/components/search_bar/search_bar.a11y.tsx
index 5eddb292805..d515af29a1c 100644
--- a/src/components/search_bar/search_bar.a11y.tsx
+++ b/src/components/search_bar/search_bar.a11y.tsx
@@ -189,8 +189,7 @@ describe('EuiSearchBar', () => {
describe('Keyboard accessibility', () => {
it('has zero violations after a full-text search', () => {
- cy.realPress('Tab');
- cy.get('input[type="search"]').should('have.focus');
+ cy.get('input[type="search"]').focus();
cy.get('input[type="search"]').type('watch');
cy.realPress('Enter');
cy.get('table.euiTable tbody').find('tr').should('have.length', 1);
@@ -203,8 +202,7 @@ describe('EuiSearchBar', () => {
});
it('has zero violations after filtering on Open items', () => {
- cy.repeatRealPress('Tab');
- cy.get('button.euiButtonEmpty').first().should('have.focus');
+ cy.get('button.euiButtonEmpty').first().focus();
cy.realPress('Enter');
cy.get('table.euiTable tbody').find('tr').should('have.length', 3);
cy.checkAxe();
@@ -216,8 +214,7 @@ describe('EuiSearchBar', () => {
});
it('has zero violations after filtering by Tags', () => {
- cy.repeatRealPress('Tab', 4);
- cy.get('button.euiButtonEmpty').last().should('have.focus');
+ cy.get('button.euiButtonEmpty').last().focus();
cy.realPress('Enter');
cy.realPress('Tab');
cy.realPress('ArrowDown');
diff --git a/src/components/table/_responsive.scss b/src/components/table/_responsive.scss
index 58f8950e89d..5b11913459f 100644
--- a/src/components/table/_responsive.scss
+++ b/src/components/table/_responsive.scss
@@ -99,6 +99,25 @@
}
}
+ // Custom actions
+ &:not(.euiTableRow-hasActions) .euiTableRowCell--hasActions:last-child {
+ width: 100%;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: $euiBorderWidthThin;
+ background-color: $euiTableActionsBorderColor;
+ }
+
+ .euiTableCellContent {
+ position: relative;
+ top: $euiSizeXS;
+ }
+ }
+
&.euiTableRow-hasActions.euiTableRow-isExpandable {
.euiTableRowCell--isExpander {
top: auto;
diff --git a/src/components/text/__snapshots__/text.styles.test.ts.snap b/src/components/text/__snapshots__/text.styles.test.ts.snap
index 5fa6844096e..f4937379d57 100644
--- a/src/components/text/__snapshots__/text.styles.test.ts.snap
+++ b/src/components/text/__snapshots__/text.styles.test.ts.snap
@@ -81,7 +81,6 @@ exports[`euiTextStyles sizes m 1`] = `
p,
dl,
blockquote,
- img,
pre,
> ul,
> ol {
@@ -243,7 +242,6 @@ exports[`euiTextStyles sizes relative 1`] = `
p,
dl,
blockquote,
- img,
pre,
> ul,
> ol {
@@ -390,7 +388,6 @@ exports[`euiTextStyles sizes s 1`] = `
p,
dl,
blockquote,
- img,
pre,
> ul,
> ol {
@@ -537,7 +534,6 @@ exports[`euiTextStyles sizes xs 1`] = `
p,
dl,
blockquote,
- img,
pre,
> ul,
> ol {
diff --git a/src/components/text/text.styles.ts b/src/components/text/text.styles.ts
index 0bcc68b2c45..fbafc8bb87c 100644
--- a/src/components/text/text.styles.ts
+++ b/src/components/text/text.styles.ts
@@ -144,7 +144,6 @@ const euiScaleText = (
p,
dl,
blockquote,
- img,
pre,
> ul,
> ol {
@@ -249,11 +248,6 @@ export const euiTextStyles = (euiThemeContext: UseEuiTheme) => {
${euiLinkCSS(euiThemeContext)}
}
- img {
- display: block;
- ${logicalCSS('max-width', '100%')}
- }
-
ul {
list-style: disc;
}