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`] = `
alt +
`; -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`] = `
alt +
+ + caption + +
`; -exports[`EuiImage is rendered with a margin 1`] = ` +exports[`EuiImage props float 1`] = `
alt +
+
+`; + +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`] = `
alt
- - caption - -
+ class="emotion-euiImageCaption" + />
`; -exports[`EuiImage is rendered with custom size 1`] = ` +exports[`EuiImage props size 1`] = `
alt +
+
+`; + +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`] = ` +
alt +
+ + 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 ( + <> + + {alt} + - if (allowFullScreen) { - return ( -
- - {isFullScreenActive && fullScreenDisplay} - {optionalCaption} -
- ); - } else { - return ( -
- {alt} - {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 ( + <> + + + ); +}; 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