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/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap index e94cccfdafc..321498fc66e 100644 --- a/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -6,41 +6,52 @@ exports[`EuiHeaderBreadcrumbs is rendered 1`] = ` class="euiBreadcrumbs euiHeaderBreadcrumbs testClass1 testClass2 euiBreadcrumbs--truncate" data-test-subj="test subject string" > - - Animals - -
    - -
    - - Boa constrictor - -
    - - Edit - +
  • + + Animals + +
  • +
  • + +
  • +
  • + + Boa constrictor + +
  • +
  • + + Edit + +
  • + `; 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';