From 40084ba4fcd01f56ee850331f7abf3c66ffe0992 Mon Sep 17 00:00:00 2001 From: Mark Florian Date: Thu, 24 Feb 2022 15:57:37 +0000 Subject: [PATCH 1/4] Implement loading icon JS helper This helper should *only* be used in existing legacy areas of code where Vue is not in use, as part of the migration strategy defined in https://gitlab.com/groups/gitlab-org/-/epics/7626. --- .../javascripts/loading_icon_for_legacy_js.js | 38 ++++++++++++++++ .../loading_icon_for_legacy_js_spec.js | 43 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 app/assets/javascripts/loading_icon_for_legacy_js.js create mode 100644 spec/frontend/loading_icon_for_legacy_js_spec.js diff --git a/app/assets/javascripts/loading_icon_for_legacy_js.js b/app/assets/javascripts/loading_icon_for_legacy_js.js new file mode 100644 index 00000000000000..602ecf0fe3eb02 --- /dev/null +++ b/app/assets/javascripts/loading_icon_for_legacy_js.js @@ -0,0 +1,38 @@ +import { __ } from '~/locale'; + +const baseCSSClass = 'gl-spinner'; + +/** + * Returns a loading icon/spinner element. + * + * This should *only* be used in existing legacy areas of code where Vue is not + * in use, as part of the migration strategy defined in + * https://gitlab.com/groups/gitlab-org/-/epics/7626. + * + * @param {object} props - The props to configure the spinner. + * @param {boolean} inline - Display the spinner inline; otherwise, as a block. + * @param {string} color - The color of the spinner ('dark' or 'light') + * @param {string} size - The size of the spinner ('sm', 'md', 'lg', 'xl') + * @param {string[]} classes - Additional classes to apply to the element. + * @param {string} label - The ARIA label to apply to the spinner. + * @returns {HTMLElement} + */ +export const loadingIconForLegacyJS = ({ + inline = false, + color = 'dark', + size = 'sm', + classes = [], + label = __('Loading'), +} = {}) => { + const container = document.createElement(inline ? 'span' : 'div'); + container.classList.add(`${baseCSSClass}-container`, ...classes); + container.setAttribute('role', 'status'); + + const spinner = document.createElement('span'); + spinner.classList.add(baseCSSClass, `${baseCSSClass}-${color}`, `${baseCSSClass}-${size}`); + spinner.setAttribute('aria-label', label); + + container.appendChild(spinner); + + return container; +}; diff --git a/spec/frontend/loading_icon_for_legacy_js_spec.js b/spec/frontend/loading_icon_for_legacy_js_spec.js new file mode 100644 index 00000000000000..46deee555ba8be --- /dev/null +++ b/spec/frontend/loading_icon_for_legacy_js_spec.js @@ -0,0 +1,43 @@ +import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js'; + +describe('loadingIconForLegacyJS', () => { + it('sets the correct defaults', () => { + const el = loadingIconForLegacyJS(); + + expect(el.tagName).toBe('DIV'); + expect(el.className).toBe('gl-spinner-container'); + expect(el.querySelector('.gl-spinner-sm')).toEqual(expect.any(HTMLElement)); + expect(el.querySelector('.gl-spinner-dark')).toEqual(expect.any(HTMLElement)); + expect(el.querySelector('[aria-label="Loading"]')).toEqual(expect.any(HTMLElement)); + expect(el.getAttribute('role')).toBe('status'); + }); + + it('renders a span if inline = true', () => { + expect(loadingIconForLegacyJS({ inline: true }).tagName).toBe('SPAN'); + }); + + it('can render a different size', () => { + const el = loadingIconForLegacyJS({ size: 'lg' }); + + expect(el.querySelector('.gl-spinner-lg')).toEqual(expect.any(HTMLElement)); + }); + + it('can render a different color', () => { + const el = loadingIconForLegacyJS({ color: 'light' }); + + expect(el.querySelector('.gl-spinner-light')).toEqual(expect.any(HTMLElement)); + }); + + it('can render a different aria-label', () => { + const el = loadingIconForLegacyJS({ label: 'Foo' }); + + expect(el.querySelector('[aria-label="Foo"]')).toEqual(expect.any(HTMLElement)); + }); + + it('can render additional classes', () => { + const classes = ['foo', 'bar']; + const el = loadingIconForLegacyJS({ classes }); + + expect(el.classList).toContain(...classes); + }); +}); -- GitLab From d232ccd52069abbb81e0bcc8a403704a5c7338d9 Mon Sep 17 00:00:00 2001 From: Mark Florian Date: Thu, 24 Feb 2022 18:02:42 +0000 Subject: [PATCH 2/4] Delegate to GlLoadingIcon for rendering This approach ensures that the spinner markup will stay in sync with how GlLoadingIcon is defined in `@gitlab/ui`, including its props' defaults. --- .../javascripts/loading_icon_for_legacy_js.js | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/loading_icon_for_legacy_js.js b/app/assets/javascripts/loading_icon_for_legacy_js.js index 602ecf0fe3eb02..574257ec262db0 100644 --- a/app/assets/javascripts/loading_icon_for_legacy_js.js +++ b/app/assets/javascripts/loading_icon_for_legacy_js.js @@ -1,6 +1,8 @@ +import Vue from 'vue'; +import { GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -const baseCSSClass = 'gl-spinner'; +const defaultValue = (prop) => GlLoadingIcon.props[prop]?.default; /** * Returns a loading icon/spinner element. @@ -18,21 +20,34 @@ const baseCSSClass = 'gl-spinner'; * @returns {HTMLElement} */ export const loadingIconForLegacyJS = ({ - inline = false, - color = 'dark', - size = 'sm', + inline = defaultValue('inline'), + color = defaultValue('color'), + size = defaultValue('size'), classes = [], label = __('Loading'), } = {}) => { - const container = document.createElement(inline ? 'span' : 'div'); - container.classList.add(`${baseCSSClass}-container`, ...classes); - container.setAttribute('role', 'status'); + const mountEl = document.createElement('div'); - const spinner = document.createElement('span'); - spinner.classList.add(baseCSSClass, `${baseCSSClass}-${color}`, `${baseCSSClass}-${size}`); - spinner.setAttribute('aria-label', label); + const vm = new Vue({ + el: mountEl, + render(h) { + return h(GlLoadingIcon, { + class: classes, + props: { + inline, + color, + size, + label, + }, + }); + }, + }); - container.appendChild(spinner); + // Ensure it's rendered + vm.$forceUpdate(); - return container; + const el = vm.$el.cloneNode(true); + vm.$destroy(); + + return el; }; -- GitLab From 4c63009b296e536cc281b79bc0f0195267a2f91a Mon Sep 17 00:00:00 2001 From: Mark Florian Date: Thu, 24 Feb 2022 16:56:15 +0000 Subject: [PATCH 3/4] Fix loading icon in activity calendar The loading icon/spinner wasn't visible, as the `spinner` CSS class was removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58706. Changelog: fixed --- .../javascripts/pages/users/activity_calendar.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 7f4e79976bcce6..996e12bc105c5a 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -7,6 +7,7 @@ import axios from '~/lib/utils/axios_utils'; import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime/date_format_utility'; import { n__, s__, __ } from '~/locale'; +import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js'; const d3 = { select }; @@ -24,12 +25,6 @@ const CONTRIB_LEGENDS = [ { title: __('30+ contributions'), min: 30 }, ]; -const LOADING_HTML = ` -
-
-
-`; - function getSystemDate(systemUtcOffsetSeconds) { const date = new Date(); const localUtcOffsetMinutes = 0 - date.getTimezoneOffset(); @@ -286,7 +281,9 @@ export default class ActivityCalendar { this.currentSelectedDate.getDate(), ].join('-'); - $(this.activitiesContainer).html(LOADING_HTML); + $(this.activitiesContainer) + .empty() + .append(loadingIconForLegacyJS({ size: 'lg' })); axios .get(this.calendarActivitiesPath, { -- GitLab From c2381cc90c4b8f4f419a9c7cc34577e84af9ae66 Mon Sep 17 00:00:00 2001 From: Jannik Lehmann Date: Mon, 28 Feb 2022 10:50:50 +0000 Subject: [PATCH 4/4] Use correct JSDoc syntax for argument properties --- app/assets/javascripts/loading_icon_for_legacy_js.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/loading_icon_for_legacy_js.js b/app/assets/javascripts/loading_icon_for_legacy_js.js index 574257ec262db0..d50a4275424219 100644 --- a/app/assets/javascripts/loading_icon_for_legacy_js.js +++ b/app/assets/javascripts/loading_icon_for_legacy_js.js @@ -12,11 +12,11 @@ const defaultValue = (prop) => GlLoadingIcon.props[prop]?.default; * https://gitlab.com/groups/gitlab-org/-/epics/7626. * * @param {object} props - The props to configure the spinner. - * @param {boolean} inline - Display the spinner inline; otherwise, as a block. - * @param {string} color - The color of the spinner ('dark' or 'light') - * @param {string} size - The size of the spinner ('sm', 'md', 'lg', 'xl') - * @param {string[]} classes - Additional classes to apply to the element. - * @param {string} label - The ARIA label to apply to the spinner. + * @param {boolean} props.inline - Display the spinner inline; otherwise, as a block. + * @param {string} props.color - The color of the spinner ('dark' or 'light') + * @param {string} props.size - The size of the spinner ('sm', 'md', 'lg', 'xl') + * @param {string[]} props.classes - Additional classes to apply to the element. + * @param {string} props.label - The ARIA label to apply to the spinner. * @returns {HTMLElement} */ export const loadingIconForLegacyJS = ({ -- GitLab