diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30fda8c0b68..02ed781c168 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- Added `markdownFormatProps` prop to `EuiMarkdownEditor` to extend the props passed to the rendered `EuiMarkdownFormat` ([#4663](https://github.com/elastic/eui/pull/4663))
- Added optional virtualized line rendering to `EuiCodeBlock` ([#4952](https://github.com/elastic/eui/pull/4952))
- Added `current` as a `status` of `EuiHorizontalStep` ([#4911](https://github.com/elastic/eui/pull/4911))
+- Improved accessibility of `EuiBreadcrumbs` ([#4763](https://github.com/elastic/eui/pull/4763))
- Exported `onChange` type for `EuiSearchBar` ([#4968](https://github.com/elastic/eui/pull/4968))
**Bug fixes**
diff --git a/src-docs/src/views/breadcrumbs/breadcrumbs.js b/src-docs/src/views/breadcrumbs/breadcrumbs.js
index 7be7e72c7da..118c0ae4f2d 100644
--- a/src-docs/src/views/breadcrumbs/breadcrumbs.js
+++ b/src-docs/src/views/breadcrumbs/breadcrumbs.js
@@ -32,6 +32,10 @@ export default () => {
},
{
text: 'Edit',
+ href: '#',
+ onClick: (e) => {
+ e.preventDefault();
+ },
},
];
diff --git a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap
index 0e37e6eb929..03e6b45122a 100644
--- a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap
+++ b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap
@@ -6,529 +6,716 @@ exports[`EuiBreadcrumbs is rendered 1`] = `
class="euiBreadcrumbs testClass1 testClass2 euiBreadcrumbs--truncate"
data-test-subj="test subject string"
>
-
- Animals
-
-
-
- Metazoans
-
-
-
-
+
+ Animals
+
+
+
+
+ Metazoans
+
+
+
+
+
+
-
-
-
-
-
-
- Boa constrictor
-
-
-
- Edit
-
+
+
+
+ Boa constrictor
+
+
+
+
+ Edit
+
+
+
`;
-exports[`EuiBreadcrumbs props max doesn't break when max exceeds the number of breadcrumbs 1`] = `
+exports[`EuiBreadcrumbs is rendered with final item as link 1`] = `
`;
-exports[`EuiBreadcrumbs props max renders 1 item 1`] = `
+exports[`EuiBreadcrumbs props max doesn't break when max exceeds the number of breadcrumbs 1`] = `
+`;
+
+exports[`EuiBreadcrumbs props max renders 1 item 1`] = `
+
`;
exports[`EuiBreadcrumbs props max renders all items with null 1`] = `
`;
exports[`EuiBreadcrumbs props responsive is rendered 1`] = `
`;
exports[`EuiBreadcrumbs props responsive is rendered as false 1`] = `
`;
exports[`EuiBreadcrumbs props responsive is rendered with custom breakpoints 1`] = `
`;
exports[`EuiBreadcrumbs props truncate as false is rendered 1`] = `
`;
diff --git a/src/components/breadcrumbs/_breadcrumbs.scss b/src/components/breadcrumbs/_breadcrumbs.scss
index d344b02d303..222912a583a 100644
--- a/src/components/breadcrumbs/_breadcrumbs.scss
+++ b/src/components/breadcrumbs/_breadcrumbs.scss
@@ -1,68 +1,82 @@
+@import '../link/variables';
+
/**
* 1. Add vertical space between breadcrumbs,
* but make sure the whole breadcrumb set doesn't add space below itself
*/
-.euiBreadcrumbs {
+.euiBreadcrumbs__list {
@include euiFontSizeS;
- margin-bottom: -$euiSizeXS; /* 1 */
display: flex;
align-items: center;
flex-wrap: wrap;
min-width: 0; // Ensure it shrinks if the window is narrow
+ margin-bottom: -$euiSizeXS; /* 1 */
}
.euiBreadcrumb {
- display: inline-block;
margin-bottom: $euiSizeXS; /* 1 */
+ display: flex;
+ align-items: center;
&:not(.euiBreadcrumb--last) {
- margin-right: $euiBreadcrumbSpacing;
color: $euiTextSubduedColor;
- }
-}
-.euiBreadcrumb--last {
- font-weight: $euiFontWeightMedium;
+ &::after {
+ content: '';
+ margin: $euiSizeXS $euiBreadcrumbSpacing 0;
+ width: 1px;
+ height: $euiSize;
+ transform: translateY(-1px) rotate(15deg);
+ background: $euiColorLightShade;
+ flex-shrink: 0;
+ }
+ }
}
.euiBreadcrumb--collapsed {
flex-shrink: 0;
}
-.euiBreadcrumbSeparator {
- flex-shrink: 0;
- display: inline-block;
- margin-right: $euiBreadcrumbSpacing;
- width: 1px;
- height: $euiSize;
- transform: translateY(-1px) rotate(15deg);
- background: $euiColorLightShade;
+.euiBreadcrumb--last {
+ font-weight: $euiFontWeightMedium;
}
-.euiBreadcrumbs__inPopover .euiBreadcrumb--last {
+.euiBreadcrumbs__inPopover .euiBreadcrumb--last .euiBreadcrumb__content {
font-weight: $euiFontWeightRegular;
- color: $euiColorDarkShade !important; // sass-lint:disable-line no-important
+ // Match to subdued EuiLink color
+ color: map-get($euiLinkColors, 'subdued');
}
.euiBreadcrumbs--truncate {
- white-space: nowrap;
- flex-wrap: nowrap;
+ .euiBreadcrumbs__list {
+ white-space: nowrap;
+ flex-wrap: nowrap;
+ }
.euiBreadcrumb:not(.euiBreadcrumb--collapsed) {
- max-width: $euiBreadcrumbTruncateWidth;
- overflow: hidden;
- text-overflow: ellipsis;
+ .euiBreadcrumb__content {
+ max-width: $euiBreadcrumbTruncateWidth;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
- &.euiBreadcrumb--last {
+ &.euiBreadcrumb--last .euiBreadcrumb__content {
max-width: none;
}
}
+
+ .euiBreadcrumb {
+ overflow: hidden;
+ }
}
+.euiBreadcrumbs--truncate,
.euiBreadcrumb--truncate {
- @include euiTextTruncate;
- max-width: $euiBreadcrumbTruncateWidth;
- text-align: center;
- vertical-align: top; // overflow hidden causes misalignment of links and slashes, this fixes that
+ .euiBreadcrumb__content {
+ @include euiTextTruncate;
+ max-width: $euiBreadcrumbTruncateWidth;
+ text-align: center;
+ vertical-align: baseline;
+ }
}
diff --git a/src/components/breadcrumbs/breadcrumbs.test.tsx b/src/components/breadcrumbs/breadcrumbs.test.tsx
index b6c1c0d500d..709f2708bac 100644
--- a/src/components/breadcrumbs/breadcrumbs.test.tsx
+++ b/src/components/breadcrumbs/breadcrumbs.test.tsx
@@ -63,6 +63,15 @@ describe('EuiBreadcrumbs', () => {
expect(component).toMatchSnapshot();
});
+ test('is rendered with final item as link', () => {
+ const customBreadcrumbs = [...breadcrumbs, { text: 'test', href: '#' }];
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
describe('props', () => {
describe('responsive', () => {
test('is rendered', () => {
diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx
index 561ce00abb2..ddddbce606e 100644
--- a/src/components/breadcrumbs/breadcrumbs.tsx
+++ b/src/components/breadcrumbs/breadcrumbs.tsx
@@ -7,7 +7,7 @@
*/
import React, {
- Fragment,
+ AriaAttributes,
FunctionComponent,
MouseEventHandler,
ReactNode,
@@ -17,7 +17,7 @@ import React, {
import classNames from 'classnames';
import { CommonProps } from '../common';
-import { EuiI18n } from '../i18n';
+import { useEuiI18n } from '../i18n';
import { EuiInnerText } from '../inner_text';
import { EuiLink } from '../link';
import { EuiPopover } from '../popover';
@@ -25,6 +25,8 @@ import { EuiIcon } from '../icon';
import { throttle } from '../../services';
import { EuiBreakpointSize, getBreakpoint } from '../../services/breakpoint';
+const CONTENT_CLASSNAME = 'euiBreadcrumb__content';
+
export type EuiBreadcrumbResponsiveMaxCount = {
/**
* Any of the following keys are allowed: `'xs' | 's' | 'm' | 'l' | 'xl'`
@@ -44,6 +46,10 @@ export type EuiBreadcrumb = CommonProps & {
* Force a max-width on the breadcrumb text
*/
truncate?: boolean;
+ /**
+ * Override the existing `aria-current` which defaults to `page` for the last breadcrumb
+ */
+ 'aria-current'?: AriaAttributes['aria-current'];
};
export type EuiBreadcrumbsProps = CommonProps & {
@@ -97,6 +103,11 @@ const limitBreadcrumbs = (
start + breadcrumbs.length - limit
);
+ if (overflowBreadcrumbs.length) {
+ overflowBreadcrumbs[overflowBreadcrumbs.length - 1]['aria-current'] =
+ 'false';
+ }
+
for (let i = 0; i < limit; i++) {
// We'll alternate with displaying breadcrumbs at the end and at the start, but be biased
// towards breadcrumbs the end so that if max is an odd number, we'll have one more
@@ -120,28 +131,25 @@ const limitBreadcrumbs = (
const EuiBreadcrumbCollapsed = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const ariaLabel = useEuiI18n(
+ 'euiBreadcrumbs.collapsedBadge.ariaLabel',
+ 'See collapsed breadcrumbs'
+ );
const ellipsisButton = (
-
- {(ariaLabel: string) => (
- setIsPopoverOpen(!isPopoverOpen)}>
- …
-
- )}
-
+ setIsPopoverOpen(!isPopoverOpen)}>
+ …
+
);
return (
-
+
setIsPopoverOpen(false)}>
@@ -153,8 +161,7 @@ const limitBreadcrumbs = (
max={0}
/>
-
-
+
);
};
@@ -165,8 +172,6 @@ const limitBreadcrumbs = (
return [...breadcrumbsAtStart, ...breadcrumbsAtEnd];
};
-const EuiBreadcrumbSeparator = () => ;
-
export const EuiBreadcrumbs: FunctionComponent = ({
breadcrumbs,
className,
@@ -175,6 +180,7 @@ export const EuiBreadcrumbs: FunctionComponent = ({
max = 5,
...rest
}) => {
+ const ariaLabel = useEuiI18n('euiBreadcrumbs.nav.ariaLabel', 'Breadcrumbs');
const [currentBreakpoint, setCurrentBreakpoint] = useState(
getBreakpoint(typeof window === 'undefined' ? -Infinity : window.innerWidth)
);
@@ -205,61 +211,49 @@ export const EuiBreadcrumbs: FunctionComponent = ({
className: breadcrumbClassName,
...breadcrumbRest
} = breadcrumb;
-
const isLastBreadcrumb = index === breadcrumbs.length - 1;
-
- const breadcrumbClasses = classNames('euiBreadcrumb', breadcrumbClassName, {
+ const className = classNames('euiBreadcrumb', {
'euiBreadcrumb--last': isLastBreadcrumb,
'euiBreadcrumb--truncate': truncate,
});
-
- let link;
-
- if (!href && !onClick) {
- link = (
-
- {(ref, innerText) => (
-
- {text}
-
- )}
-
- );
- } else {
- link = (
-
- {(ref, innerText) => (
+ const linkProps = {
+ className: classNames(CONTENT_CLASSNAME, breadcrumbClassName),
+ 'aria-current': isLastBreadcrumb ? 'page' : undefined,
+ } as { className: string; 'aria-current': AriaAttributes['aria-current'] };
+
+ const link = (
+
+ {(ref, innerText) => {
+ const title = innerText === '' ? undefined : innerText;
+
+ if (!href && !onClick) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ return (
{text}
- )}
-
- );
- }
-
- let separator;
-
- if (!isLastBreadcrumb) {
- separator = ;
- }
+ );
+ }}
+
+ );
return (
-
+
{link}
- {separator}
-
+
);
});
@@ -289,8 +283,8 @@ export const EuiBreadcrumbs: FunctionComponent = ({
});
return (
-
`;
diff --git a/src/components/header/header_breadcrumbs/_header_breadcrumbs.scss b/src/components/header/header_breadcrumbs/_header_breadcrumbs.scss
index ce8fa5e2c0d..eb3d083c48b 100644
--- a/src/components/header/header_breadcrumbs/_header_breadcrumbs.scss
+++ b/src/components/header/header_breadcrumbs/_header_breadcrumbs.scss
@@ -1,6 +1,5 @@
-// Breadcrumb navigation included in the header.
-
.euiHeaderBreadcrumbs {
+ @include euiTextTruncate;
margin-left: $euiSizeM;
margin-right: $euiSizeM;
display: flex;
diff --git a/src/components/link/_link.scss b/src/components/link/_link.scss
index 755cf6de466..7b2f22289bb 100644
--- a/src/components/link/_link.scss
+++ b/src/components/link/_link.scss
@@ -28,12 +28,6 @@
&:focus {
@include euiFocusRing(null, 'outer');
-
- @if ($name == 'ghost') {
- background-color: shade($color, $euiFocusTransparencyPercent);
- } @else {
- @include euiFocusBackground($color);
- }
}
}
}
diff --git a/src/global_styling/mixins/_link.scss b/src/global_styling/mixins/_link.scss
index 16195e99aff..98dac59b9cc 100644
--- a/src/global_styling/mixins/_link.scss
+++ b/src/global_styling/mixins/_link.scss
@@ -7,6 +7,5 @@
&:focus {
text-decoration: underline;
- background: $euiFocusBackgroundColor;
}
}
diff --git a/src/themes/eui-amsterdam/global_styling/mixins/_link.scss b/src/themes/eui-amsterdam/global_styling/mixins/_link.scss
index 1a6b802276b..f925d9e0849 100644
--- a/src/themes/eui-amsterdam/global_styling/mixins/_link.scss
+++ b/src/themes/eui-amsterdam/global_styling/mixins/_link.scss
@@ -11,6 +11,5 @@
@include euiFocusRing(null, 'outer');
// sass-lint:disable-block no-misspelled-properties no-important
text-decoration-thickness: $euiBorderWidthThick !important;
- background: transparent !important;
}
}
diff --git a/src/themes/eui-amsterdam/overrides/_breadcrumbs.scss b/src/themes/eui-amsterdam/overrides/_breadcrumbs.scss
new file mode 100644
index 00000000000..83855df2919
--- /dev/null
+++ b/src/themes/eui-amsterdam/overrides/_breadcrumbs.scss
@@ -0,0 +1,12 @@
+
+// Inset outline otherwise the truncation cuts it off
+.euiBreadcrumb__content.euiLink:focus {
+ outline-offset: -1px;
+}
+
+// Fix all the font-weights to be consistent
+.euiBreadcrumb--last,
+.euiBreadcrumb__content,
+.euiBreadcrumbs__inPopover .euiBreadcrumb--last .euiBreadcrumb__content {
+ font-weight: $euiButtonFontWeight;
+}
diff --git a/src/themes/eui-amsterdam/overrides/_header.scss b/src/themes/eui-amsterdam/overrides/_header.scss
index 34232776c56..36529798494 100644
--- a/src/themes/eui-amsterdam/overrides/_header.scss
+++ b/src/themes/eui-amsterdam/overrides/_header.scss
@@ -28,82 +28,3 @@
.euiHeader--default + .euiHeader--default {
border-top: $euiBorderThin;
}
-
-// Breadcrumbs
-
-.euiHeaderBreadcrumbs {
- font-size: $euiFontSizeXS;
- line-height: $euiSize;
- margin-left: $euiSizeS;
- margin-right: $euiSizeS;
-
- // No separators
- .euiBreadcrumbSeparator {
- display: none !important; // sass-lint:disable-line no-important
- }
-
- // Only the header breadcrumbs get the new Amsterdam style so that there can
- // still be default text only breadcrumbs for places like EuiControlBar
- .euiBreadcrumb {
- @include euiButtonDefaultStyle($euiTextColor);
- line-height: $euiSize;
- font-weight: $euiFontWeightMedium;
- padding: $euiSizeXS $euiSize;
- clip-path: polygon(0 0, calc(100% - #{$euiSizeS}) 0, 100% 50%, calc(100% - #{$euiSizeS}) 100%, 0 100%, $euiSizeS 50%);
-
- &:focus {
- @include euiFocusRing(null, 'inner');
-
- &:focus-visible {
- // Turn radius and clip path off when in focus so the focus ring looks correct
- border-radius: 0;
- clip-path: none;
- }
- }
-
- // If it's a link the easiest way to detect is via our .euiLink class since it can accept either href or onClick
- // Also helps to add specificity for overriding hover state
- &.euiBreadcrumb--collapsed,
- &.euiLink {
- @include euiButtonDefaultStyle($euiColorPrimary);
-
- &:hover,
- &:focus {
- color: $euiColorPrimary;
- }
- }
-
- &.euiBreadcrumb--collapsed .euiLink {
- &,
- &:hover,
- &:focus {
- color: $euiColorPrimary;
- }
- }
-
- &:not(.euiBreadcrumb--last) {
- margin-right: -$euiSizeXS;
- }
-
- &:first-child {
- padding-left: $euiSizeM;
- border-radius: $euiBorderRadius 0 0 $euiBorderRadius;
- clip-path: polygon(0 0, calc(100% - #{$euiSizeS}) 0, 100% 50%, calc(100% - #{$euiSizeS}) 100%, 0 100%);
- }
- }
-
- .euiBreadcrumb--last {
- border-radius: 0 $euiBorderRadius $euiBorderRadius 0;
- padding-right: $euiSizeM;
- clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, #{$euiSizeS} 50%);
- }
-
- // In case the item is first AND last, aka only, just make it a fully rounded item
- .euiBreadcrumb:only-child {
- clip-path: none;
- padding-left: $euiSizeM;
- padding-right: $euiSizeM;
- border-radius: $euiBorderRadius;
- }
-}
-
diff --git a/src/themes/eui-amsterdam/overrides/_header_breadcrumbs.scss b/src/themes/eui-amsterdam/overrides/_header_breadcrumbs.scss
new file mode 100644
index 00000000000..6436711067f
--- /dev/null
+++ b/src/themes/eui-amsterdam/overrides/_header_breadcrumbs.scss
@@ -0,0 +1,71 @@
+// sass-lint:disable nesting-depth
+
+.euiHeaderBreadcrumbs {
+ line-height: $euiSize;
+ margin-left: $euiSizeS;
+ margin-right: $euiSizeS;
+
+ // Only the header breadcrumbs get the new Amsterdam style so that there can
+ // still be default text only breadcrumbs for places like EuiControlBar
+ .euiBreadcrumb__content {
+ @include euiButtonDefaultStyle($euiTextColor);
+ font-size: $euiFontSizeXS;
+ line-height: $euiSize;
+ font-weight: $euiFontWeightMedium;
+ padding: $euiSizeXS $euiSize;
+ clip-path: polygon(0 0, calc(100% - #{$euiSizeS}) 0, 100% 50%, calc(100% - #{$euiSizeS}) 100%, 0 100%, $euiSizeS 50%);
+
+ &.euiLink {
+ @include euiButtonDefaultStyle($euiColorPrimary);
+
+ &:focus {
+ @include euiFocusRing(null, 'inner');
+
+ // Turn clip path off and add full border-radius when in focus so the focus ring looks correct.
+ // This won't work in Safari, but that's ok
+ &:focus-visible {
+ border-radius: $euiBorderRadius;
+ clip-path: none;
+ }
+ }
+ }
+ }
+
+ .euiBreadcrumb {
+ // Remove separator
+ &::after {
+ display: none;
+ }
+
+ &:not(.euiBreadcrumb--last) {
+ margin-right: -$euiSizeXS;
+ }
+
+ &:first-child {
+ .euiBreadcrumb__content {
+ padding-left: $euiSizeM;
+ border-radius: $euiBorderRadius 0 0 $euiBorderRadius;
+ clip-path: polygon(0 0, calc(100% - #{$euiSizeS}) 0, 100% 50%, calc(100% - #{$euiSizeS}) 100%, 0 100%);
+ }
+ }
+
+ // In case the item is first AND last, aka only, just make it a fully rounded item.
+ // Needs to come after `:first-child` for specificity
+ &:only-child {
+ .euiBreadcrumb__content {
+ clip-path: none;
+ padding-left: $euiSizeM;
+ padding-right: $euiSizeM;
+ border-radius: $euiBorderRadius;
+ }
+ }
+ }
+
+ .euiBreadcrumb--last {
+ .euiBreadcrumb__content {
+ border-radius: 0 $euiBorderRadius $euiBorderRadius 0;
+ padding-right: $euiSizeM;
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, #{$euiSizeS} 50%);
+ }
+ }
+}
diff --git a/src/themes/eui-amsterdam/overrides/_index.scss b/src/themes/eui-amsterdam/overrides/_index.scss
index abf196c8909..a9ce099058e 100644
--- a/src/themes/eui-amsterdam/overrides/_index.scss
+++ b/src/themes/eui-amsterdam/overrides/_index.scss
@@ -1,4 +1,5 @@
@import 'avatar';
+@import 'breadcrumbs';
@import 'button';
@import 'button_empty';
@import 'button_group';
@@ -12,6 +13,7 @@
@import 'form_control_layout';
@import 'form_control_layout_delimited';
@import 'form_controls';
+@import 'header_breadcrumbs';
@import 'header';
@import 'hue';
@import 'list_group_item';