diff --git a/src-docs/src/views/image/float.js b/src-docs/src/views/image/float.tsx
similarity index 67%
rename from src-docs/src/views/image/float.js
rename to src-docs/src/views/image/float.tsx
index cc29381f5a5..2cbee58269c 100644
--- a/src-docs/src/views/image/float.js
+++ b/src-docs/src/views/image/float.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { EuiImage, EuiText } from '../../../../src/components';
+// @ts-ignore faker has no Typescript defs
import { fake } from 'faker';
export default () => (
@@ -10,9 +11,9 @@ export default () => (
float="right"
margin="l"
hasShadow
- caption="Random nature image"
+ caption="A randomized image"
allowFullScreen
- alt="Random nature image"
+ alt="" // Because the image is randomized, there is no meaningful alt text we can generate here.
src="https://picsum.photos/800/500"
/>
{fake('{{lorem.paragraphs}}')}
@@ -24,8 +25,8 @@ export default () => (
margin="l"
hasShadow
allowFullScreen
- caption="Another random image"
- alt="Random nature image"
+ caption="Another randomized image"
+ alt="" // Because the image is randomized, there is no meaningful alt text we can generate here.
src="https://picsum.photos/300/300"
/>
{fake('{{lorem.paragraphs}}')}
diff --git a/src-docs/src/views/image/image.js b/src-docs/src/views/image/image.js
deleted file mode 100644
index a4a1820fb15..00000000000
--- a/src-docs/src/views/image/image.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-
-import { EuiImage } from '../../../../src/components';
-
-export default () => (
-
-);
diff --git a/src-docs/src/views/image/image.tsx b/src-docs/src/views/image/image.tsx
new file mode 100644
index 00000000000..6ef1d5ed428
--- /dev/null
+++ b/src-docs/src/views/image/image.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { EuiImage } from '../../../../src/components';
+
+export default () => (
+
+ Mastigias papua , also known as spotted jelly
+
+ }
+ alt="Many small white-spotted pink jellyfish floating in a dark aquarium"
+ src="https://images.unsplash.com/photo-1650253618249-fb0d32d3865c?w=900&h=900&fit=crop&q=60"
+ />
+);
diff --git a/src-docs/src/views/image/image_example.js b/src-docs/src/views/image/image_example.js
index c2aeba36dae..20651afee76 100644
--- a/src-docs/src/views/image/image_example.js
+++ b/src-docs/src/views/image/image_example.js
@@ -2,7 +2,12 @@ import React, { Fragment } from 'react';
import { GuideSectionTypes } from '../../components';
-import { EuiCode, EuiCallOut, EuiImage } from '../../../../src/components';
+import {
+ EuiCode,
+ EuiCallOut,
+ EuiLink,
+ EuiImage,
+} from '../../../../src/components';
EuiImage.__docgenInfo.props.src.required = true;
import imageConfig from './playground';
@@ -10,8 +15,8 @@ import imageConfig from './playground';
import Image from './image';
const imageSource = require('!!raw-loader!./image');
const imageSnippet = `
`;
@@ -19,8 +24,8 @@ import ImageSizes from './image_size';
const imageSizesSource = require('!!raw-loader!./image_size');
const imageSizesSnippet = `
`;
@@ -28,16 +33,16 @@ import ImageZoom from './image_zoom';
const imageZoomSource = require('!!raw-loader!./image_zoom');
const imageZoomSnippet = `
`;
import ImageFloat from './float';
const imageFloatSource = require('!!raw-loader!./float');
const imageFloatSnippet = `
@@ -54,10 +59,36 @@ export const ImageExample = {
},
],
text: (
-
- Use EuiImage when you need to place a static image
- into a page with an optional caption.
-
+ <>
+
+ Use EuiImage when you need to place a static image
+ into a page with an optional caption.
+
+
+
+ This page has several examples of alt text written to aid screen
+ reader users, as well as several examples of when not to
+ include alt text. When an image is decorative, or if the image is
+ adequately described by surrounding text, it is better to pass an
+ empty {'""'} string instead.
+
+
+ When an image is not already sufficiently described, the alt text
+ passed should help non-visual users understand the purpose of the
+ image within the context of the overall page. See{' '}
+
+ WebAIM
+ {' '}
+ for a more detailed guide to writing effective alt text.
+
+
+ >
),
props: { EuiImage },
demo: ,
diff --git a/src-docs/src/views/image/image_size.js b/src-docs/src/views/image/image_size.tsx
similarity index 57%
rename from src-docs/src/views/image/image_size.js
rename to src-docs/src/views/image/image_size.tsx
index 1ff44417c44..262f16378da 100644
--- a/src-docs/src/views/image/image_size.js
+++ b/src-docs/src/views/image/image_size.tsx
@@ -2,6 +2,11 @@ import React from 'react';
import { EuiImage, EuiSpacer } from '../../../../src/components';
+const src =
+ 'https://images.unsplash.com/photo-1477747219299-60f95c811fef?w=1000&h=1000&fit=crop&q=60';
+const alt =
+ 'A cozy breakfast scene. In the background is a plate of waffles and blueberries. In the middle ground is a glass of orange juice and a small cup of cream. In the foreground is a plate of Eggs Benedict with a side of salad and cherry tomatoes.';
+
export default () => (
(
allowFullScreen
size={50}
caption="Custom size (50)"
- alt="Accessible image alt goes here"
- src="https://source.unsplash.com/1000x1000/?Nature"
+ alt={alt}
+ src={src}
+ wrapperProps={{ className: 'eui-textLeft' }}
/>
(
hasShadow
allowFullScreen
caption="Small"
- alt="Accessible image alt goes here"
- src="https://source.unsplash.com/1000x1000/?Nature"
+ alt={alt}
+ src={src}
/>
(
hasShadow
allowFullScreen
caption="Medium"
- alt="Accessible image alt goes here"
- src="https://source.unsplash.com/1000x1000/?Nature"
+ alt={alt}
+ src={src}
/>
(
hasShadow
allowFullScreen
caption="Large"
- alt="Accessible image alt goes here"
- src="https://source.unsplash.com/1000x1000/?Nature"
+ alt={alt}
+ src={src}
/>
(
hasShadow
allowFullScreen
caption="Extra large"
- alt="Accessible image alt goes here"
- src="https://source.unsplash.com/1000x1000/?Nature"
+ alt={alt}
+ src={src}
/>
(
allowFullScreen
size="fullWidth"
caption="Full width"
- alt="Accessible image alt goes here"
- src="https://source.unsplash.com/1000x1000/?Nature"
+ alt={alt}
+ src={src}
/>
);
diff --git a/src-docs/src/views/image/image_zoom.js b/src-docs/src/views/image/image_zoom.js
deleted file mode 100644
index 5fea5d1f745..00000000000
--- a/src-docs/src/views/image/image_zoom.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-
-import {
- EuiImage,
- EuiFlexGroup,
- EuiFlexItem,
-} from '../../../../src/components';
-
-export default () => (
-
-
-
-
-
-
-
-
-);
diff --git a/src-docs/src/views/image/image_zoom.tsx b/src-docs/src/views/image/image_zoom.tsx
new file mode 100644
index 00000000000..c50099982ad
--- /dev/null
+++ b/src-docs/src/views/image/image_zoom.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+
+import {
+ EuiImage,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+} from '../../../../src/components';
+
+export default () => (
+
+
+
+
+
+
+ Browser usage on{' '}
+
+ Wikimedia (CC BY 3.0)
+
+ >
+ }
+ alt="Pie chart describing browser usage on Wikimedia on October 2011. Internet Explorer occupies 34 percent, Firefox occupies 23 percent, Chrome occupies 20 percent, Safari occupies 11 percent, Opera occupies 5%, Android occupies 1.9 percent, and other browsers occupy 3.5 percent."
+ src="https://upload.wikimedia.org/wikipedia/commons/a/a2/Wikimedia_browser_share_pie_chart.png"
+ fullScreenIconColor="dark"
+ size={300}
+ style={{ padding: '20px 30px', background: 'white' }}
+ />
+
+
+);
diff --git a/src/components/image/__snapshots__/image.test.tsx.snap b/src/components/image/__snapshots__/image.test.tsx.snap
index f62eaa00b62..0f72150156c 100644
--- a/src/components/image/__snapshots__/image.test.tsx.snap
+++ b/src/components/image/__snapshots__/image.test.tsx.snap
@@ -2,108 +2,252 @@
exports[`EuiImage is rendered 1`] = `
+
`;
-exports[`EuiImage is rendered and allows fullscreen 1`] = `
+exports[`EuiImage props allowFullScreen 1`] = `
-
+
+
`;
-exports[`EuiImage is rendered with a float 1`] = `
+exports[`EuiImage props caption 1`] = `
+
+
+ caption
+
+
`;
-exports[`EuiImage is rendered with a margin 1`] = `
+exports[`EuiImage props float 1`] = `
+
+
+`;
+
+exports[`EuiImage props fullScreenIconColor 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`EuiImage props hasShadow 1`] = `
+
+
+
+
+
+
`;
-exports[`EuiImage is rendered with a node as the caption 1`] = `
+exports[`EuiImage props margin 1`] = `
-
- caption
-
-
+ class="emotion-euiImageCaption"
+ />
`;
-exports[`EuiImage is rendered with custom size 1`] = `
+exports[`EuiImage props size 1`] = `
+
+
+`;
+
+exports[`EuiImage props src vs url src 1`] = `
+
+
+
`;
-exports[`EuiImage is rendered with src 1`] = `
+exports[`EuiImage props src vs url url 1`] = `
+
+
+
+`;
+
+exports[`EuiImage props wrapperProps 1`] = `
+
+
+
+ caption
+
+
`;
diff --git a/src/components/image/_image.scss b/src/components/image/_image.scss
deleted file mode 100644
index 94489b5397b..00000000000
--- a/src/components/image/_image.scss
+++ /dev/null
@@ -1,212 +0,0 @@
-/**
- * 1. Fix for IE where the image correctly resizes in width but doesn't collapse its height
- (https://github.com/philipwalton/flexbugs/issues/75#issuecomment-134702421)
- */
-
-// Main that wraps images.
-.euiImage {
- display: inline-block;
- max-width: 100%;
- position: relative;
- min-height: 1px; /* 1 */
- line-height: 0; // Fixes cropping when image is resized by forcing its height to be determined by the image not line-height
- flex-shrink: 0; // Don't ever let this shrink in height if direct descendent of flex
-
- // Required for common usage of nesting within EuiText
- .euiImage__img {
- margin-bottom: 0;
- max-width: 100%;
- }
-
- &.euiImage--hasShadow {
- .euiImage__img {
- @include euiBottomShadowMedium;
- }
- }
-
- .euiImage__button {
- position: relative;
- cursor: pointer;
-
- // transition the shadow
- transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
-
- &:focus {
- outline: 2px solid $euiFocusRingColor;
- }
-
- &:hover .euiImage__icon {
- visibility: visible;
- fill-opacity: 1;
- }
-
- &--fullWidth {
- width: 100%;
- }
- }
-
- &.euiImage--allowFullScreen {
- &:hover .euiImage__caption {
- text-decoration: underline;
- }
-
- &:not(.euiImage--hasShadow) .euiImage__button:hover,
- &:not(.euiImage--hasShadow) .euiImage__button:focus {
- @include euiBottomShadowMedium;
- }
-
- &.euiImage--hasShadow .euiImage__button:hover,
- &.euiImage--hasShadow .euiImage__button:focus {
- @include euiBottomShadow;
- }
- }
-
- // These sizes are mostly suggestions. Don't look too hard for meaning in their values.
- // Size is applied to the image, rather than the figure to work better with floats
- &.euiImage--small .euiImage__img {
- width: convertToRem(120px);
- }
-
- &.euiImage--medium .euiImage__img {
- width: convertToRem(200px);
- }
-
- &.euiImage--large .euiImage__img {
- width: convertToRem(360px);
- }
-
- &.euiImage--xlarge .euiImage__img {
- width: convertToRem(600px);
- }
-
- &.euiImage--fullWidth {
- width: 100%;
- }
-
- &.euiImage--original {
- .euiImage__img {
- width: auto;
- max-width: 100%;
- }
- }
-
- &.euiImage--floatLeft {
- float: left;
-
- &[class*='euiImage--margin'] {
- margin-left: 0;
- margin-top: 0;
- }
- }
-
- &.euiImage--floatRight {
- float: right;
-
- &[class*='euiImage--margin'] {
- margin-right: 0;
- margin-top: 0;
- }
- }
-
- &.euiImage--marginSmall {
- margin: $euiSizeS;
- }
-
- &.euiImage--marginMedium {
- margin: $euiSize;
- }
-
- &.euiImage--marginLarge {
- margin: $euiSizeL;
- }
-
- &.euiImage--marginXlarge {
- margin: $euiSizeXL;
- }
-}
-
-// The image itself is full width within the container.
-.euiImage__img {
- width: 100%;
- vertical-align: middle;
-}
-
-.euiImage__caption {
- @include euiFontSizeS;
- margin-top: $euiSizeXS;
- text-align: center;
-}
-
-.euiImage__icon {
- visibility: hidden;
- fill-opacity: 0;
- position: absolute;
- right: $euiSize;
- top: $euiSize;
- transition: fill-opacity $euiAnimSpeedSlow $euiAnimSlightResistance;
- cursor: pointer;
-}
-
-// The FullScreen image that optionally pops up on click.
-.euiImage-isFullScreen {
- position: relative;
- max-height: 80vh;
- max-width: 80vw;
- animation: euiImageFullScreen $euiAnimSpeedExtraSlow $euiAnimSlightBounce;
-
- &:hover {
- .euiImage__button {
- @include euiBottomShadow;
- }
-
- .euiImage__caption {
- text-decoration: underline;
- }
- }
-
- &__img {
- max-height: 80vh;
- max-width: 80vw;
- vertical-align: middle;
- cursor: pointer;
- transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
- }
-}
-
-.euiImage-isFullScreenCloseIcon {
- position: absolute;
- right: $euiSize;
- top: $euiSize;
- pointer-events: none;
-}
-
-@keyframes euiImageFullScreen {
- 0% {
- opacity: 0;
- transform: translateY($euiSizeXL * 2);
- }
-
- 100% {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@include euiBreakpoint('xs', 's', 'm') {
-
- .euiImage {
-
- &.euiImage--floatLeft,
- &.euiImage--floatRight {
- float: none;
-
- // Return back to whatever margin settings were set without the float
- &[class*='euiImage--margin'] {
- margin-top: inherit;
- margin-right: inherit;
- margin-bottom: inherit;
- margin-left: inherit;
- }
- }
- }
-}
diff --git a/src/components/image/_index.scss b/src/components/image/_index.scss
deleted file mode 100644
index eb326aae4dd..00000000000
--- a/src/components/image/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import 'image';
diff --git a/src/components/image/image.styles.ts b/src/components/image/image.styles.ts
new file mode 100644
index 00000000000..8bff84141b8
--- /dev/null
+++ b/src/components/image/image.styles.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 { css } from '@emotion/react';
+import { logicalCSS } from '../../global_styling';
+import { UseEuiTheme } from '../../services';
+import { euiShadow } from '../../themes/amsterdam/global_styling/mixins';
+
+export const euiImageStyles = (euiThemeContext: UseEuiTheme) => ({
+ euiImage: css`
+ vertical-align: middle;
+ ${logicalCSS('max-width', '100%')};
+
+ &,
+ // Required for common usage of nesting within EuiText
+ [class*='euiText'] & {
+ ${logicalCSS('margin-bottom', 0)};
+ }
+ `,
+ // Variations
+ isFullScreen: css`
+ position: relative;
+ ${logicalCSS('max-height', '80vh')};
+ ${logicalCSS('max-width', '80vw')};
+ `,
+ hasShadow: css`
+ ${euiShadow(euiThemeContext, 's')};
+ `,
+ // Sizes
+ // These sizes are mostly suggestions. Don't look too hard for meaning in their values.
+ // Size is applied to the image, rather than the wrapper figure to work better with floats
+ s: css`
+ ${logicalCSS('width', '100px')}
+ `,
+ m: css`
+ ${logicalCSS('width', '200px')}
+ `,
+ l: css`
+ ${logicalCSS('width', '360px')}
+ `,
+ xl: css`
+ ${logicalCSS('width', '600px')}
+ `,
+ original: css`
+ ${logicalCSS('width', 'auto')}
+ `,
+ fullWidth: css`
+ ${logicalCSS('width', '100%')}
+ `,
+ customSize: css`
+ // A custom max-width and max-height is set in the style tag
+ // We set the width back to auto to ensure aspect ratio is kept
+ ${logicalCSS('width', 'auto')}
+ `,
+});
diff --git a/src/components/image/image.test.tsx b/src/components/image/image.test.tsx
index c191006954a..5853d05bb4d 100644
--- a/src/components/image/image.test.tsx
+++ b/src/components/image/image.test.tsx
@@ -8,87 +8,124 @@
import React from 'react';
import { render, mount, ReactWrapper } from 'enzyme';
-import { requiredProps, findTestSubject } from '../../test';
+import { requiredProps as commonProps, findTestSubject } from '../../test';
import { act } from 'react-dom/test-utils';
import { keys } from '../../services';
+import { shouldRenderCustomStyles } from '../../test/internal';
import { EuiImage } from './image';
describe('EuiImage', () => {
- test('is rendered', () => {
- const component = render(
-
- );
-
- expect(component).toMatchSnapshot();
- });
+ const requiredProps = {
+ ...commonProps,
+ alt: '',
+ src: '/cat.jpg',
+ };
- test('is rendered and allows fullscreen', () => {
- const component = render(
-
- );
+ shouldRenderCustomStyles( );
+ test('is rendered', () => {
+ const component = render( );
expect(component).toMatchSnapshot();
});
- test('is rendered with src', () => {
- const component = render(
-
- );
+ describe('props', () => {
+ describe('src vs url', () => {
+ test('src', () => {
+ const component = render( );
+ expect(component).toMatchSnapshot();
+ });
- expect(component).toMatchSnapshot();
- });
+ test('url', () => {
+ const component = render( );
+ expect(component).toMatchSnapshot();
+ });
- test('is rendered with a float', () => {
- const component = render(
-
- );
+ it('picks src over url when both are present (and throws a typescript error)', () => {
+ const component = render(
+ // @ts-expect-error - 'types of property url are incompatible'
+
+ );
+ expect(component.find('img').attr('src')).toEqual('/dog.jpg');
+ });
+ });
- expect(component).toMatchSnapshot();
- });
+ test('float', () => {
+ const component = render( );
+ expect(component).toMatchSnapshot();
+ });
- test('is rendered with a margin', () => {
- const component = render( );
+ test('margin', () => {
+ const component = render( );
+ expect(component).toMatchSnapshot();
+ });
- expect(component).toMatchSnapshot();
- });
+ test('size', () => {
+ const component = render( );
+ expect(component).toMatchSnapshot();
+ });
- test('is rendered with custom size', () => {
- const component = render( );
+ test('caption', () => {
+ const component = render(
+ caption} />
+ );
+ expect(component).toMatchSnapshot();
+ });
- expect(component).toMatchSnapshot();
- });
+ test('allowFullScreen', () => {
+ const component = render( );
+ expect(component).toMatchSnapshot();
+ });
- test('is rendered with a node as the caption', () => {
- const component = render(
- caption} url="/cat.jpg" />
- );
+ test('fullScreenIconColor', () => {
+ const component = render(
+
+ );
+ expect(component).toMatchSnapshot();
+ });
- expect(component).toMatchSnapshot();
+ test('hasShadow', () => {
+ const component = render(
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ test('wrapperProps', () => {
+ const component = render(
+ caption}
+ url="/cat.jpg"
+ wrapperProps={{
+ ...requiredProps,
+ style: { border: '2px solid red' },
+ }}
+ />
+ );
+ expect(component).toMatchSnapshot();
+ });
});
- describe('Fullscreen behaviour', () => {
+ describe('fullscreen behaviour', () => {
let component: ReactWrapper;
beforeAll(() => {
- const testProps = {
- ...requiredProps,
- 'data-test-subj': 'euiImage',
- };
-
component = mount(
);
});
@@ -103,9 +140,7 @@ describe('EuiImage', () => {
);
expect(overlayMask.length).toBe(1);
- const fullScreenImage = overlayMask[0].querySelectorAll(
- '[data-test-subj=euiImage]'
- );
+ const fullScreenImage = overlayMask[0].querySelectorAll('figure img');
expect(fullScreenImage.length).toBe(1);
});
@@ -126,25 +161,34 @@ describe('EuiImage', () => {
});
test('close using ESCAPE key', () => {
- const deactivateFullScreenBtn = document.querySelectorAll(
+ const deactivateFullScreenBtn = document.querySelector(
'[data-test-subj=deactivateFullScreenButton]'
);
- expect(deactivateFullScreenBtn.length).toBe(1);
+ expect(deactivateFullScreenBtn).toBeTruthy();
+
+ // Ignores non-escape keys
+ act(() => {
+ const escapeKeydownEvent = new KeyboardEvent('keydown', {
+ key: keys.TAB,
+ bubbles: true,
+ });
+ deactivateFullScreenBtn!.dispatchEvent(escapeKeydownEvent);
+ });
+ expect(deactivateFullScreenBtn).toBeTruthy();
+ // Removes full screen overlay on escape key
act(() => {
const escapeKeydownEvent = new KeyboardEvent('keydown', {
key: keys.ESCAPE,
bubbles: true,
});
- (deactivateFullScreenBtn[0] as HTMLElement).dispatchEvent(
- escapeKeydownEvent
- );
+ deactivateFullScreenBtn!.dispatchEvent(escapeKeydownEvent);
});
- const overlayMask = document.querySelectorAll(
+ const overlayMask = document.querySelector(
'[data-test-subj=fullScreenOverlayMask]'
);
- expect(overlayMask.length).toBe(0);
+ expect(overlayMask).toBeFalsy();
});
test('close using overlay mask', () => {
diff --git a/src/components/image/image.tsx b/src/components/image/image.tsx
index 345b0c732a0..b4f9f739669 100644
--- a/src/components/image/image.tsx
+++ b/src/components/image/image.tsx
@@ -6,280 +6,106 @@
* Side Public License, v 1.
*/
-import React, {
- FunctionComponent,
- ImgHTMLAttributes,
- useState,
- ReactNode,
-} from 'react';
+import React, { FunctionComponent, useState } from 'react';
import classNames from 'classnames';
-import { CommonProps, ExclusiveUnion } from '../common';
-import { EuiOverlayMask } from '../overlay_mask';
+import { useEuiTheme } from '../../services';
-import { EuiIcon } from '../icon';
+import { EuiImageWrapper } from './image_wrapper';
+import { euiImageStyles } from './image.styles';
+import { EuiImageFullScreenWrapper } from './image_fullscreen_wrapper';
+import type { EuiImageProps, EuiImageSize } from './image_types';
-import { useEuiI18n } from '../i18n';
-
-import { EuiFocusTrap } from '../focus_trap';
-
-import { keys } from '../../services';
-import { useInnerText } from '../inner_text';
-
-type ImageSize = 's' | 'm' | 'l' | 'xl' | 'fullWidth' | 'original';
-type Floats = 'left' | 'right';
-type Margins = 's' | 'm' | 'l' | 'xl';
-
-const sizeToClassNameMap: { [size in ImageSize]: string } = {
- s: 'euiImage--small',
- m: 'euiImage--medium',
- l: 'euiImage--large',
- xl: 'euiImage--xlarge',
- fullWidth: 'euiImage--fullWidth',
- original: 'euiImage--original',
-};
-
-const marginToClassNameMap: { [margin in Margins]: string } = {
- s: 'euiImage--marginSmall',
- m: 'euiImage--marginMedium',
- l: 'euiImage--marginLarge',
- xl: 'euiImage--marginXlarge',
-};
-
-const floatToClassNameMap: { [float in Floats]: string } = {
- left: 'euiImage--floatLeft',
- right: 'euiImage--floatRight',
-};
-
-export const SIZES = Object.keys(sizeToClassNameMap);
-
-type FullScreenIconColor = 'light' | 'dark';
-
-const fullScreenIconColorMap: { [color in FullScreenIconColor]: string } = {
- light: 'ghost',
- dark: 'default',
-};
-
-type _EuiImageSrcOrUrl = ExclusiveUnion<
- {
- /**
- * Requires either `src` or `url` but defaults to using `src` if both are provided
- */
- src: string;
- },
- {
- url: string;
- }
->;
-
-export type EuiImageProps = CommonProps &
- _EuiImageSrcOrUrl &
- Omit, 'src' | 'alt'> & {
- /**
- * Separate from the caption is a title on the alt tag itself.
- * This one is required for accessibility.
- */
- alt: string;
- /**
- * Accepts `s` / `m` / `l` / `xl` / `original` / `fullWidth` / or a CSS size of `number` or `string`.
- * `fullWidth` will set the figure to stretch to 100% of its container.
- * `string` and `number` types will max both the width or height, whichever is greater.
- */
- size?: ImageSize | number | string;
- /**
- * Changes the color of the icon that floats above the image when it can be clicked to fullscreen.
- * The default value of `light` is fine unless your image has a white background, in which case you should change it to `dark`.
- */
- fullScreenIconColor?: FullScreenIconColor;
- /**
- * Provides the visible caption to the image
- */
- caption?: ReactNode;
- /**
- * When set to `true` (default) will apply a slight shadow to the image
- */
- hasShadow?: boolean;
- /**
- * When set to `true` will make the image clickable to a larger version
- */
- allowFullScreen?: boolean;
- /**
- * Float the image to the left or right. Useful in large text blocks.
- */
- float?: Floats;
- /**
- * Margin around the image.
- */
- margin?: Margins;
- };
+import { SIZES } from './image_types';
export const EuiImage: FunctionComponent = ({
className,
+ alt,
url,
src,
size = 'original',
- caption,
hasShadow,
- allowFullScreen,
- fullScreenIconColor = 'light',
- alt,
style,
+ wrapperProps,
+ fullScreenIconColor,
+ allowFullScreen,
+ caption,
float,
margin,
...rest
}) => {
- const [isFullScreenActive, setIsFullScreenActive] = useState(false);
-
- const onKeyDown = (event: React.KeyboardEvent) => {
- if (event.key === keys.ESCAPE) {
- event.preventDefault();
- event.stopPropagation();
- closeFullScreen();
- }
+ const [isFullScreen, setIsFullScreen] = useState(false);
+
+ const isNamedSize =
+ typeof size === 'string' && SIZES.includes(size as EuiImageSize);
+
+ const classes = classNames('euiImage', className);
+
+ const euiTheme = useEuiTheme();
+
+ const styles = euiImageStyles(euiTheme);
+
+ const cssStyles = [
+ styles.euiImage,
+ isNamedSize && styles[size as EuiImageSize],
+ !isNamedSize && styles.customSize,
+ hasShadow && styles.hasShadow,
+ ];
+
+ const cssIsFullScreenStyles = [styles.euiImage, styles.isFullScreen];
+
+ const isCustomSize = !isNamedSize && size !== 'original';
+ const customSize = typeof size === 'string' ? size : `${size}px`;
+ const imageStyleWithCustomSize = isCustomSize
+ ? {
+ ...style,
+ maxWidth: customSize,
+ maxHeight: customSize,
+ }
+ : style;
+
+ const isFullWidth = size === 'fullWidth';
+
+ const commonWrapperProps = {
+ hasShadow,
+ wrapperProps,
+ setIsFullScreen,
+ fullScreenIconColor,
+ isFullWidth,
+ allowFullScreen,
+ alt,
+ caption,
+ float,
+ margin,
};
- const closeFullScreen = () => {
- setIsFullScreenActive(false);
+ const commonImgProps = {
+ className: classes,
+ src: src || url,
+ ...rest,
};
- const openFullScreen = () => {
- setIsFullScreenActive(true);
- };
-
- const customStyle: React.CSSProperties = { ...style };
-
- let classes = classNames(
- 'euiImage',
- {
- 'euiImage--hasShadow': hasShadow,
- 'euiImage--allowFullScreen': allowFullScreen,
- },
- margin ? marginToClassNameMap[margin] : null,
- float ? floatToClassNameMap[float] : null,
- className
- );
-
- if (typeof size === 'string' && SIZES.includes(size)) {
- classes = `${classes} ${sizeToClassNameMap[size as ImageSize]}`;
- } else {
- classes = `${classes}`;
- customStyle.maxWidth = size;
- customStyle.maxHeight = size;
- // Set width back to auto to ensure aspect ratio is kept
- customStyle.width = 'auto';
- }
-
- let allowFullScreenButtonClasses = 'euiImage__button';
-
- // when the button is not custom we need it to go full width
- // to match the parent '.euiImage' width except when the size is original
- if (typeof size === 'string' && size !== 'original' && SIZES.includes(size)) {
- allowFullScreenButtonClasses = `${allowFullScreenButtonClasses} euiImage__button--fullWidth`;
- } else {
- allowFullScreenButtonClasses = `${allowFullScreenButtonClasses}`;
- }
-
- const [optionalCaptionRef, optionalCaptionText] = useInnerText();
- let optionalCaption;
- if (caption) {
- optionalCaption = (
-
- {caption}
-
- );
- }
-
- const allowFullScreenIcon = (
-
- );
-
- const fullScreenDisplay = (
-
-
- <>
-
-
-
-
- {optionalCaption}
-
-
- >
-
-
- );
-
- const fullscreenLabel = useEuiI18n(
- 'euiImage.openImage',
- 'Open fullscreen {alt} image',
- { alt }
- );
+ return (
+ <>
+
+
+
- if (allowFullScreen) {
- return (
-
-
+ {allowFullScreen && isFullScreen && (
+
- {allowFullScreenIcon}
-
- {isFullScreenActive && fullScreenDisplay}
- {optionalCaption}
-
- );
- } else {
- return (
-
-
- {optionalCaption}
-
- );
- }
+
+ )}
+ >
+ );
};
diff --git a/src/components/image/image_button.styles.ts b/src/components/image/image_button.styles.ts
new file mode 100644
index 00000000000..219409f9ec2
--- /dev/null
+++ b/src/components/image/image_button.styles.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { css } from '@emotion/react';
+import { euiFocusRing, logicalCSS, euiCanAnimate } from '../../global_styling';
+import { UseEuiTheme } from '../../services';
+import { euiShadow } from '../../themes/amsterdam/global_styling/mixins';
+
+export const euiImageButtonStyles = (euiThemeContext: UseEuiTheme) => {
+ const { euiTheme } = euiThemeContext;
+
+ return {
+ // Base
+ euiImageButton: css`
+ position: relative;
+ cursor: pointer;
+ text-align: match-parent;
+ line-height: 0;
+
+ // Shadow on hover - use a pseudo element & opacity for maximum animation performance
+ &::before {
+ opacity: 0;
+ content: '';
+ pointer-events: none; // Prevent interacting with this element, it's for visual effect only
+ position: absolute; // Skip logical properties here - should all be the same
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ ${euiCanAnimate} {
+ transition: opacity ${euiTheme.animation.fast}
+ ${euiTheme.animation.resistance};
+ }
+ }
+
+ &:hover,
+ &:focus {
+ &::before {
+ opacity: 1;
+ }
+
+ [class*='euiImageButton__icon'] {
+ opacity: 1;
+ }
+ }
+
+ &:focus {
+ ${euiFocusRing(euiTheme, 'outset')}
+ }
+ `,
+ fullWidth: css`
+ ${logicalCSS('width', '100%')}
+ `,
+ shadowHover: css`
+ &::before {
+ ${euiShadow(euiThemeContext, 's')}
+ }
+ `,
+ hasShadowHover: css`
+ &::before {
+ ${euiShadow(euiThemeContext, 'm')}
+ }
+ `,
+ };
+};
+
+export const euiImageButtonIconStyles = ({ euiTheme }: UseEuiTheme) => ({
+ // Base
+ euiImageButton__icon: css`
+ position: absolute;
+ ${logicalCSS('top', euiTheme.size.base)};
+ ${logicalCSS('right', euiTheme.size.base)};
+ `,
+ openFullScreen: css`
+ opacity: 0;
+ cursor: pointer;
+
+ ${euiCanAnimate} {
+ transition: opacity ${euiTheme.animation.slow}
+ ${euiTheme.animation.resistance};
+ }
+ `,
+ closeFullScreen: css`
+ // Fullscreen close event handled by EuiOverlayMask
+ pointer-events: none;
+ `,
+});
diff --git a/src/components/image/image_button.tsx b/src/components/image/image_button.tsx
new file mode 100644
index 00000000000..304c4ae2197
--- /dev/null
+++ b/src/components/image/image_button.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 } from 'react';
+
+import { useEuiTheme } from '../../services';
+import { useEuiI18n } from '../i18n';
+import { EuiIcon } from '../icon';
+import { EuiScreenReaderOnly } from '../accessibility';
+
+import {
+ euiImageButtonStyles,
+ euiImageButtonIconStyles,
+} from './image_button.styles';
+import type {
+ EuiImageButtonProps,
+ EuiImageButtonIconColor,
+} from './image_types';
+
+const fullScreenIconColorMap: {
+ [color in EuiImageButtonIconColor]: string;
+} = {
+ light: 'ghost',
+ dark: 'default',
+};
+
+export const EuiImageButton: FunctionComponent = ({
+ hasAlt,
+ hasShadow,
+ children,
+ onClick,
+ onKeyDown,
+ isFullScreen,
+ isFullWidth,
+ fullScreenIconColor = 'light',
+ ...rest
+}) => {
+ const euiTheme = useEuiTheme();
+
+ const buttonStyles = euiImageButtonStyles(euiTheme);
+
+ const cssButtonStyles = [
+ buttonStyles.euiImageButton,
+ hasShadow ? buttonStyles.hasShadowHover : buttonStyles.shadowHover,
+ !isFullScreen && isFullWidth && buttonStyles.fullWidth,
+ ];
+
+ const iconStyles = euiImageButtonIconStyles(euiTheme);
+ const cssIconStyles = [
+ iconStyles.euiImageButton__icon,
+ iconStyles.openFullScreen,
+ ];
+
+ const openFullScreenInstructions = useEuiI18n(
+ 'euiImageButton.openFullScreen',
+ 'Click to open this image in fullscreen mode'
+ );
+ const closeFullScreenInstructions = useEuiI18n(
+ 'euiImageButton.closeFullScreen',
+ 'Press Escape or click to close image fullscreen mode'
+ );
+
+ const iconColor =
+ fullScreenIconColorMap[fullScreenIconColor as EuiImageButtonIconColor];
+
+ return (
+ <>
+
+ {isFullScreen && (
+ // In fullscreen mode, instructions should come first to allow screen reader
+ // users to quickly exit vs. potentially reading out long/unskippable alt text
+
+
+ {closeFullScreenInstructions}
+ {hasAlt && ' — '}
+
+
+ )}
+
+ {children}
+
+ {!isFullScreen && (
+
+
+
+ {hasAlt && ' — '}
+ {openFullScreenInstructions}
+
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/src/components/image/image_caption.styles.ts b/src/components/image/image_caption.styles.ts
new file mode 100644
index 00000000000..89a08203321
--- /dev/null
+++ b/src/components/image/image_caption.styles.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { css } from '@emotion/react';
+import { euiFontSize, logicalCSS } from '../../global_styling';
+import { UseEuiTheme, transparentize } from '../../services';
+
+export const euiImageCaptionStyles = (euiThemeContext: UseEuiTheme) => {
+ const { euiTheme } = euiThemeContext;
+
+ return {
+ // Base
+ euiImageCaption: css`
+ ${euiFontSize(euiThemeContext, 's')};
+ ${logicalCSS('margin-top', euiTheme.size.xs)};
+ `,
+ isOnOverlayMask: css`
+ color: ${euiTheme.colors.ghost};
+ text-shadow: 0 1px 2px ${transparentize(euiTheme.colors.ink, 0.6)};
+
+ [class*='euiLink'] {
+ color: ${euiTheme.colors.ghost}; // Override link color for visibility
+ }
+ `,
+ };
+};
diff --git a/src/components/image/image_caption.tsx b/src/components/image/image_caption.tsx
new file mode 100644
index 00000000000..d27db0505d2
--- /dev/null
+++ b/src/components/image/image_caption.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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, { forwardRef, Ref } from 'react';
+
+import { useEuiTheme } from '../../services';
+
+import { euiImageCaptionStyles } from './image_caption.styles';
+import type { EuiImageCaptionProps } from './image_types';
+
+export const EuiImageCaption = forwardRef(
+ ({ caption, isOnOverlayMask = false }, ref: Ref) => {
+ const euiTheme = useEuiTheme();
+ const styles = euiImageCaptionStyles(euiTheme);
+ const cssStyles = [
+ styles.euiImageCaption,
+ isOnOverlayMask && styles.isOnOverlayMask,
+ ];
+
+ return (
+
+ {caption}
+
+ );
+ }
+);
+
+EuiImageCaption.displayName = 'EuiImageCaption';
diff --git a/src/components/image/image_fullscreen_wrapper.styles.ts b/src/components/image/image_fullscreen_wrapper.styles.ts
new file mode 100644
index 00000000000..9230791dbd1
--- /dev/null
+++ b/src/components/image/image_fullscreen_wrapper.styles.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { css, keyframes } from '@emotion/react';
+import {
+ logicalCSS,
+ logicalTextAlignCSS,
+ euiCanAnimate,
+} from '../../global_styling';
+import { UseEuiTheme } from '../../services';
+
+export const euiImageFullscreenWrapperStyles = (
+ euiThemeContext: UseEuiTheme
+) => {
+ const { euiTheme } = euiThemeContext;
+
+ return {
+ // Base
+ euiImageFullscreenWrapper: css`
+ ${logicalCSS('max-height', '80vh')};
+ ${logicalCSS('max-width', '80vw')};
+ ${logicalTextAlignCSS('center')}; // Aligns both caption and image
+ line-height: 0; // Fixes cropping when image is resized by forcing its height to be determined by the image not line-height
+
+ ${euiCanAnimate} {
+ animation: ${euiImageFullScreen(euiTheme.size.xxxxl)}
+ ${euiTheme.animation.extraSlow} ${euiTheme.animation.bounce};
+ }
+
+ &:hover [class*='euiImageCaption'] {
+ text-decoration: underline;
+ }
+ `,
+ // Sizes
+ fullWidth: css`
+ ${logicalCSS('width', '100%')}
+ `,
+ };
+};
+
+const euiImageFullScreen = (size: string) => keyframes`
+ 0% {
+ opacity: 0;
+ transform: translateY(${size});
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+`;
diff --git a/src/components/image/image_fullscreen_wrapper.tsx b/src/components/image/image_fullscreen_wrapper.tsx
new file mode 100644
index 00000000000..7d2887aa063
--- /dev/null
+++ b/src/components/image/image_fullscreen_wrapper.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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 } from 'react';
+import classNames from 'classnames';
+
+import { EuiFocusTrap } from '../focus_trap';
+import { EuiOverlayMask } from '../overlay_mask';
+import { EuiIcon } from '../icon';
+import { useEuiTheme, keys } from '../../services';
+import { useInnerText } from '../inner_text';
+
+import { euiImageFullscreenWrapperStyles } from './image_fullscreen_wrapper.styles';
+import type { EuiImageWrapperProps } from './image_types';
+
+import { EuiImageButton } from './image_button';
+import { euiImageButtonIconStyles } from './image_button.styles';
+
+import { EuiImageCaption } from './image_caption';
+
+export const EuiImageFullScreenWrapper: FunctionComponent = ({
+ alt,
+ hasShadow,
+ caption,
+ children,
+ setIsFullScreen,
+ wrapperProps,
+ isFullWidth,
+ fullScreenIconColor,
+}) => {
+ const euiTheme = useEuiTheme();
+
+ const styles = euiImageFullscreenWrapperStyles(euiTheme);
+
+ const cssStyles = [styles.euiImageFullscreenWrapper];
+
+ const classes = classNames(
+ 'euiImageFullScreenWrapper',
+ wrapperProps && wrapperProps.className
+ );
+
+ const onKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === keys.ESCAPE) {
+ event.preventDefault();
+ event.stopPropagation();
+ closeFullScreen();
+ }
+ };
+
+ const closeFullScreen = () => {
+ setIsFullScreen(false);
+ };
+
+ const [optionalCaptionRef, optionalCaptionText] = useInnerText();
+
+ const iconStyles = euiImageButtonIconStyles(euiTheme);
+ const cssIconStyles = [
+ iconStyles.euiImageButton__icon,
+ iconStyles.closeFullScreen,
+ ];
+
+ return (
+
+
+ <>
+
+
+ {children}
+
+
+
+ {/* Must be outside the `figure` element in order to escape the translateY transition. see https://www.w3.org/TR/css-transforms-1/#transform-rendering */}
+
+ >
+
+
+ );
+};
diff --git a/src/components/image/image_types.ts b/src/components/image/image_types.ts
new file mode 100644
index 00000000000..14c41374dde
--- /dev/null
+++ b/src/components/image/image_types.ts
@@ -0,0 +1,112 @@
+/*
+ * 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 { HTMLAttributes, ReactNode, ImgHTMLAttributes } from 'react';
+import { CommonProps, ExclusiveUnion } from '../common';
+
+export const SIZES = ['s', 'm', 'l', 'xl', 'fullWidth', 'original'] as const;
+export type EuiImageSize = typeof SIZES[number];
+
+const FLOATS = ['left', 'right'] as const;
+export type EuiImageWrapperFloat = typeof FLOATS[number];
+
+const MARGINS = ['s', 'm', 'l', 'xl'] as const;
+export type EuiImageWrapperMargin = typeof MARGINS[number];
+
+export type EuiImageButtonIconColor = 'light' | 'dark';
+
+type _EuiImageSrcOrUrl = ExclusiveUnion<
+ {
+ /**
+ * Requires either `src` or `url` but defaults to using `src` if both are provided
+ */
+ src: string;
+ },
+ {
+ url: string;
+ }
+>;
+
+export type EuiImageProps = CommonProps &
+ Omit, 'src' | 'alt'> &
+ _EuiImageSrcOrUrl & {
+ /**
+ * Alt text should describe the image to aid screen reader users. See
+ * https://webaim.org/techniques/alttext/ for a guide on writing
+ * effective alt text.
+ *
+ * If no meaningful description exists, or if the image is adequately
+ * described by the surrounding text, pass an empty string.
+ */
+ alt: string;
+ /**
+ * Provides a visible caption to the image
+ */
+ caption?: ReactNode;
+ /**
+ * Accepts `s` / `m` / `l` / `xl` / `original` / `fullWidth` / or a CSS size of `number` or `string`.
+ * `fullWidth` will set the figure to stretch to 100% of its container.
+ * `string` and `number` types will max both the width or height, whichever is greater.
+ */
+ size?: EuiImageSize | number | string;
+ /**
+ * Float the image to the left or right. Useful in large text blocks.
+ */
+ float?: EuiImageWrapperFloat;
+ /**
+ * Margin around the image.
+ */
+ margin?: EuiImageWrapperMargin;
+ /**
+ * When set to `true` (default) will apply a slight shadow to the image
+ */
+ hasShadow?: boolean;
+ /**
+ * When set to `true` will make the image clickable to a larger version
+ */
+ allowFullScreen?: boolean;
+ /**
+ * Changes the color of the icon that floats above the image when it can be clicked to fullscreen.
+ * The default value of `light` is fine unless your image has a white background, in which case you should change it to `dark`.
+ */
+ fullScreenIconColor?: EuiImageButtonIconColor;
+ /**
+ * Props to add to the wrapping figure element
+ */
+ wrapperProps?: HTMLAttributes;
+ };
+
+export type EuiImageWrapperProps = Pick<
+ EuiImageProps,
+ | 'alt'
+ | 'caption'
+ | 'float'
+ | 'margin'
+ | 'hasShadow'
+ | 'wrapperProps'
+ | 'fullScreenIconColor'
+ | 'allowFullScreen'
+> & {
+ isFullWidth: boolean;
+ setIsFullScreen: (isFullScreen: boolean) => void;
+};
+
+export type EuiImageButtonProps = Pick<
+ EuiImageProps,
+ 'hasShadow' | 'fullScreenIconColor'
+> & {
+ hasAlt: boolean;
+ onClick: () => void;
+ onKeyDown?: (e: React.KeyboardEvent) => void;
+ isFullWidth: boolean;
+ isFullScreen?: boolean;
+};
+
+export type EuiImageCaptionProps = Pick & {
+ isOnOverlayMask?: boolean;
+};
diff --git a/src/components/image/image_wrapper.styles.ts b/src/components/image/image_wrapper.styles.ts
new file mode 100644
index 00000000000..4c68d66da93
--- /dev/null
+++ b/src/components/image/image_wrapper.styles.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { css } from '@emotion/react';
+import {
+ logicalCSS,
+ logicalTextAlignCSS,
+ logicalSide,
+} from '../../global_styling';
+import { UseEuiTheme } from '../../services';
+
+export const euiImageWrapperStyles = (euiThemeContext: UseEuiTheme) => {
+ const { euiTheme } = euiThemeContext;
+
+ return {
+ // Base
+ euiImageWrapper: css`
+ display: table; // inline-block causes margins not to correctly collapse
+ ${logicalCSS('max-width', '100%')}
+ ${logicalTextAlignCSS('center')}; // Aligns both caption and image
+ line-height: 0; // Fixes cropping when image is resized by forcing its height to be determined by the image not line-height
+ flex-shrink: 0; // Don't ever let this shrink in height if direct descendent of flex
+ `,
+ allowFullScreen: css`
+ &:hover [class*='euiImageCaption'] {
+ text-decoration: underline;
+ }
+ `,
+ // Margins
+ s: css`
+ margin: ${euiTheme.size.s};
+ `,
+ m: css`
+ margin: ${euiTheme.size.base};
+ `,
+ l: css`
+ margin: ${euiTheme.size.l};
+ `,
+ xl: css`
+ margin: ${euiTheme.size.xl};
+ `,
+ // Floats
+ // 1: Logical properties/values in `float` is currently not yet supported by all browsers w/o flags
+ // @see https://caniuse.com/mdn-css_properties_float_flow_relative_values for when we can remove left/right fallbacks
+ left: css`
+ @media only screen and (min-width: ${euiTheme.breakpoint.m}px) {
+ float: left; /* 1 */
+ float: ${logicalSide.left};
+ ${logicalCSS('margin-left', '0')};
+ ${logicalCSS('margin-top', '0')};
+ }
+ `,
+ right: css`
+ @media only screen and (min-width: ${euiTheme.breakpoint.m}px) {
+ float: right; /* 1 */
+ float: ${logicalSide.right};
+ ${logicalCSS('margin-right', '0')};
+ ${logicalCSS('margin-top', '0')};
+ }
+ `,
+ // Sizes
+ fullWidth: css`
+ ${logicalCSS('width', '100%')}
+ `,
+ };
+};
diff --git a/src/components/image/image_wrapper.tsx b/src/components/image/image_wrapper.tsx
new file mode 100644
index 00000000000..a2a351f6718
--- /dev/null
+++ b/src/components/image/image_wrapper.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 } from 'react';
+import classNames from 'classnames';
+
+import { useEuiTheme } from '../../services';
+import { useInnerText } from '../inner_text';
+
+import type { EuiImageWrapperProps } from './image_types';
+
+import { euiImageWrapperStyles } from './image_wrapper.styles';
+import { EuiImageButton } from './image_button';
+import { EuiImageCaption } from './image_caption';
+
+export const EuiImageWrapper: FunctionComponent = ({
+ alt,
+ caption,
+ hasShadow,
+ allowFullScreen,
+ float,
+ margin,
+ children,
+ setIsFullScreen,
+ wrapperProps,
+ fullScreenIconColor,
+ isFullWidth,
+}) => {
+ const openFullScreen = () => {
+ setIsFullScreen(true);
+ };
+
+ const classes = classNames(
+ 'euiImageWrapper',
+ wrapperProps && wrapperProps.className
+ );
+
+ const euiTheme = useEuiTheme();
+
+ const styles = euiImageWrapperStyles(euiTheme);
+ const cssFigureStyles = [
+ styles.euiImageWrapper,
+ float && styles[float],
+ margin && styles[margin],
+ allowFullScreen && styles.allowFullScreen,
+ isFullWidth && styles.fullWidth,
+ ];
+
+ const [optionalCaptionRef, optionalCaptionText] = useInnerText();
+
+ return (
+
+ {allowFullScreen ? (
+ <>
+
+ {children}
+
+ >
+ ) : (
+ children
+ )}
+
+
+
+ );
+};
diff --git a/src/components/image/index.ts b/src/components/image/index.ts
index fdbb7b61bcd..6fc04e46d62 100644
--- a/src/components/image/index.ts
+++ b/src/components/image/index.ts
@@ -6,5 +6,5 @@
* Side Public License, v 1.
*/
-export type { EuiImageProps } from './image';
+export type { EuiImageProps } from './image_types';
export { EuiImage } from './image';
diff --git a/src/components/index.scss b/src/components/index.scss
index 4cdd868f7c2..fde72a15190 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -25,7 +25,6 @@
@import 'flyout/index';
@import 'form/index';
@import 'header/index';
-@import 'image/index';
@import 'key_pad_menu/index';
@import 'list_group/index';
@import 'markdown_editor/index';
diff --git a/src/components/text/text.styles.ts b/src/components/text/text.styles.ts
index 1fa9fafe0cc..ec2a885ec74 100644
--- a/src/components/text/text.styles.ts
+++ b/src/components/text/text.styles.ts
@@ -229,7 +229,7 @@ export const euiTextStyles = (euiThemeContext: UseEuiTheme) => {
img {
display: block;
- width: 100%;
+ ${logicalCSS('max-width', '100%')}
}
ul {
diff --git a/src/themes/amsterdam/overrides/_image.scss b/src/themes/amsterdam/overrides/_image.scss
deleted file mode 100644
index 3e240097340..00000000000
--- a/src/themes/amsterdam/overrides/_image.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-.euiImage-isFullScreen {
- .euiImage__caption {
- color: $euiColorGhost;
- text-shadow: 0 1px 2px transparentize($euiColorInk, .6);
- }
-}
-
-.euiImage-isFullScreenCloseIcon {
- fill: $euiColorGhost;
-}
\ No newline at end of file
diff --git a/src/themes/amsterdam/overrides/_index.scss b/src/themes/amsterdam/overrides/_index.scss
index f4cceb3bab6..a45e2dc0132 100644
--- a/src/themes/amsterdam/overrides/_index.scss
+++ b/src/themes/amsterdam/overrides/_index.scss
@@ -19,7 +19,6 @@
@import 'header';
@import 'hue';
@import 'list_group_item';
-@import 'image';
@import 'key_pad_menu';
@import 'markdown_editor';
@import 'modal';
diff --git a/upcoming_changelogs/5969.md b/upcoming_changelogs/5969.md
new file mode 100644
index 00000000000..0386ae93383
--- /dev/null
+++ b/upcoming_changelogs/5969.md
@@ -0,0 +1,11 @@
+- Updated `EuiText.img` styles to prevent images from growing full width
+- Improved `EuiImage`'s `allowFullScreen` screen reader experience
+- Updated `EuiImage`'s full screen mode to use the `fullScreenExit` icon
+
+**CSS-in-JS**
+
+- Converted `EuiImage` to Emotion
+
+**Breaking changes**
+
+- Updated `EuiImage.className` to be applied to the `img` instead of the parent wrapper `figure` and added `wrapperProps` prop so that consumers can apply props to the `figure` element