diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf3b10d0e9..c3f99aece90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # [`master`](https://github.com/elastic/eui/tree/master) +- Added `` component to allow for image sizing and zooms. [(#262)](https://github.com/elastic/eui/pull/262) - Updated `` to append `
` to body. [(#254)](https://github.com/elastic/eui/pull/254) **Bug fixes** diff --git a/src-docs/src/services/routes/routes.js b/src-docs/src/services/routes/routes.js index e4109ac0c44..cfdd162aad4 100644 --- a/src-docs/src/services/routes/routes.js +++ b/src-docs/src/services/routes/routes.js @@ -83,6 +83,9 @@ import { HorizontalRuleExample } import { IconExample } from '../../views/icon/icon_example'; +import { ImageExample } + from '../../views/image/image_example'; + import { KeyPadMenuExample } from '../../views/key_pad_menu/key_pad_menu_example'; @@ -208,6 +211,7 @@ const components = [ HealthExample, HorizontalRuleExample, IconExample, + ImageExample, KeyPadMenuExample, LinkExample, LoadingExample, diff --git a/src-docs/src/views/icon/icon_colors.js b/src-docs/src/views/icon/icon_colors.js index ad09c198eeb..075d992c539 100644 --- a/src-docs/src/views/icon/icon_colors.js +++ b/src-docs/src/views/icon/icon_colors.js @@ -17,6 +17,7 @@ const iconColors = [ 'danger', 'text', 'subdued', + 'ghost', ]; export default () => ( diff --git a/src-docs/src/views/image/image.js b/src-docs/src/views/image/image.js new file mode 100644 index 00000000000..5b3ec595e17 --- /dev/null +++ b/src-docs/src/views/image/image.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { + EuiImage, +} from '../../../../src/components'; + +export default () => ( + +); diff --git a/src-docs/src/views/image/image_example.js b/src-docs/src/views/image/image_example.js new file mode 100644 index 00000000000..bfd85cdd807 --- /dev/null +++ b/src-docs/src/views/image/image_example.js @@ -0,0 +1,112 @@ +import React from 'react'; + +import { renderToHtml } from '../../services'; + +import { + GuideSectionTypes, +} from '../../components'; + +import { + EuiCode, +} from '../../../../src/components'; + +import Image from './image'; +const imageSource = require('!!raw-loader!./image'); +const imageHtml = renderToHtml(Image); + +import ImageSizes from './image_size'; +const imageSizesSource = require('!!raw-loader!./image_size'); +const imageSizesHtml = renderToHtml(ImageSizes); + +import ImageZoom from './image_zoom'; +const imageZoomSource = require('!!raw-loader!./image_zoom'); +const imageZoomHtml = renderToHtml(ImageZoom); + +export const ImageExample = { + title: 'Image', + sections: [ + { + title: 'Image', + source: [{ + type: GuideSectionTypes.JS, + code: imageSource, + }, { + type: GuideSectionTypes.HTML, + code: imageHtml, + }], + text: ( +
+

+ Use EuiImage when you need to place a static image + into a page with an optional caption. It has the following props. +

+
    +
  • + size accepts s / m / l / xl / original / fullWidth. + The latter will set the figure to stretch to 100% of its container. +
  • +
  • + allowFullScreen when set to true will make the image + clicakable to a larger version. +
  • +
  • + fullScreenIconColor allows you to change 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. +
  • +
  • + hasShadow when set to true (default) will apply + a slight shadow below the image. +
  • +
  • + caption will provide a caption to the image. +
  • +
  • + alt Sepearate from the caption is a title on the alt tag itself. + This one is required for accessibility. +
  • +
+
+ ), + demo: , + }, + { + title: 'Click an image for a full screen version', + source: [{ + type: GuideSectionTypes.JS, + code: imageZoomSource, + }, { + type: GuideSectionTypes.HTML, + code: imageZoomHtml, + }], + text: ( +

+ Apply the allowFullScreen prop to make the image + clickable and show a full screen version. Note that the second image also + passes fullScreenIconColor="dark" to change icon color + to better contrast against the light background of that image. +

+ ), + demo: , + }, + { + title: 'Images can be sized', + source: [{ + type: GuideSectionTypes.JS, + code: imageSizesSource, + }, { + type: GuideSectionTypes.HTML, + code: imageSizesHtml, + }], + text: ( +

+ Images can be sized by passing the size prop a value + of s / m / l / xl / original / fullWidth. Note that this size + is applied to the width of the image. +

+ ), + demo: , + }, + ], +}; diff --git a/src-docs/src/views/image/image_size.js b/src-docs/src/views/image/image_size.js new file mode 100644 index 00000000000..7f02f61f802 --- /dev/null +++ b/src-docs/src/views/image/image_size.js @@ -0,0 +1,63 @@ +import React from 'react'; + +import { + EuiImage, + EuiSpacer, +} from '../../../../src/components'; + +export default () => ( +
+ + + + + + + + + + + +
+); diff --git a/src-docs/src/views/image/image_zoom.js b/src-docs/src/views/image/image_zoom.js new file mode 100644 index 00000000000..67e9f3534f8 --- /dev/null +++ b/src-docs/src/views/image/image_zoom.js @@ -0,0 +1,33 @@ +import React from 'react'; + +import { + EuiImage, + EuiFlexGroup, + EuiFlexItem, +} from '../../../../src/components'; + +export default () => ( + + + + + + + + +); diff --git a/src/components/icon/_icon.scss b/src/components/icon/_icon.scss index b1ed2a71a4b..cf4bd261a1d 100644 --- a/src/components/icon/_icon.scss +++ b/src/components/icon/_icon.scss @@ -41,6 +41,10 @@ fill: $euiColorDanger; } +.euiIcon--ghost { + fill: $euiColorGhost; +} + .euiIcon--small { @include size($euiSizeM); } diff --git a/src/components/icon/icon.js b/src/components/icon/icon.js index 0699a2acdf9..472b8589845 100644 --- a/src/components/icon/icon.js +++ b/src/components/icon/icon.js @@ -224,6 +224,7 @@ const colorToClassMap = { danger: 'euiIcon--danger', text: 'euiIcon--text', subdued: 'euiIcon--subdued', + ghost: 'euiIcon--ghost', }; export const COLORS = Object.keys(colorToClassMap); diff --git a/src/components/image/__snapshots__/image.test.js.snap b/src/components/image/__snapshots__/image.test.js.snap new file mode 100644 index 00000000000..b8cdc299489 --- /dev/null +++ b/src/components/image/__snapshots__/image.test.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiImage is rendered 1`] = ` +
+ alt +
+`; diff --git a/src/components/image/_image.scss b/src/components/image/_image.scss new file mode 100644 index 00000000000..7435246c697 --- /dev/null +++ b/src/components/image/_image.scss @@ -0,0 +1,93 @@ +// Main
that wraps images. +.euiImage { + display: inline-block; + max-width: 100%; + position: relative; + + &.euiImage--hasShadow { + .euiImage__img { + @include euiBottomShadowMedium; + } + } + + &.euiImage--allowFullScreen:hover { + .euiImage__img { + cursor: pointer; + } + + .euiImage__icon { + visibility: visible; + opacity: 1; + } + } + + // These sizes are mostly suggestions. Don't look too hard for meaning in their values. + &.euiImage--small { + width: convertToRem(120px); + } + + &.euiImage--medium { + width: convertToRem(200px); + } + + &.euiImage--large { + width: convertToRem(360px); + } + + &.euiImage--xlarge { + width: convertToRem(600px); + } + + &.euiImage--fullWidth { + width: 100%; + } +} + +// The image itself is full width within the container. +.euiImage__img { + width: 100%; +} + +.euiImage__caption { + text-align: center; + @include euiFontSizeS; +} + +.euiImage__icon { + visibility: hidden; + opacity: 0; + position: absolute; + right: $euiSize; + top: $euiSize; + transition: opacity $euiAnimSpeedSlow $euiAnimSlightResistance ; + cursor: pointer; +} + +// The FullScreen image that optionally pops up on click. +.euiImageFullScreen { + max-height: 80vh; + max-width: 80vw; + animation: euiImageFullScreen $euiAnimSpeedExtraSlow $euiAnimSlightBounce; + + .euiImageFullScreen__img { + max-height: 80vh; + max-width: 80vw; + cursor: pointer; + } + + &:hover .euiImageFullScreen__img { + cursor: pointer; + } +} + + +@keyframes euiImageFullScreen { + 0% { + opacity: 0; + transform: translateY($euiSizeXL * 2); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/components/image/_index.scss b/src/components/image/_index.scss new file mode 100644 index 00000000000..eb326aae4dd --- /dev/null +++ b/src/components/image/_index.scss @@ -0,0 +1 @@ +@import 'image'; diff --git a/src/components/image/image.js b/src/components/image/image.js new file mode 100644 index 00000000000..cb2a110e100 --- /dev/null +++ b/src/components/image/image.js @@ -0,0 +1,155 @@ +import React, { + Component, +} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { keyCodes } from '../../services'; +import FocusTrap from 'focus-trap-react'; + +import { + EuiOverlayMask, + EuiIcon, +} from '../../components'; + +const sizeToClassNameMap = { + s: 'euiImage--small', + m: 'euiImage--medium', + l: 'euiImage--large', + xl: 'euiImage--xlarge', + fullWidth: 'euiImage--fullWidth', + original: '', +}; + +export const SIZES = Object.keys(sizeToClassNameMap); + +const fullScreenIconColorMap = { + light: 'ghost', + dark: 'default', +}; + +export class EuiImage extends Component { + + static propTypes = { + children: PropTypes.node, + className: PropTypes.string, + } + + constructor(props) { + super(props); + + this.state = { + isImageFullScreen: false, + }; + + this.toggleImageFullScreen = this.toggleImageFullScreen.bind(this); + } + + onKeyDown = event => { + if (event.keyCode === keyCodes.ESCAPE) { + this.toggleImageFullScreen(); + } + }; + + // Only toggle the state if allowed by allowFullScreen prop. + toggleImageFullScreen() { + const currentState = this.state.isImageFullScreen; + if (this.props.allowFullScreen) { + this.setState({ + isImageFullScreen: !currentState, + }); + } + } + + render() { + const { + className, + url, + size, + caption, + hasShadow, + allowFullScreen, + fullScreenIconColor, + alt, + ...rest + } = this.props; + + const classes = classNames( + 'euiImage', + sizeToClassNameMap[size], + { + 'euiImage--hasShadow': hasShadow, + 'euiImage--allowFullScreen': allowFullScreen, + }, + className + ); + + let optionalCaption; + if (caption) { + optionalCaption = ( +
+ {caption} +
+ ); + } + + let optionalIcon; + if (allowFullScreen) { + optionalIcon = ; + } + + let FullScreenDisplay; + + if (this.state.isImageFullScreen) { + FullScreenDisplay = ( + this.figure, + }} + > + +
{ this.figure = node; }} + className="euiImageFullScreen" + onClick={this.toggleImageFullScreen} + tabIndex={0} + onKeyDown={this.onKeyDown} + > + {alt} + {optionalCaption} +
+
+
+ ); + } + + return ( +
+ {alt} + {optionalCaption} + + {/* + If the below FullScreen image renders, it actually attaches to the body because of + EuiOverlayMask's React portal usage. + */} + {optionalIcon} + {FullScreenDisplay} +
+ ); + } +} + +EuiImage.propTypes = { + alt: PropTypes.string.isRequired, + size: PropTypes.string.isRequired, + fullScreenIconColor: PropTypes.string, +}; + +EuiImage.defaultProps = { + size: 'original', + fullScreenIconColor: 'light', +}; diff --git a/src/components/image/image.test.js b/src/components/image/image.test.js new file mode 100644 index 00000000000..c455d7810a4 --- /dev/null +++ b/src/components/image/image.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test'; + +import { EuiImage } from './image'; + +describe('EuiImage', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component) + .toMatchSnapshot(); + }); +}); diff --git a/src/components/image/index.js b/src/components/image/index.js new file mode 100644 index 00000000000..d7957eb8399 --- /dev/null +++ b/src/components/image/index.js @@ -0,0 +1,3 @@ +export { + EuiImage, +} from './image'; diff --git a/src/components/index.js b/src/components/index.js index 99c7f991da9..9d0b1cc4929 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -120,6 +120,10 @@ export { EuiIcon, } from './icon'; +export { + EuiImage, +} from './image'; + export { EuiLoadingKibana, EuiLoadingChart, diff --git a/src/components/index.scss b/src/components/index.scss index c80db7ad755..6100b05f01b 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -21,6 +21,7 @@ @import 'health/index'; @import 'horizontal_rule/index'; @import 'icon/index'; +@import 'image/index'; @import 'key_pad_menu/index'; @import 'link/index'; @import 'loading/index';