diff --git a/src-docs/src/views/timeline/timeline_example.js b/src-docs/src/views/timeline/timeline_example.js index 9674d3018b7..17c87166abd 100644 --- a/src-docs/src/views/timeline/timeline_example.js +++ b/src-docs/src/views/timeline/timeline_example.js @@ -89,10 +89,13 @@ export const TimelineExample = { iconType="accessibility" title={ <> - For accessibility, it is highly recommended to provide a - descriptive aria-label, or a text node ID of - an external label to the aria-labelledby prop - of the EuiTimeline. + For accessibility, it is required to wrap your{' '} + EuiTimelineItem's with a{' '} + EuiTimeline. It is highly recommended to + provide a descriptive aria-label, or a text + node ID of an external label to the{' '} + aria-labelledby prop of the{' '} + EuiTimeline. } /> diff --git a/src-docs/src/views/timeline/timeline_item.tsx b/src-docs/src/views/timeline/timeline_item.tsx index ba9870c6a27..20ec99b0fd1 100644 --- a/src-docs/src/views/timeline/timeline_item.tsx +++ b/src-docs/src/views/timeline/timeline_item.tsx @@ -1,13 +1,20 @@ import React from 'react'; -import { EuiTimelineItem, EuiText, EuiCode } from '../../../../src/components'; +import { + EuiTimeline, + EuiTimelineItem, + EuiText, + EuiCode, +} from '../../../../src/components'; export default () => ( - - -

- I'm the children and you can find the{' '} - icon on the left side. -

-
-
+ + + +

+ I'm the children and you can find the{' '} + icon on the left side. +

+
+
+
); diff --git a/src/components/timeline/__snapshots__/timeline.test.tsx.snap b/src/components/timeline/__snapshots__/timeline.test.tsx.snap index 66682e29b5b..77109522b75 100644 --- a/src/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/src/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -2,11 +2,11 @@ exports[`EuiTimeline is rendered with items 1`] = `
  1. +
    +
    + +
    +
    +
    +

    + + janet@elastic.co + + was invited to the project +

    +
    +
  2. +
+`; + +exports[`EuiTimeline props gutterSize l is rendered 1`] = ` +
    +
  1. +
    +
    + +
    +
    +
    +

    + + janet@elastic.co + + was invited to the project +

    +
    +
  2. +
  3. +
    +
    + +
    +
    +
    +

    + + janet@elastic.co + + was invited to the project +

    +
    +
  4. +
+`; + +exports[`EuiTimeline props gutterSize m is rendered 1`] = ` +
    +
  1. +
    +
    + +
    +
    +
    +

    + + janet@elastic.co + + was invited to the project +

    +
    +
  2. +
  3. +
    +
    + +
    +
    +
    +

    + + janet@elastic.co + + was invited to the project +

    +
    +
  4. +
+`; + +exports[`EuiTimeline props gutterSize xl is rendered 1`] = ` +
    +
  1. +
    +
    + +
    +
    +
    +

    + + janet@elastic.co + + was invited to the project +

    +
    +
  2. +
  3. { + return ` + gap: ${gutterSize}; + + // The vertical line height needs to be adjusted with the gutter size + [class*='euiTimelineItem-']:not(:last-child) > [class*='euiTimelineItemIcon-']::before { + ${logicalCSS('height', `calc(100% + ${gutterSize})`)}; + } + `; +}; + +// The vertical line should only appear when the EuiTimelineItem's +// are wrapped in a EuiTimeline. That's why these styles live here. +const timelineVerticalLine = (euiTheme: UseEuiTheme['euiTheme']) => { + return ` + [class*='euiTimelineItem-'] > [class*='euiTimelineItemIcon-']::before { + content: ''; + position: absolute; + ${logicalCSS('top', 0)}; + ${logicalCSS('width', euiTheme.size.xxs)}; + background-color: ${euiTheme.colors.lightShade}; + } + + > [class*='euiTimelineItem-center']:first-child > [class*='euiTimelineItemIcon-']::before { + ${logicalCSS('top', '50%')}; + } + + > [class*='euiTimelineItem-center']:last-child:not(:only-child) > [class*='euiTimelineItemIcon-']::before { + ${logicalCSS('height', '50%')}; + } + `; +}; + +export const euiTimelineStyles = ({ euiTheme }: UseEuiTheme) => { + return { + euiTimeline: css` + display: flex; + flex-direction: column; + ${timelineVerticalLine(euiTheme)} + `, + m: css(_gutterSizeAdjustment(euiTheme.size.base)), + l: css(_gutterSizeAdjustment(euiTheme.size.l)), + xl: css(_gutterSizeAdjustment(euiTheme.size.xl)), + }; +}; diff --git a/src/components/timeline/timeline.test.tsx b/src/components/timeline/timeline.test.tsx index 224b1dcb3d9..527ca4dd8ba 100644 --- a/src/components/timeline/timeline.test.tsx +++ b/src/components/timeline/timeline.test.tsx @@ -9,33 +9,47 @@ import React from 'react'; import { render } from 'enzyme'; import { EuiAvatar } from '../avatar'; -import { EuiTimeline, EuiTimelineProps } from './timeline'; +import { EuiTimeline, EuiTimelineProps, GUTTER_SIZES } from './timeline'; + +const items: EuiTimelineProps['items'] = [ + { + icon: , + verticalAlign: 'center', + children: ( +

    + janet@elastic.co was invited to the project +

    + ), + }, + { + icon: 'bolt', + verticalAlign: 'top', + children: ( +

    + janet@elastic.co was invited to the project +

    + ), + }, +]; describe('EuiTimeline', () => { test('is rendered with items', () => { - const items: EuiTimelineProps['items'] = [ - { - icon: , - verticalAlign: 'center', - children: ( -

    - janet@elastic.co was invited to the project -

    - ), - }, - { - icon: 'bolt', - verticalAlign: 'top', - children: ( -

    - janet@elastic.co was invited to the project -

    - ), - }, - ]; - const component = render(); expect(component).toMatchSnapshot(); }); + + describe('props', () => { + describe('gutterSize', () => { + GUTTER_SIZES.forEach((gutterSize) => { + test(`${gutterSize} is rendered`, () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); + }); }); diff --git a/src/components/timeline/timeline.tsx b/src/components/timeline/timeline.tsx index 6b58d67bac6..3c56796bd88 100644 --- a/src/components/timeline/timeline.tsx +++ b/src/components/timeline/timeline.tsx @@ -9,8 +9,14 @@ import React, { HTMLAttributes, FunctionComponent } from 'react'; import { CommonProps } from '../common'; import classNames from 'classnames'; +import { useEuiTheme } from '../../services'; import { EuiTimelineItem, EuiTimelineItemProps } from './timeline_item'; +import { euiTimelineStyles } from './timeline.styles'; + +export const GUTTER_SIZES = ['m', 'l', 'xl'] as const; +export type EuiTimelineGutterSize = typeof GUTTER_SIZES[number]; + export interface EuiTimelineProps extends HTMLAttributes, CommonProps { @@ -18,19 +24,29 @@ export interface EuiTimelineProps * List of timeline items to render. See #EuiTimelineItem */ items?: EuiTimelineItemProps[]; + /** + * Sets the size of the vertical space between each timeline item + */ + gutterSize?: EuiTimelineGutterSize; } export const EuiTimeline: FunctionComponent = ({ className, items = [], children, + gutterSize = 'xl', ...rest }) => { const classes = classNames('euiTimeline', className); + const euiTheme = useEuiTheme(); + const styles = euiTimelineStyles(euiTheme); + + const cssStyles = [styles.euiTimeline, styles[gutterSize]]; + return ( // eslint-disable-next-line jsx-a11y/no-redundant-roles -
      +
        {items.map((item, index) => ( ))} diff --git a/src/components/timeline/timeline_item.styles.ts b/src/components/timeline/timeline_item.styles.ts index a722beeda27..2c8f05dc486 100644 --- a/src/components/timeline/timeline_item.styles.ts +++ b/src/components/timeline/timeline_item.styles.ts @@ -7,33 +7,16 @@ */ import { css } from '@emotion/react'; -import { UseEuiTheme } from '../../services'; -export const euiTimelineItemStyles = ({ euiTheme }: UseEuiTheme) => ({ - euiTimelineItem: css` - display: flex; - - &:not(:last-of-type) { - padding-bottom: ${euiTheme.size.xl}; - } - - &:first-of-type { - > [class*='euiTimelineItemIcon-center']::before { - top: 50%; - // Adding to the height the padding bottom from the parent container - height: calc(50% + ${euiTheme.size.xl}); - } - } - - &:last-of-type { - > [class*='euiTimelineItemIcon']::before { - display: none; - } - - &:not(:only-child) > [class*='euiTimelineItemIcon-center']::before { - top: 0; - height: 50%; - } - } - `, -}); +export const euiTimelineItemStyles = () => { + return { + euiTimelineItem: css` + display: flex; + position: relative; + `, + // Vertical alignments + // These classes are being targeted in timeline.styles.ts + top: css``, + center: css``, + }; +}; diff --git a/src/components/timeline/timeline_item.tsx b/src/components/timeline/timeline_item.tsx index 33db6219525..25877f22299 100644 --- a/src/components/timeline/timeline_item.tsx +++ b/src/components/timeline/timeline_item.tsx @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import React, { FunctionComponent, HTMLAttributes, ElementType } from 'react'; +import React, { FunctionComponent, HTMLAttributes } from 'react'; import { CommonProps } from '../common'; -import { useEuiTheme } from '../../services'; import { EuiTimelineItemEvent, @@ -19,12 +18,11 @@ import { EuiTimelineItemIconProps, } from './timeline_item_icon'; import { euiTimelineItemStyles } from './timeline_item.styles'; - export const VERTICAL_ALIGN = ['top', 'center'] as const; export type EuiTimelineItemVerticalAlign = typeof VERTICAL_ALIGN[number]; export interface EuiTimelineItemProps - extends Omit, 'children'>, + extends Omit, 'children'>, CommonProps, Omit, Omit { @@ -32,12 +30,6 @@ export interface EuiTimelineItemProps * Vertical alignment of the event with the icon */ verticalAlign?: EuiTimelineItemVerticalAlign; - /** - * Sets the HTML element for `EuiTimelineItem`. - * By default, the element renders as a `
      1. `. - * Only change the HTML element when it is not wrapped in a `EuiTimeline` that renders as a `
          `. - */ - component?: ElementType; } export const EuiTimelineItem: FunctionComponent = ({ @@ -46,18 +38,13 @@ export const EuiTimelineItem: FunctionComponent = ({ icon, iconAriaLabel, className, - component = 'li', ...rest }) => { - const euiTheme = useEuiTheme(); - const styles = euiTimelineItemStyles(euiTheme); - - const cssStyles = [styles.euiTimelineItem]; - - const Element = component; + const styles = euiTimelineItemStyles(); + const cssStyles = [styles.euiTimelineItem, styles[verticalAlign]]; return ( - +
        1. = ({ {children} - +
        2. ); }; diff --git a/src/components/timeline/timeline_item_icon.styles.ts b/src/components/timeline/timeline_item_icon.styles.ts index 63531516b26..9fb281ac6d3 100644 --- a/src/components/timeline/timeline_item_icon.styles.ts +++ b/src/components/timeline/timeline_item_icon.styles.ts @@ -8,28 +8,17 @@ import { css } from '@emotion/react'; import { UseEuiTheme } from '../../services'; +import { logicalCSS } from '../../global_styling'; export const euiTimelineItemIconStyles = ({ euiTheme }: UseEuiTheme) => ({ euiTimelineItemIcon: css` display: flex; position: relative; flex-grow: 0; - margin-right: ${euiTheme.size.base}; - - // timeline vertical line - &::before { - content: ''; - position: absolute; - top: 0; - left: calc(${euiTheme.size.xxl} / 2); - width: calc(${euiTheme.size.xs} / 2); - // Adding to the height the padding bottom from the parent container - height: calc(100% + ${euiTheme.size.xl}); - background-color: ${euiTheme.colors.lightShade}; - } + justify-content: center; + ${logicalCSS('margin-right', euiTheme.size.base)}; `, euiTimelineItemIcon__content: css` - min-width: ${euiTheme.size.xxl}; display: flex; justify-content: center; align-items: center; diff --git a/src/global_styling/functions/__snapshots__/logicals.test.ts.snap b/src/global_styling/functions/__snapshots__/logicals.test.ts.snap index 4bf7be6eedd..5cb2bec1b29 100644 --- a/src/global_styling/functions/__snapshots__/logicals.test.ts.snap +++ b/src/global_styling/functions/__snapshots__/logicals.test.ts.snap @@ -64,7 +64,7 @@ exports[`logicalCSS mixin returns a string property for each directional propert exports[`logicalCSS mixin returns a string property for each directional property: inset 1`] = `"inset: 8px;"`; -exports[`logicalCSS mixin returns a string property for each directional property: left 1`] = `"inset-inline-end: 8px;"`; +exports[`logicalCSS mixin returns a string property for each directional property: left 1`] = `"inset-inline-start: 8px;"`; exports[`logicalCSS mixin returns a string property for each directional property: margin-bottom 1`] = `"margin-block-end: 8px;"`; @@ -102,7 +102,7 @@ exports[`logicalCSS mixin returns a string property for each directional propert exports[`logicalCSS mixin returns a string property for each directional property: padding-vertical 1`] = `"padding-block: 8px;"`; -exports[`logicalCSS mixin returns a string property for each directional property: right 1`] = `"inset-inline-start: 8px;"`; +exports[`logicalCSS mixin returns a string property for each directional property: right 1`] = `"inset-inline-end: 8px;"`; exports[`logicalCSS mixin returns a string property for each directional property: top 1`] = `"inset-block-start: 8px;"`; @@ -304,7 +304,7 @@ Object { exports[`logicalStyle mixin returns an object property for each directional property: left 1`] = ` Object { - "insetInlineEnd": "8px", + "insetInlineStart": "8px", } `; @@ -418,7 +418,7 @@ Object { exports[`logicalStyle mixin returns an object property for each directional property: right 1`] = ` Object { - "insetInlineStart": "8px", + "insetInlineEnd": "8px", } `; diff --git a/src/global_styling/functions/logicals.ts b/src/global_styling/functions/logicals.ts index 8547425bac4..ae0b39d1fd4 100644 --- a/src/global_styling/functions/logicals.ts +++ b/src/global_styling/functions/logicals.ts @@ -47,9 +47,9 @@ const logicalPaddings = { const logicalPosition = { top: 'inset-block-start', - right: 'inset-inline-start', + right: 'inset-inline-end', bottom: 'inset-block-end', - left: 'inset-inline-end', + left: 'inset-inline-start', horizontal: 'inset-block', vertical: 'inset-inline', inset: 'inset', diff --git a/upcoming_changelogs/5955.md b/upcoming_changelogs/5955.md new file mode 100644 index 00000000000..f25287e1dc0 --- /dev/null +++ b/upcoming_changelogs/5955.md @@ -0,0 +1,10 @@ +- Added `gutterSize` prop to `EuiTimeline` + +**Bug fixes** + +- Fixed bug in `EuiTimelineItem` where the vertical line was not showing on the last item when `verticalAlign` was set to `center` +- Fixed bug in `logicalCSS()` where the left and right `logicalPosition`s were wrong + +**Breaking changes** + +- Removed `component` prop from `EuiTimelineItem`, which now defaults to `li`. Consequently, a `EuiTimeline` (`ol`) is required to wrap the timeline items \ No newline at end of file