From ae50b0a03ca21961c478b6458b90f918be32d6ba Mon Sep 17 00:00:00 2001 From: Tamas Kovacs Date: Tue, 7 Apr 2026 10:21:18 +0200 Subject: [PATCH] feat(ui-img): rework Img BREAKING CHANGE: contains breaking changes due to component using the new theming system INSTUI-4970 --- packages/ui-img/package.json | 21 +-- packages/ui-img/src/Img/v2/README.md | 124 +++++++++++++++ .../src/Img/{v1 => v2}/__tests__/Img.test.tsx | 0 packages/ui-img/src/Img/v2/index.tsx | 139 ++++++++++++++++ packages/ui-img/src/Img/v2/props.ts | 89 +++++++++++ packages/ui-img/src/Img/v2/styles.ts | 149 ++++++++++++++++++ packages/ui-img/src/exports/b.ts | 25 +++ packages/ui-img/tsconfig.build.json | 1 + pnpm-lock.yaml | 3 + 9 files changed, 541 insertions(+), 10 deletions(-) create mode 100644 packages/ui-img/src/Img/v2/README.md rename packages/ui-img/src/Img/{v1 => v2}/__tests__/Img.test.tsx (100%) create mode 100644 packages/ui-img/src/Img/v2/index.tsx create mode 100644 packages/ui-img/src/Img/v2/props.ts create mode 100644 packages/ui-img/src/Img/v2/styles.ts create mode 100644 packages/ui-img/src/exports/b.ts diff --git a/packages/ui-img/package.json b/packages/ui-img/package.json index 8e97ae3470..c98fcb305d 100644 --- a/packages/ui-img/package.json +++ b/packages/ui-img/package.json @@ -29,6 +29,7 @@ "@instructure/shared-types": "workspace:*", "@instructure/ui-dom-utils": "workspace:*", "@instructure/ui-react-utils": "workspace:*", + "@instructure/ui-themes": "workspace:*", "@instructure/ui-view": "workspace:*" }, "devDependencies": { @@ -67,18 +68,18 @@ "default": "./es/exports/a.js" }, "./v11_7": { - "src": "./src/exports/a.ts", - "types": "./types/exports/a.d.ts", - "import": "./es/exports/a.js", - "require": "./lib/exports/a.js", - "default": "./es/exports/a.js" + "src": "./src/exports/b.ts", + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./latest": { - "src": "./src/exports/a.ts", - "types": "./types/exports/a.d.ts", - "import": "./es/exports/a.js", - "require": "./lib/exports/a.js", - "default": "./es/exports/a.js" + "src": "./src/exports/b.ts", + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" } } } diff --git a/packages/ui-img/src/Img/v2/README.md b/packages/ui-img/src/Img/v2/README.md new file mode 100644 index 0000000000..73a6cdb8ba --- /dev/null +++ b/packages/ui-img/src/Img/v2/README.md @@ -0,0 +1,124 @@ +--- +describes: Img +--- + +An accessible image component + +```js +--- +type: example +--- + +``` + +### Margin and display + +Use the `margin` prop to add space around ``. Setting the `display` prop to `block` makes +the image a block-level element. + +```js +--- +type: example +--- + + A placeholder image + + + +``` + +### Color overlay + +The `overlay` prop accepts parameters for `color`, `opacity`, and `blend`. + +```js +--- +type: example +--- + + A placeholder image + A placeholder image + A placeholder image + +``` + +### Cover + +When the `constrain` prop is set to `cover` the image fills the _full_ width and height of its +containing element, while maintaining the aspect ratio of the source image. + +```js +--- +type: example +--- +
+ +
+``` + +### Contain + +When the `constrain` prop is set to `contain` the image fits within the width and height of its +containing element, while maintaining the aspect ratio of the source image. + +```js +--- +type: example +--- + + + +``` + +### Grayscale and blur filters + +Please note: these should only be used for presentational effects. + +```js +--- +type: example +--- + + A placeholder image + A placeholder image + +``` + +### Guidelines + +```js +--- +type: embed +--- + +
+ Contextual images must have alternative text that describes the information or function represented by them + Decorative images that do not present important content, are used for layout or non-informative purposes, and do not appear within a link do not need to be presented to screen readers. Decorative and spacer images should have null alternative text (alt="") +
+
+``` diff --git a/packages/ui-img/src/Img/v1/__tests__/Img.test.tsx b/packages/ui-img/src/Img/v2/__tests__/Img.test.tsx similarity index 100% rename from packages/ui-img/src/Img/v1/__tests__/Img.test.tsx rename to packages/ui-img/src/Img/v2/__tests__/Img.test.tsx diff --git a/packages/ui-img/src/Img/v2/index.tsx b/packages/ui-img/src/Img/v2/index.tsx new file mode 100644 index 0000000000..f9680c164d --- /dev/null +++ b/packages/ui-img/src/Img/v2/index.tsx @@ -0,0 +1,139 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Component } from 'react' + +import { View } from '@instructure/ui-view/latest' +import { passthroughProps } from '@instructure/ui-react-utils' + +import { withStyle } from '@instructure/emotion' + +import generateStyle from './styles' + +import { allowedProps } from './props' +import type { ImgProps } from './props' + +/** +--- +category: components +--- +**/ +@withStyle(generateStyle) +class Img extends Component { + static readonly componentId = 'Img' + + static allowedProps = allowedProps + static defaultProps = { + alt: '', + display: 'inline-block', + withGrayscale: false, + withBlur: false + } + + ref: Element | null = null + + handleRef = (el: Element | null) => { + const { elementRef } = this.props + + this.ref = el + + if (typeof elementRef === 'function') { + elementRef(el) + } + } + + componentDidMount() { + this.props.makeStyles?.() + } + + componentDidUpdate() { + this.props.makeStyles?.() + } + + render() { + const { + src, + alt, + margin, + display, + overlay, + withGrayscale, + withBlur, + constrain, + width, + height, + elementRef, + styles, + loading, + ...props + } = this.props + + const a11yProps = { + alt: alt || '' + } + + const imageProps = { + css: styles?.img, + src, + loading + } + + const containerProps = { + ...passthroughProps(props), + width, + height, + margin, + display, + elementRef: this.handleRef + } + + if (overlay) { + // if a background image is rendered we add the a11y props on the container element + const rootProps = { + ...containerProps + } + + return ( + + {/* eslint-disable-next-line jsx-a11y/alt-text*/} + {} + {overlay && } + + ) + } else { + return ( + + ) + } + } +} + +export default Img +export { Img } diff --git a/packages/ui-img/src/Img/v2/props.ts b/packages/ui-img/src/Img/v2/props.ts new file mode 100644 index 0000000000..c0ca8539cc --- /dev/null +++ b/packages/ui-img/src/Img/v2/props.ts @@ -0,0 +1,89 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { OtherHTMLAttributes } from '@instructure/shared-types' +import type { + Spacing, + WithStyleProps, + ComponentStyle +} from '@instructure/emotion' +import type { NewComponentTypes } from '@instructure/ui-themes' + +type ImgOwnProps = { + src: string + alt?: string + display?: 'inline-block' | 'block' + /** + * Gets passed down to the img component. Same as the native HTML img's loading attribute + */ + loading?: 'eager' | 'lazy' + /** + * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, + * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via + * familiar CSS-like shorthand. For example: `margin="small auto large"`. + */ + margin?: Spacing + /** + * Valid values for `opacity` are `0` - `10`. Valid values for `blend` are + * `normal` (default), `multiply`, `screen`, `overlay`, and `color-burn`. + */ + overlay?: { + color: string + opacity: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 + blend?: 'normal' | 'multiply' | 'screen' | 'overlay' | 'color-burn' + } + withGrayscale?: boolean + withBlur?: boolean + constrain?: 'cover' | 'contain' + elementRef?: (element: Element | null) => void + height?: string | number + width?: string | number +} + +type PropKeys = keyof ImgOwnProps + +type AllowedPropKeys = Readonly> + +type ImgProps = ImgOwnProps & + WithStyleProps & + OtherHTMLAttributes + +type ImgStyle = ComponentStyle<'overlay' | 'container' | 'img'> +const allowedProps: AllowedPropKeys = [ + 'src', + 'alt', + 'display', + 'loading', + 'margin', + 'overlay', + 'withGrayscale', + 'withBlur', + 'constrain', + 'elementRef', + 'height', + 'width' +] + +export type { ImgProps, ImgStyle } +export { allowedProps } diff --git a/packages/ui-img/src/Img/v2/styles.ts b/packages/ui-img/src/Img/v2/styles.ts new file mode 100644 index 0000000000..6873e65259 --- /dev/null +++ b/packages/ui-img/src/Img/v2/styles.ts @@ -0,0 +1,149 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { NewComponentTypes } from '@instructure/ui-themes' +import type { ImgProps, ImgStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: NewComponentTypes['Img'], + props: ImgProps +): ImgStyle => { + const { overlay, withBlur, withGrayscale, constrain } = props + + const isCover = constrain === 'cover' + const isContain = constrain === 'contain' + + // if overlay or filters are updated via props, + // make the transition look smooth + const transitionStyle = { + transition: `all ${componentTheme.effectTransitionDuration}` + } + + const getFilterStyle = () => { + const filters = [] + + withBlur && filters.push(`blur(${componentTheme.imageBlurAmount})`) + withGrayscale && filters.push('grayscale(1)') + + return filters.length > 0 + ? { + ...transitionStyle, + filter: filters.join(' ') + } + : { + filter: 'none' + } + } + + const fillContainer = { + width: '100%', + height: '100%' + } + + const imgCoverStyle = { + objectFit: 'cover', + ...fillContainer + } + + const imgContainStyle = { + objectFit: 'contain', + ...fillContainer, + ...(overlay && { + width: 'auto', + height: 'auto', + maxWidth: '100%', + maxHeight: '100%' + }) + } + + return { + overlay: { + label: 'img__overlay', + position: 'absolute', + top: '0px', + left: '0px', + width: '100%', + height: '100%', + ...transitionStyle, + ...(overlay && { + backgroundColor: overlay.color, + opacity: overlay.opacity * 0.1, + mixBlendMode: overlay.blend ? overlay.blend : undefined + }) + }, + + container: { + label: 'img__container', + ...(overlay && { + position: 'relative', + overflow: + 'hidden' /* stops blurred images extending past overlay borders */ + }), + ...(isCover && fillContainer), + ...(isContain && { height: 'inherit' }) + }, + + img: { + label: 'img', + // reset image styles (initial: all was causing conflicts + // View's CSS and overriding height/width attrs) + margin: '0', + padding: '0', + float: 'none', + top: 'auto', + bottom: 'auto', + left: 'auto', + right: 'auto', + lineHeight: 'normal', + position: 'static', + transform: 'none', + maxHeight: 'none', + minHeight: '0', + minWidth: '0', + maxWidth: '100%', + + ...getFilterStyle(), + + ...(overlay && { + // when image is contained in overlay, + // avoid extra space at bottom from inline/line-height + display: 'block' + }), + ...(isCover && imgCoverStyle), + ...(isContain && imgContainStyle) + } + } +} + +export default generateStyle diff --git a/packages/ui-img/src/exports/b.ts b/packages/ui-img/src/exports/b.ts new file mode 100644 index 0000000000..90d62444bc --- /dev/null +++ b/packages/ui-img/src/exports/b.ts @@ -0,0 +1,25 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export { Img } from '../Img/v2' +export type { ImgProps } from '../Img/v2/props' diff --git a/packages/ui-img/tsconfig.build.json b/packages/ui-img/tsconfig.build.json index b98a354f6f..27eaaae0f3 100644 --- a/packages/ui-img/tsconfig.build.json +++ b/packages/ui-img/tsconfig.build.json @@ -12,6 +12,7 @@ { "path": "../shared-types/tsconfig.build.json" }, { "path": "../ui-dom-utils/tsconfig.build.json" }, { "path": "../ui-react-utils/tsconfig.build.json" }, + { "path": "../ui-themes/tsconfig.build.json" }, { "path": "../ui-view/tsconfig.build.json" }, { "path": "../ui-babel-preset/tsconfig.build.json" }, { "path": "../ui-axe-check/tsconfig.build.json" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4180ce7132..0e52896fe7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2468,6 +2468,9 @@ importers: '@instructure/ui-react-utils': specifier: workspace:* version: link:../ui-react-utils + '@instructure/ui-themes': + specifier: workspace:* + version: link:../ui-themes '@instructure/ui-view': specifier: workspace:* version: link:../ui-view