diff --git a/breakings/2.2.3/image.yml b/breakings/2.2.3/image.yml new file mode 100644 index 0000000000..0c0b3c64b9 --- /dev/null +++ b/breakings/2.2.3/image.yml @@ -0,0 +1,12 @@ +- scope: 'component' + name: 'el-image' + type: 'props' + version: '2.2.3' + commit_hash: '7a48556' + description: | + Per [HTMLImageElement.loading Request](https://github.com/element-plus/element-plus/issues/7841), + Add [native loading](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) support for Image components. + props: + - api: 'loading' + before: '' + after: '"eager" | "lazy"' diff --git a/docs/en-US/component/image.md b/docs/en-US/component/image.md index e467f6855d..1aecad53c8 100644 --- a/docs/en-US/component/image.md +++ b/docs/en-US/component/image.md @@ -33,6 +33,14 @@ image/load-failed ## Lazy Load +:::tip + +Native `loading` has been supported since , you can use `loading = "lazy"` to replace `lazy = true`. + +If the current browser supports native lazy loading, the native lazy loading will be used first, otherwise will be implemented through scroll. + +::: + :::demo Use lazy load by `lazy = true`. Image will load until scroll into view when set. You can indicate scroll container that adds scroll listener to by `scroll-container`. If undefined, will be the nearest parent container whose overflow property is auto or scroll. image/lazy-load @@ -51,20 +59,21 @@ image/image-preview ### Image Attributes -| Name | Description | Type | Default | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- | -| `src` | image source, same as native. | `string` | — | -| `fit` | indicate how the image should be resized to fit its container, same as [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit). | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale'-down'` | — | -| `hide-on-click-modal` | when enabling preview, use this flag to control whether clicking on backdrop can exit preview mode. | `boolean` | `false` | -| `lazy` | whether to use lazy load. | `boolean` | `false` | -| `scroll-container` | the container to add scroll listener when using lazy load. | `string \| HTMLElement` | the nearest parent container whose overflow property is auto or scroll. | -| `alt` | native attribute `alt`. | `string` | — | -| `referrer-policy` | native attribute `referrerPolicy`. | `string` | — | -| `preview-src-list` | allow big image preview. | `string[]` | — | -| `z-index` | set image preview z-index. | `number` | — | -| `initial-index` | initial preview image index, less than the length of `url-list`. | `number` | `0` | -| `close-on-press-escape` | whether the image-viewer can be closed by pressing ESC | `boolean` | `true` | -| `preview-teleported` | whether to append image-viewer to body. A nested parent element attribute transform should have this attribute set to `true`. | `boolean` | `false` | +| Attribute | Description | Type | Default | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- | +| `src` | image source, same as native. | `string` | — | +| `fit` | indicate how the image should be resized to fit its container, same as [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit). | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale'-down'` | — | +| `hide-on-click-modal` | when enabling preview, use this flag to control whether clicking on backdrop can exit preview mode. | `boolean` | `false` | +| `loading` | Indicates how the browser should load the image, same as [native](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) | `'eager' \| 'lazy'` | — | +| `lazy` | whether to use lazy load. | `boolean` | `false` | +| `scroll-container` | the container to add scroll listener when using lazy load. | `string \| HTMLElement` | the nearest parent container whose overflow property is auto or scroll. | +| `alt` | native attribute `alt`. | `string` | — | +| `referrer-policy` | native attribute `referrerPolicy`. | `string` | — | +| `preview-src-list` | allow big image preview. | `string[]` | — | +| `z-index` | set image preview z-index. | `number` | — | +| `initial-index` | initial preview image index, less than the length of `url-list`. | `number` | `0` | +| `close-on-press-escape` | whether the image-viewer can be closed by pressing ESC | `boolean` | `true` | +| `preview-teleported` | whether to append image-viewer to body. A nested parent element attribute transform should have this attribute set to `true`. | `boolean` | `false` | ### Image Events diff --git a/packages/components/image/__tests__/image.test.tsx b/packages/components/image/__tests__/image.test.tsx index 075ed81422..af54622dc8 100644 --- a/packages/components/image/__tests__/image.test.tsx +++ b/packages/components/image/__tests__/image.test.tsx @@ -115,6 +115,22 @@ describe('Image.vue', () => { ).not.toContain('display: none') }) + test('native loading attributes', async () => { + const wrapper = mount(Image, { + props: { + src: IMAGE_SUCCESS, + loading: 'eager', + } as ElImageProps, + }) + + await doubleWait() + expect(wrapper.find('img').exists()).toBe(true) + expect(wrapper.find('img').attributes('loading')).toBe('eager') + + await wrapper.setProps({ loading: undefined }) + expect(wrapper.find('img').attributes('loading')).toBe(undefined) + }) + test('$attrs', async () => { const alt = 'this ia alt' const props: ElImageProps = { diff --git a/packages/components/image/src/image.ts b/packages/components/image/src/image.ts index 59a17d3857..39301cbd37 100644 --- a/packages/components/image/src/image.ts +++ b/packages/components/image/src/image.ts @@ -21,6 +21,10 @@ export const imageProps = buildProps({ values: ['', 'contain', 'cover', 'fill', 'none', 'scale-down'], default: '', }, + loading: { + type: String, + values: ['eager', 'lazy'], + }, lazy: { type: Boolean, default: false, diff --git a/packages/components/image/src/image.vue b/packages/components/image/src/image.vue index 63de73eae4..90e3bf03bb 100644 --- a/packages/components/image/src/image.vue +++ b/packages/components/image/src/image.vue @@ -1,19 +1,22 @@ - + + {{ t('el.image.error') }} - () const hasLoadError = ref(false) -const loading = ref(true) -const imgWidth = ref(0) -const imgHeight = ref(0) +const isLoading = ref(true) const showViewer = ref(false) const container = ref() - const _scrollContainer = ref() + +const supportLoading = isClient && 'loading' in HTMLImageElement.prototype let stopScrollListener: (() => void) | undefined let stopWheelListener: (() => void) | undefined @@ -107,49 +110,27 @@ const imageIndex = computed(() => { return previewIndex }) +const isManual = computed(() => { + if (props.loading === 'eager') return false + return (!supportLoading && props.loading === 'lazy') || props.lazy +}) + const loadImage = () => { if (!isClient) return // reset status - loading.value = true + isLoading.value = true hasLoadError.value = false - - const img = new Image() - const currentImageSrc = props.src - - // load & error callbacks are only responsible for currentImageSrc - img.addEventListener('load', (e) => { - if (currentImageSrc !== props.src) { - return - } - handleLoad(e, img) - }) - img.addEventListener('error', (e) => { - if (currentImageSrc !== props.src) { - return - } - handleError(e) - }) - - // bind html attrs - // so it can behave consistently - Object.entries(rawAttrs).forEach(([key, value]) => { - // avoid onload to be overwritten - if (key.toLowerCase() === 'onload') return - img.setAttribute(key, value as string) - }) - img.src = currentImageSrc + imageSrc.value = props.src } -function handleLoad(e: Event, img: HTMLImageElement) { - imgWidth.value = img.width - imgHeight.value = img.height - loading.value = false +function handleLoad() { + isLoading.value = false hasLoadError.value = false } function handleError(event: Event) { - loading.value = false + isLoading.value = false hasLoadError.value = true emit('error', event) } @@ -235,9 +216,9 @@ function switchViewer(val: number) { watch( () => props.src, () => { - if (props.lazy) { + if (isManual.value) { // reset status - loading.value = true + isLoading.value = true hasLoadError.value = false removeLazyLoadListener() addLazyLoadListener() @@ -248,7 +229,7 @@ watch( ) onMounted(() => { - if (props.lazy) { + if (isManual.value) { addLazyLoadListener() } else { loadImage() diff --git a/packages/theme-chalk/src/image.scss b/packages/theme-chalk/src/image.scss index 4b82f54b98..fe50bb905e 100644 --- a/packages/theme-chalk/src/image.scss +++ b/packages/theme-chalk/src/image.scss @@ -6,6 +6,12 @@ height: 100%; } +%position { + position: absolute; + top: 0; + left: 0; +} + @include b(image) { position: relative; display: inline-block; @@ -17,11 +23,13 @@ } @include e(placeholder) { + @extend %position !optional; @extend %size !optional; background: getCssVar('fill-color', 'light'); } @include e(error) { + @extend %position !optional; @extend %size !optional; display: flex; justify-content: center;