diff --git a/create-quasar/package.json b/create-quasar/package.json index fb969105170..c22a8d7704d 100644 --- a/create-quasar/package.json +++ b/create-quasar/package.json @@ -1,6 +1,6 @@ { "name": "create-quasar", - "version": "2.0.0", + "version": "2.0.1", "description": "Scaffolds Quasar Apps, AppExtensions or UI kits", "type": "module", "author": { diff --git a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json index 1a72db6f6e2..50910f108a2 100644 --- a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json +++ b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json @@ -30,7 +30,7 @@ "eslint-plugin-vue": "^9.30.0", "globals": "^15.12.0", "vue-tsc": "^2.0.29", - "@vue/eslint-config-typescript": "^14.1.3", + "@vue/eslint-config-typescript": "^14.4.0", "vite-plugin-checker": "^0.8.0", <% } %> <% if (prettier) { %> diff --git a/create-quasar/templates/app/quasar-v2/ts-vite-2/eslint/_eslint.config.js b/create-quasar/templates/app/quasar-v2/ts-vite-2/eslint/_eslint.config.js index 24320bf8e41..6ef1fd3ca18 100644 --- a/create-quasar/templates/app/quasar-v2/ts-vite-2/eslint/_eslint.config.js +++ b/create-quasar/templates/app/quasar-v2/ts-vite-2/eslint/_eslint.config.js @@ -2,10 +2,10 @@ import js from '@eslint/js' import globals from 'globals' import pluginVue from 'eslint-plugin-vue' import pluginQuasar from '@quasar/app-vite/eslint' -import vueTsEslintConfig from '@vue/eslint-config-typescript'<% if (prettier) { %> +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'<% if (prettier) { %> import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting'<% } %> -export default [ +export default defineConfigWithVueTs( { /** * Ignore the following files. @@ -18,7 +18,7 @@ export default [ // ignores: [] }, - ...pluginQuasar.configs.recommended(), + pluginQuasar.configs.recommended(), js.configs.recommended, /** @@ -33,7 +33,7 @@ export default [ * pluginVue.configs["flat/recommended"] * -> Above, plus rules to enforce subjective community defaults to ensure consistency. */ - ...pluginVue.configs[ 'flat/essential' ], + pluginVue.configs[ 'flat/essential' ], { files: ['**/*.ts', '**/*.vue'], @@ -45,20 +45,7 @@ export default [ } }, // https://github.com/vuejs/eslint-config-typescript - ...vueTsEslintConfig({ - // Optional: extend additional configurations from typescript-eslint'. - // Supports all the configurations in - // https://typescript-eslint.io/users/configs#recommended-configurations - extends: [ - // By default, only the 'recommendedTypeChecked' rules are enabled. - 'recommendedTypeChecked' - // You can also manually enable the stylistic rules. - // "stylistic", - - // Other utility configurations, such as 'eslintRecommended', (note that it's in camelCase) - // are also extendable here. But we don't recommend using them directly. - ] - }), + vueTsConfigs.recommendedTypeChecked, { languageOptions: { @@ -96,4 +83,4 @@ export default [ }<% if (prettier) { %>, prettierSkipFormatting<% } %> -] +) diff --git a/create-quasar/templates/app/quasar-v2/ts-webpack-4/BASE/_package.json b/create-quasar/templates/app/quasar-v2/ts-webpack-4/BASE/_package.json index b8a8e904285..b5055ef6404 100644 --- a/create-quasar/templates/app/quasar-v2/ts-webpack-4/BASE/_package.json +++ b/create-quasar/templates/app/quasar-v2/ts-webpack-4/BASE/_package.json @@ -30,7 +30,7 @@ "eslint": "^9.14.0", "eslint-plugin-vue": "^9.30.0", "eslint-webpack-plugin": "^4.2.0", - "@vue/eslint-config-typescript": "^14.1.3", + "@vue/eslint-config-typescript": "^14.4.0", "globals": "^15.12.0", <% } %> <% if (prettier) { %> diff --git a/create-quasar/templates/app/quasar-v2/ts-webpack-4/eslint/_eslint.config.js b/create-quasar/templates/app/quasar-v2/ts-webpack-4/eslint/_eslint.config.js index 1da668fd247..30bcc8e06c3 100644 --- a/create-quasar/templates/app/quasar-v2/ts-webpack-4/eslint/_eslint.config.js +++ b/create-quasar/templates/app/quasar-v2/ts-webpack-4/eslint/_eslint.config.js @@ -2,10 +2,10 @@ import js from '@eslint/js' import globals from 'globals' import pluginVue from 'eslint-plugin-vue' import pluginQuasar from '@quasar/app-webpack/eslint' -import vueTsEslintConfig from '@vue/eslint-config-typescript'<% if (prettier) { %> +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'<% if (prettier) { %> import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting'<% } %> -export default [ +export default defineConfigWithVueTs( { /** * Ignore the following files. @@ -18,7 +18,7 @@ export default [ // ignores: [] }, - ...pluginQuasar.configs.recommended(), + pluginQuasar.configs.recommended(), js.configs.recommended, /** @@ -33,7 +33,7 @@ export default [ * pluginVue.configs["flat/recommended"] * -> Above, plus rules to enforce subjective community defaults to ensure consistency. */ - ...pluginVue.configs[ 'flat/essential' ], + pluginVue.configs[ 'flat/essential' ], // this rule needs to be above the vueTsEslintConfig to avoid error: 'You have used a rule which requires type information, but don't have parserOptions set to generate type information for this file.' { @@ -46,20 +46,7 @@ export default [ } }, // https://github.com/vuejs/eslint-config-typescript - ...vueTsEslintConfig({ - // Optional: extend additional configurations from typescript-eslint'. - // Supports all the configurations in - // https://typescript-eslint.io/users/configs#recommended-configurations - extends: [ - // By default, only the 'recommendedTypeChecked' rules are enabled. - 'recommendedTypeChecked' - // You can also manually enable the stylistic rules. - // "stylistic", - - // Other utility configurations, such as 'eslintRecommended', (note that it's in camelCase) - // are also extendable here. But we don't recommend using them directly. - ] - }), + vueTsConfigs.recommendedTypeChecked, { languageOptions: { @@ -97,4 +84,4 @@ export default [ }<% if (prettier) { %>, prettierSkipFormatting<% } %> -] +) diff --git a/docs/src/assets/docs-homepage.js b/docs/src/assets/docs-homepage.js index a77aff158f8..45142aff97a 100644 --- a/docs/src/assets/docs-homepage.js +++ b/docs/src/assets/docs-homepage.js @@ -8,7 +8,7 @@ import { mdiImageSizeSelectSmall, mdiTable, mdiHumanMaleBoard -} from '@quasar/extras/mdi-v6' +} from '@quasar/extras/mdi-v7' export const mostUsedPages = [ { diff --git a/docs/src/assets/links.header.js b/docs/src/assets/links.header.js index b68fa218d17..52a6eaa71a4 100644 --- a/docs/src/assets/links.header.js +++ b/docs/src/assets/links.header.js @@ -5,7 +5,7 @@ import { fasCubes, fasBolt } from '@quasar/extras/fontawesome-v5' import { mdiBug, mdiClipboardText, mdiCodepen, mdiFlare, mdiFlask, mdiGithub, mdiJsfiddle, mdiPaletteSwatch, mdiPuzzle, mdiShoppingMusic, mdiStarCircle, mdiThemeLightDark, mdiViewDashboard, mdiSecurity, mdiMathIntegralBox -} from '@quasar/extras/mdi-v6' +} from '@quasar/extras/mdi-v7' import { socialLinks } from './links.social.js' diff --git a/docs/src/assets/links.integrations.js b/docs/src/assets/links.integrations.js index 46765f85573..4fd39054bb1 100644 --- a/docs/src/assets/links.integrations.js +++ b/docs/src/assets/links.integrations.js @@ -10,7 +10,7 @@ import { mdiServer, mdiStarCircle, mdiSvg, mdiTelevisionPlay, mdiTestTube -} from '@quasar/extras/mdi-v6' +} from '@quasar/extras/mdi-v7' export const platformIcons = [ 'img:https://cdn.quasar.dev/img/custom-svg-icons/chrome.svg', diff --git a/docs/src/assets/links.social.js b/docs/src/assets/links.social.js index 5d7d46a47bc..73a49f5358d 100644 --- a/docs/src/assets/links.social.js +++ b/docs/src/assets/links.social.js @@ -1,12 +1,12 @@ -import { fabFacebook, fabGithub, fabXTwitter } from '@quasar/extras/fontawesome-v6' -import { mdiDiscord, mdiForum } from '@quasar/extras/mdi-v6' +import { fabFacebook, fabGithub, fabXTwitter, fabDiscord } from '@quasar/extras/fontawesome-v6' +import { mdiForum } from '@quasar/extras/mdi-v7' export const socialLinks = { name: 'Social', mq: 1310, children: [ { name: 'Github', icon: fabGithub, path: 'https://github.quasar.dev', external: true }, - { name: 'Discord', icon: mdiDiscord, path: 'https://chat.quasar.dev', external: true }, + { name: 'Discord', icon: fabDiscord, path: 'https://chat.quasar.dev', external: true }, { name: 'Forum', icon: mdiForum, path: 'https://forum.quasar.dev', external: true }, { name: 'X (Twitter)', icon: fabXTwitter, path: 'https://twitter.quasar.dev', external: true }, { name: 'Facebook', icon: fabFacebook, path: 'https://facebook.quasar.dev', external: true } diff --git a/docs/src/components/DocApi.vue b/docs/src/components/DocApi.vue index 6a82c3bf48f..93b31776358 100644 --- a/docs/src/components/DocApi.vue +++ b/docs/src/components/DocApi.vue @@ -81,7 +81,7 @@ diff --git a/docs/src/examples/QTable/CustomColor.vue b/docs/src/examples/QTable/CustomColor.vue index fb510de1465..f47499c46d6 100644 --- a/docs/src/examples/QTable/CustomColor.vue +++ b/docs/src/examples/QTable/CustomColor.vue @@ -2,9 +2,11 @@
diff --git a/docs/src/layouts/doc-layout/search/ResultError.vue b/docs/src/layouts/doc-layout/search/ResultError.vue index f9c10dbdb08..71b2d129430 100644 --- a/docs/src/layouts/doc-layout/search/ResultError.vue +++ b/docs/src/layouts/doc-layout/search/ResultError.vue @@ -7,5 +7,5 @@ diff --git a/docs/src/layouts/doc-layout/search/SearchResults.vue b/docs/src/layouts/doc-layout/search/SearchResults.vue index fdfcc47ca0e..784e8437a6b 100644 --- a/docs/src/layouts/doc-layout/search/SearchResults.vue +++ b/docs/src/layouts/doc-layout/search/SearchResults.vue @@ -24,7 +24,7 @@ diff --git a/docs/src/pages/layout/grid/flex-playground/FlexChild.vue b/docs/src/pages/layout/grid/flex-playground/FlexChild.vue index b2754a1cb22..882b9bfd9b4 100644 --- a/docs/src/pages/layout/grid/flex-playground/FlexChild.vue +++ b/docs/src/pages/layout/grid/flex-playground/FlexChild.vue @@ -28,7 +28,7 @@ ``` @@ -154,7 +154,7 @@ import { fasFont } from '@quasar/extras/fontawesome-v5' ```html diff --git a/ui/src/components/menu/__tests__/WrapperTwo.vue b/ui/src/components/menu/__tests__/WrapperTwo.vue deleted file mode 100644 index 599c7b9655f..00000000000 --- a/ui/src/components/menu/__tests__/WrapperTwo.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/ui/src/components/pagination/QPagination.js b/ui/src/components/pagination/QPagination.js index 2fd8fc299cb..bacf3fb0cfd 100644 --- a/ui/src/components/pagination/QPagination.js +++ b/ui/src/components/pagination/QPagination.js @@ -317,7 +317,8 @@ export default createComponent({ getBtn({ key: 'bls', disable: props.disable || props.modelValue <= minProp.value, - icon: icons.value[ 0 ] + icon: icons.value[ 0 ], + 'aria-label': $q.lang.pagination.first }, minProp.value) ) @@ -325,7 +326,8 @@ export default createComponent({ getBtn({ key: 'ble', disable: props.disable || props.modelValue >= maxProp.value, - icon: icons.value[ 3 ] + icon: icons.value[ 3 ], + 'aria-label': $q.lang.pagination.last }, maxProp.value) ) } @@ -335,7 +337,8 @@ export default createComponent({ getBtn({ key: 'bdp', disable: props.disable || props.modelValue <= minProp.value, - icon: icons.value[ 1 ] + icon: icons.value[ 1 ], + 'aria-label': $q.lang.pagination.prev }, props.modelValue - 1) ) @@ -343,7 +346,8 @@ export default createComponent({ getBtn({ key: 'bdn', disable: props.disable || props.modelValue >= maxProp.value, - icon: icons.value[ 2 ] + icon: icons.value[ 2 ], + 'aria-label': $q.lang.pagination.next }, props.modelValue + 1) ) } diff --git a/ui/src/components/parallax/QParallax.js b/ui/src/components/parallax/QParallax.js index c9ec464cea7..48a58df42b2 100644 --- a/ui/src/components/parallax/QParallax.js +++ b/ui/src/components/parallax/QParallax.js @@ -134,7 +134,7 @@ export default createComponent({ onBeforeUnmount(() => { stop() - observer !== void 0 && observer.disconnect() + observer?.disconnect() mediaEl.onload = mediaEl.onloadstart = mediaEl.loadedmetadata = null }) diff --git a/ui/src/components/popup-edit/QPopupEdit.js b/ui/src/components/popup-edit/QPopupEdit.js index c6a8f9a854b..9efc783ab3b 100644 --- a/ui/src/components/popup-edit/QPopupEdit.js +++ b/ui/src/components/popup-edit/QPopupEdit.js @@ -163,8 +163,8 @@ export default createComponent({ Object.assign(proxy, { set, cancel, - show (e) { menuRef.value !== null && menuRef.value.show(e) }, - hide (e) { menuRef.value !== null && menuRef.value.hide(e) }, + show (e) { menuRef.value?.show(e) }, + hide (e) { menuRef.value?.hide(e) }, updatePosition }) diff --git a/ui/src/components/pull-to-refresh/QPullToRefresh.js b/ui/src/components/pull-to-refresh/QPullToRefresh.js index 0bef55d5ca9..2ac570043f1 100644 --- a/ui/src/components/pull-to-refresh/QPullToRefresh.js +++ b/ui/src/components/pull-to-refresh/QPullToRefresh.js @@ -149,7 +149,7 @@ export default createComponent({ timer = setTimeout(() => { timer = null animating.value = false - done && done() + done?.() }, 300) } diff --git a/ui/src/components/rating/QRating.sass b/ui/src/components/rating/QRating.sass index eb5878bd6b8..f96ecb3ce58 100644 --- a/ui/src/components/rating/QRating.sass +++ b/ui/src/components/rating/QRating.sass @@ -14,7 +14,7 @@ text-shadow: $rating-shadow position: relative opacity: .4 - transition: transform .2s ease-in, opacity .2s ease-in + transition: transform .2s ease-in, opacity .2s ease-in, color .2s ease-in &--hovered transform: scale(1.3) diff --git a/ui/src/components/resize-observer/QResizeObserver.js b/ui/src/components/resize-observer/QResizeObserver.js index c889c61820b..8b97cbe4cac 100644 --- a/ui/src/components/resize-observer/QResizeObserver.js +++ b/ui/src/components/resize-observer/QResizeObserver.js @@ -117,7 +117,7 @@ export default createComponent({ function onObjLoad () { cleanup() - if (targetEl && targetEl.contentDocument) { + if (targetEl?.contentDocument) { curDocView = targetEl.contentDocument.defaultView curDocView.addEventListener('resize', trigger, listenOpts.passive) emitEvent() diff --git a/ui/src/components/scroll-observer/QScrollObserver.js b/ui/src/components/scroll-observer/QScrollObserver.js index 5d49315a3e9..a011386f411 100644 --- a/ui/src/components/scroll-observer/QScrollObserver.js +++ b/ui/src/components/scroll-observer/QScrollObserver.js @@ -53,7 +53,7 @@ export default createComponent({ }) function emitEvent () { - clearTimer !== null && clearTimer() + clearTimer?.() const top = Math.max(0, getVerticalScrollPosition(localScrollTarget)) const left = getHorizontalScrollPosition(localScrollTarget) @@ -123,7 +123,7 @@ export default createComponent({ }) onBeforeUnmount(() => { - clearTimer !== null && clearTimer() + clearTimer?.() unconfigureScrollTarget() }) diff --git a/ui/src/components/select/QSelect.js b/ui/src/components/select/QSelect.js index 42e4446d797..2f06dcc4724 100644 --- a/ui/src/components/select/QSelect.js +++ b/ui/src/components/select/QSelect.js @@ -273,7 +273,7 @@ export default createComponent({ const needsHtmlFn = computed(() => ( props.optionsHtml === true ? () => true - : opt => opt !== void 0 && opt !== null && opt.html === true + : opt => opt?.html === true )) const valueAsHtml = computed(() => ( @@ -533,7 +533,7 @@ export default createComponent({ hidePopup() } - targetRef.value !== null && targetRef.value.focus() + targetRef.value?.focus() if ( innerValue.value.length === 0 @@ -545,7 +545,9 @@ export default createComponent({ return } - (hasDialog !== true || dialogFieldFocused.value === true) && state.focus() + if (hasDialog !== true || dialogFieldFocused.value === true) { + state.focus() + } selectInputText() @@ -894,7 +896,7 @@ export default createComponent({ fn(val, mode === 'add-unique') if (props.multiple !== true) { - targetRef.value !== null && targetRef.value.focus() + targetRef.value?.focus() hidePopup() } } @@ -1214,7 +1216,7 @@ export default createComponent({ function onDialogFieldFocus (e) { stop(e) - targetRef.value !== null && targetRef.value.focus() + targetRef.value?.focus() dialogFieldFocused.value = true window.scrollTo(window.pageXOffset || window.scrollX || document.body.scrollLeft || 0, 0) } @@ -1511,7 +1513,7 @@ export default createComponent({ if (hasDialog !== true && menu.value === true) { closeMenu() - targetRef.value !== null && targetRef.value.focus() + targetRef.value?.focus() return } diff --git a/ui/src/components/select/QSelect.json b/ui/src/components/select/QSelect.json index 65c39d49c19..4e282fff5f1 100644 --- a/ui/src/components/select/QSelect.json +++ b/ui/src/components/select/QSelect.json @@ -260,7 +260,7 @@ "fill-input": { "type": "Boolean", - "desc": "Fills the input with current value; Useful along with 'hide-selected'; Does NOT works along with 'multiple' selection", + "desc": "Fills the input with current value; Useful along with 'hide-selected'; Does NOT work along with 'multiple' selection", "category": "behavior" }, diff --git a/ui/src/components/select/__tests__/QSelect.cy.js b/ui/src/components/select/__tests__/QSelect.cy.js deleted file mode 100644 index eb40d27cffc..00000000000 --- a/ui/src/components/select/__tests__/QSelect.cy.js +++ /dev/null @@ -1,2018 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import { vModelAdapter } from '@quasar/quasar-app-extension-testing-e2e-cypress' -import { h, ref } from 'vue' -import QSelect from '../QSelect.js' - -function getHostElement (extendedSelector = '') { - // A majority of tests click on the select, so we ensure the select is visible before the click happens. - // See https://github.com/cypress-io/cypress/discussions/14889 - // See https://www.cypress.io/blog/2020/08/17/when-can-the-test-navigate#visible-elements - return extendedSelector ? cy.get(`.q-select ${ extendedSelector }`) : cy.get('.q-select').should('be.visible') -} - -function mountQSelect (options = {}) { - if (!options.props?.modelValue) { - options.props = { - modelValue: null, - ...options.props ?? {} - } - } - - return cy.mount(QSelect, options) -} - -// QSelect does not set the `data-cy` attribute on the root element, but on the `.q-field__native` element -// This means we cannot use data-cy everywhere, but instead use a custom class `select-root` for this purpose -describe('QSelect API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): fill-input', () => { - it.skip(' ', () => { - // - }) - }) - describe('(prop): new-value-mode', () => { - it.skip(' ', () => { - // - }) - }) - describe('(prop): autocomplete', () => { - it.skip(' ', () => { - // - }) - }) - describe('(prop): transition-show', () => { - it.skip(' ', () => { - // - }) - }) - describe('(prop): transition-hide', () => { - it.skip(' ', () => { - // - }) - }) - describe('(prop): transition-duration', () => { - it.skip(' ', () => { - // - }) - }) - describe('(prop): behavior', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: content', () => { - describe('(prop): dropdown-icon', () => { - it('should use the dropdown-icon supplied', () => { - const icon = 'map' - mountQSelect({ - props: { - dropdownIcon: icon - } - }) - getHostElement() - .get(`div:contains(${ icon })`) - .should('exist') - }) - }) - - describe('(prop): use-input', () => { - it('should render an input inside the select', () => { - mountQSelect({ - props: { - useInput: true - } - }) - getHostElement() - .get('input') - .should('exist') - }) - - it('should render an input, but it shouldn\'t be visible', () => { - mountQSelect() - - getHostElement() - .get('input') - .should('not.be.visible') - }) - - it.skip('should not render an input by default', () => { - // Native input is now always rendered, due to having a target for autocomplete - // Refer to commit: https://github.com/quasarframework/quasar/commit/21a3af0dfe01bac0da617737562b599edee397a2 - }) - }) - - describe('(prop): input-debounce', () => { - it('should use an input-debounce of 500ms by default', () => { - const fn = cy.stub() - const text = 'Hello there' - mountQSelect({ - props: { - useInput: true, - onFilter: fn - } - }) - getHostElement() - .get('input') - .type(text) - .then(() => { - expect(fn).not.to.be.calledWith(text) - }) - .wait(500) - .then(() => { - expect(fn).to.be.calledWith(text) - }) - }) - - it('should use a custom input-debounce', () => { - const fn = cy.stub() - const text = 'Hello there' - mountQSelect({ - props: { - useInput: true, - onFilter: fn, - inputDebounce: 800 - } - }) - getHostElement() - .get('input') - .type(text) - .wait(500) - .then(() => { - expect(fn).not.to.be.calledWith(text) - }) - .wait(300) - .then(() => { - expect(fn).to.be.calledWith(text) - }) - }) - }) - }) - - describe('Category: content|behavior', () => { - describe('(prop): hide-dropdown-icon', () => { - it('should show the dropdown-icon when this property is not supplied', () => { - mountQSelect() - getHostElement() - .get('.q-icon') - .should('exist') - }) - - it('should hide the dropdown-icon when this property is true', () => { - mountQSelect({ - props: { - hideDropdownIcon: true - } - }) - getHostElement() - .get('.q-icon') - .should('not.exist') - }) - }) - }) - - describe('Category: general', () => { - describe('(prop): tabindex', () => { - it('should have a default tabindex of 0', () => { - mountQSelect() - getHostElement('[tabindex="0"]') - .should('exist') - }) - - it('should set the tabindex to the supplied value', () => { - const tabindex = 2 - mountQSelect({ - props: { - tabindex - } - }) - getHostElement(`[tabindex="${ tabindex }"]`) - .should('exist') - getHostElement('[tabindex="0"]') - .should('not.exist') - }) - }) - }) - - describe('Category: model', () => { - describe('(prop): model-value', () => { - it('should have the option selected passed in the model-value', () => { - const modelValue = 'Option 1' - mountQSelect({ - props: { - modelValue, - options: [ 'Option 1', 'Option 2', 'Option 3' ] - } - }) - getHostElement() - .should('include.text', modelValue) - }) - }) - - describe('(prop): emit-value', () => { - it('should emit the value under the value key, if options are objects', () => { - const fn = cy.stub() - mountQSelect({ - props: { - emitValue: true, - 'onUpdate:modelValue': fn, - options: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 } ] - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains('Option 1') - .should('be.visible') - .click() - cy.get('.q-menu') - .then(() => { - expect(fn).to.have.been.calledWith(1) - }) - }) - - it('should emit the whole object by default if options are objects', () => { - const fn = cy.stub() - const options = [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 } ] - mountQSelect({ - props: { - 'onUpdate:modelValue': fn, - options - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains('Option 1') - .should('be.visible') - .click() - cy.get('.q-menu') - .then(() => { - expect(fn).to.have.been.calledWith(options[ 0 ]) - }) - }) - }) - }) - - describe('Category: model|selection', () => { - describe('(prop): multiple', () => { - it('should select only one option by default', () => { - const options = [ 'Option 1', 'Option 2' ] - const model = ref(null) - mountQSelect({ - props: { - ...vModelAdapter(model), - options - } - }) - - getHostElement().click() - cy.withinSelectMenu(() => { - cy.contains('Option 1') - .should('be.visible') - .click() - cy.contains('Option 1') - .then(() => { - expect(model.value).to.equal(options[ 0 ]) - }) - }) - - getHostElement().click() - cy.withinSelectMenu(() => { - cy.contains('Option 2') - .should('be.visible') - .click() - cy.contains('Option 2') - .then(() => { - expect(model.value).to.equal(options[ 1 ]) - }) - }) - }) - - it('should select multiple options if multiple is true', () => { - const options = [ 'Option 1', 'Option 2' ] - const model = ref([]) - mountQSelect({ - props: { - ...vModelAdapter(model), - multiple: true, - options - } - }) - - getHostElement().click() - cy.withinSelectMenu({ - persistent: true, - fn: () => { - cy.contains('Option 1') - .should('be.visible') - .click() - cy.contains('Option 1') - .then(() => { - expect(model.value).to.eql([ options[ 0 ] ]) - }) - - cy.contains('Option 2') - .should('be.visible') - .click() - cy.contains('Option 2') - .then(() => { - expect(model.value).to.eql(options) - }) - } - }) - }) - }) - }) - - describe('Category: options', () => { - describe('(prop): options', () => { - it('should show each option when opening the dropdown', () => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .children() - .should('contain', options[ 0 ]) - .and('contain', options[ 1 ]) - .and('contain', options[ 2 ]) - .and('contain', options[ 3 ]) - }) - }) - - describe('(prop): option-value', () => { - it('should use the value key as option-value by default', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - const model = ref(null) - mountQSelect({ - props: { - ...vModelAdapter(model), - options, - emitValue: true - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains(options[ 0 ].label) - .should('be.visible') - .click() - cy.get('.q-menu') - .then(() => { - expect(model.value).to.equal(options[ 0 ].value) - }) - }) - - it('should use a custom key supplied by option-value', () => { - const options = [ { label: 'Option one', test: 1 }, { label: 'Option two', test: 2 } ] - const model = ref(null) - mountQSelect({ - props: { - ...vModelAdapter(model), - options, - emitValue: true, - optionValue: 'test' - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains(options[ 0 ].label) - .should('be.visible') - .click() - cy.get('.q-menu') - .then(() => { - expect(model.value).to.equal(options[ 0 ].test) - }) - }) - - it('should accept a function as option-value', () => { - const options = [ { label: 'Option one', test: 1 }, { label: 'Option two', test: 2 } ] - const model = ref(null) - mountQSelect({ - props: { - ...vModelAdapter(model), - options, - emitValue: true, - optionValue: (val) => val.test - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains(options[ 0 ].label) - .should('be.visible') - .click() - cy.get('.q-menu') - .then(() => { - expect(model.value).to.equal(options[ 0 ].test) - }) - }) - }) - - describe('(prop): option-label', () => { - it('should use the "label" key by default as option-label', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .children() - .should('contain', options[ 0 ].label) - .and('contain', options[ 1 ].label) - }) - - it('should use the key supplied by option-label', () => { - const options = [ { test: 'Option one', value: 1 }, { test: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - optionLabel: 'test' - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .children() - .should('contain', options[ 0 ].test) - .and('contain', options[ 1 ].test) - }) - - it('should accept a function as option-label', () => { - const options = [ { test: 'Option one', value: 1 }, { test: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - optionLabel: (item) => (item === null ? 'Null' : item.test) - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .children() - .should('contain', options[ 0 ].test) - .and('contain', options[ 1 ].test) - }) - }) - describe('(prop): option-disable', () => { - it('should use the "disable" key by default as option-disable', () => { - const options = [ { label: 'Option one', value: 1, disable: true }, { label: 'Option two', value: 2, disable: true } ] - mountQSelect({ - props: { - options - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('[role="option"][aria-disabled="true"]') - .should('have.length', 2) - }) - - it('should use the key supplied by option-disable', () => { - const options = [ { label: 'Option one', value: 1, test: true }, { label: 'Option two', value: 2, disable: true } ] - mountQSelect({ - props: { - options, - optionDisable: 'test' - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('[role="option"][aria-disabled="true"]') - .should('have.length', 1) - .should('have.text', options[ 0 ].label) - }) - - it('should accept a function as option-disable', () => { - const options = [ { label: 'Option one', value: 1, test: true }, { label: 'Option two', value: 2, disable: true } ] - mountQSelect({ - props: { - options, - optionDisable: (item) => (item === null ? true : item.test) - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('[role="option"][aria-disabled="true"]') - .should('have.length', 1) - .should('have.text', options[ 0 ].label) - }) - }) - - describe('(prop): options-dense', () => { - it('should show options list dense', () => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options, - optionsDense: true - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('.q-item') - .should('have.class', 'q-item--dense') - }) - }) - - describe('(prop): options-dark', () => { - it('should show options list in dark mode', () => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options, - optionsDark: true - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('.q-item') - .should('have.class', 'q-item--dark') - }) - }) - - describe('(prop): options-selected-class', () => { - it('should have text-{color} applied as selected by default', () => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options, - modelValue: 'Option 1', - color: 'orange' - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains('[role="option"]', options[ 0 ]) - .should('have.class', 'text-orange') - }) - - it('should not have default active class when passed option is empty', () => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options, - modelValue: 'Option 1', - optionsSelectedClass: '', - color: 'orange' - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains('[role="option"]', options[ 0 ]) - .should('not.have.class', 'text-orange') - }) - - it('should have class name supplied by options-selected-class on active item', () => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options, - modelValue: 'Option 1', - optionsSelectedClass: 'test-class', - color: 'orange' - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains('[role="option"]', options[ 0 ]) - .should('not.have.class', 'text-orange') - .should('have.class', 'test-class') - cy.get('.q-menu') - .contains('[role="option"]', options[ 1 ]) - .should('not.have.class', 'text-orange') - .should('not.have.class', 'test-class') - }) - }) - - describe('(prop): options-html', () => { - it('should not render options with html by default', () => { - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains('Option 1') - .should('have.color', 'black') - .should('not.have.css', 'font-weight', '700') - }) - - it('should render options with html when options-html is true', () => { - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - optionsHtml: true - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .contains('Option 1') - .should('have.color', 'red') - .should('have.css', 'font-weight', '700') - }) - }) - - describe('(prop): options-cover', () => { - it('should make the popup menu cover the select', (done) => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options, - optionsCover: true - } - }) - getHostElement() - .click() - cy.get('.q-menu').should('be.visible') - getHostElement() - .isNotActionable(done) - }) - - it('should not make the popup menu cover the select when use-input is used', () => { - const options = [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ] - mountQSelect({ - props: { - options, - optionsCover: true, - useInput: true - } - }) - getHostElement() - .click() - .click({ timeout: 100 }) - }) - }) - - describe('(prop): menu-shrink', () => { - it('should shrink the menu', () => { - const options = [ '1', '2', '3', '4' ] - mountQSelect({ - props: { - options, - menuShrink: true - } - }) - let selectWidth = 0 - getHostElement() - .then(($el) => { - selectWidth = $el[ 0 ].clientWidth - }) - .click() - cy.get('.q-menu') - .then(($el) => { - expect($el[ 0 ].clientWidth).to.below(selectWidth) - }) - }) - }) - - describe('(prop): map-options', () => { - it('should display the label of the selected value instead of the value itself', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1, - mapOptions: true - } - }) - getHostElement() - .contains(options[ 0 ].label) - }) - - it('should display the selected value as string by default', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1 - } - }) - getHostElement() - .contains(options[ 0 ].value) - }) - }) - }) - - describe('Category: position', () => { - describe.skip('(prop): menu-anchor', () => { - // This is a menu property which is tested in QMenu.spec.js - }) - - describe.skip('(prop): menu-self', () => { - // This is a menu property which is tested in QMenu.spec.js - }) - - describe.skip('(prop): menu-offset', () => { - // This is a menu property which is tested in QMenu.spec.js - }) - }) - - describe('Category: selection', () => { - describe('(prop): display-value', () => { - it('should override the default selection string', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1, - displayValue: 'Test' - } - }) - getHostElement() - .should('not.contain', options[ 0 ].value) - .should('contain', 'Test') - }) - - it('should not override the default selection string when using `use-chips`', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1, - displayValue: 'Test', - useChips: true - } - }) - getHostElement() - .should('contain', options[ 0 ].value) - .should('not.contain', 'Test') - }) - - it('should not override the default selection string when using `selected` slot', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1, - displayValue: 'Test' - }, - slots: { - selected: () => 'Hello there' - } - }) - getHostElement() - .should('not.contain', options[ 0 ].value) - .should('not.contain', 'Test') - .should('contain', 'Hello there') - }) - }) - - describe('(prop): display-value-html', () => { - it('should render the selected option as html', () => { - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - modelValue: options[ 0 ], - displayValueHtml: true - } - }) - getHostElement() - .contains('Option 1') - .should('have.color', 'red') - .should('have.css', 'font-weight', '700') - }) - - it('should not render the selected option as html when using `selected` slot', () => { - const html = 'Option 1' - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - modelValue: options[ 0 ], - displayValueHtml: true - }, - slots: { - selected: () => html - } - }) - getHostElement() - .contains(html) - }) - - it('should not render the selected option as html when using `selected-item` slot', () => { - const html = 'Option 1' - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - modelValue: options[ 0 ], - displayValueHtml: true - }, - slots: { - 'selected-item': () => html - } - }) - getHostElement() - .contains(html) - }) - }) - - describe('(prop): hide-selected', () => { - it('should not show the selected value', () => { - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - modelValue: options[ 0 ], - hideSelected: true - } - }) - getHostElement() - .should('not.contain', options[ 0 ]) - }) - - it('should set the value on the underlying input when using hide-selected', () => { - // Todo: it its not really clear from the docs that you need to use `useInput` and `fillInput` together with this prop to achieve this - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - modelValue: options[ 0 ], - hideSelected: true, - fillInput: true, - useInput: true - } - }) - getHostElement() - .get('input') - .should('have.value', options[ 0 ]) - }) - }) - - describe('(prop): max-values', () => { - it('should allow a maximum number of selections', () => { - const max = 3 - const options = [ '1', '2', '3', '4', '5' ] - const model = ref([]) - mountQSelect({ - props: { - ...vModelAdapter(model), - options, - maxValues: max, - multiple: true - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('[role="option"]') - .should('be.visible') - .as('clicked') - .click({ multiple: true }) - cy.get('@clicked') - .then(() => { - expect(model.value.length).to.equal(max) - }) - }) - }) - - describe('(prop): use-chips', () => { - it('should use QChips to show the selected value', () => { - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - modelValue: options[ 0 ], - useChips: true - } - }) - getHostElement() - .get('.q-chip') - .should('contain', options[ 0 ]) - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): popup-content-class', () => { - it('should apply the class to the popup element', () => { - const className = 'test-class' - mountQSelect({ - props: { - options: [ '1', '2 ' ], - popupContentClass: className - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .should('have.class', className) - }) - }) - - describe('(prop): popup-content-style', () => { - it('should apply the style definitions to the popup element', () => { - const style = 'background: red;' - mountQSelect({ - props: { - options: [ '1', '2 ' ], - popupContentStyle: style - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .should('have.backgroundColor', 'red') - }) - }) - - describe('(prop): input-class', () => { - it('should apply a class to the input element when using `useInput`', () => { - const className = 'test-class' - mountQSelect({ - props: { - useInput: true, - inputClass: className - } - }) - getHostElement() - .get('input') - .should('have.class', className) - }) - }) - - describe('(prop): input-style', () => { - it('should apply a style to the input element when using `useInput`', () => { - const style = 'font-size: 30px' - mountQSelect({ - props: { - useInput: true, - inputStyle: style - } - }) - getHostElement() - .get('input') - .should('have.css', 'font-size', '30px') - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): selected', () => { - it('should display when something is selected', () => { - const selectedString = 'Test slot selected' - const options = [ 'Option 1', 'Option 2' ] - mountQSelect({ - props: { - options, - modelValue: options[ 0 ] - }, - slots: { - selected: () => selectedString - } - }) - getHostElement() - .should('contain', selectedString) - }) - }) - - describe('(slot): loading', () => { - it('should display when element is loading', () => { - const loadingString = 'Test slot loading' - mountQSelect({ - props: { - loading: true - }, - slots: { - loading: () => loadingString - } - }) - getHostElement() - .should('contain', loadingString) - }) - - it('should not display when element is loading', () => { - const loadingString = 'Test slot loading' - mountQSelect({ - props: { - loading: false - }, - slots: { - loading: () => loadingString - } - }) - getHostElement() - .should('not.contain', loadingString) - }) - }) - - describe('(slot): before-options', () => { - it('should display the slot content before the options', () => { - mountQSelect({ - props: { - options: [ '1', '2', '3' ] - }, - slots: { - 'before-options': () => h('div', { class: 'dummyClass' }, 'Hello') - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .children().first() - .should('have.class', 'dummyClass') - }) - }) - - describe('(slot): after-options', () => { - it('should display the slot content after the options', () => { - mountQSelect({ - props: { - options: [ '1', '2', '3' ] - }, - slots: { - 'after-options': () => h('div', { class: 'dummyClass' }, 'Hello') - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .children().last() - .should('have.class', 'dummyClass') - }) - }) - - describe('(slot): no-option', () => { - it('should display the slot content when there are no options', () => { - const compareString = 'No options :(' - mountQSelect({ - props: { - options: [ ] - }, - slots: { - 'no-option': () => compareString - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .should('contain', compareString) - }) - - it('should pass the inputValue to the slot scope', () => { - const compareString = 'No options :(' - mountQSelect({ - props: { - options: [ ], - useInput: true - }, - slots: { - 'no-option': (scope) => compareString + scope.inputValue - } - }) - getHostElement() - .click() - .type('Hello') - cy.get('.q-menu') - .should('contain', compareString + 'Hello') - }) - - it('should not display the slot content when there are options', () => { - const compareString = 'No options :(' - mountQSelect({ - props: { - options: [ '1', '2', '3' ] - }, - slots: { - 'no-option': () => compareString - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .should('not.contain', compareString) - }) - }) - - describe('(slot): selected-item', () => { - it('should override the default selection slot', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1 - }, - slots: { - 'selected-item': () => 'Test' - } - }) - getHostElement() - .should('not.contain', options[ 0 ].value) - .should('contain', 'Test') - }) - - it('should pass the selected option index to the slot scope', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1 - }, - slots: { - 'selected-item': (scope) => 'Test' + scope.index - } - }) - getHostElement() - .should('contain', 'Test0') - }) - - it('should pass the selected option value to the slot scope', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - mountQSelect({ - props: { - options, - modelValue: 1 - }, - slots: { - 'selected-item': (scope) => 'Test' + scope.opt - } - }) - getHostElement() - .should('contain', 'Test1') - }) - - it('should pass a removeAtIndex function to the slot scope', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - const model = ref(1) - mountQSelect({ - props: { - ...vModelAdapter(model), - options - }, - slots: { - 'selected-item': (scope) => h('button', { class: 'remove', onClick: () => scope.removeAtIndex(scope.index) }, 'Remove') - } - }) - getHostElement() - .get('button.remove') - .click() - getHostElement() - .get('button.remove') - .should('not.exist') - }) - - it('should pass a toggleOption function to the slot scope', () => { - const options = [ { label: 'Option one', value: 1 }, { label: 'Option two', value: 2 } ] - const model = ref(1) - mountQSelect({ - props: { - ...vModelAdapter(model), - options - }, - slots: { - 'selected-item': (scope) => h('button', { class: 'toggle', onClick: () => scope.toggleOption(2) }, 'Toggle' + scope.opt) - } - }) - getHostElement() - .get('button.toggle') - .should('contain', 'Toggle1') - .click() - getHostElement() - .get('button.toggle') - .should('contain', 'Toggle2') - }) - }) - - describe('(slot): option', () => { - it('should render a list of the provided slot as options', () => { - const options = [ '1', '2', '3' ] - mountQSelect({ - props: { - options - }, - slots: { - option: (scope) => h('div', { class: 'custom-option' }, scope.opt) - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('.custom-option') - .should('have.length', options.length) - }) - - it('should have a selected property in the scope', () => { - const options = [ '1', '2', '3' ] - mountQSelect({ - props: { - modelValue: '1', - options - }, - slots: { - option: (scope) => h('div', { class: `custom-option-${ scope.selected }` }, scope.opt + scope.selected) - } - }) - getHostElement() - .click() - cy.get('.q-menu') - .get('.custom-option-true') - .should('have.length', 1) - .should('contain', options[ 0 ]) - }) - }) - }) - - describe('Events', () => { - describe('(event): update:model-value', () => { - it('should emit event when model value changes', () => { - const fn = cy.stub() - mountQSelect({ - props: { - options: [ '1', '2', '3' ], - modelValue: null, - 'onUpdate:modelValue': fn - } - }) - - expect(fn).not.to.be.called - getHostElement() - .click() - cy.get('.q-menu') - .get('[role="option"]') - .first() - .should('be.visible') - .as('clicked') - .click() - cy.get('@clicked') - .then(() => { - expect(fn).to.be.calledWith('1') - }) - }) - }) - - describe('(event): input-value', () => { - it('should emit event when text input changes', () => { - const fn = cy.stub() - mountQSelect({ - props: { - modelValue: null, - onInputValue: fn, - useInput: true, - inputDebounce: 0 - } - }) - - expect(fn).not.to.be.called - getHostElement() - .get('input') - .type('h') - .then(() => { - expect(fn).to.be.calledWith('h') - }) - }) - }) - - describe('(event): remove', () => { - it('should emit event when a selected item is removed from selection', () => { - const fn = cy.stub() - const model = ref([ '2', '3' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - onRemove: fn, - multiple: true, - options: [ '1', '2', '3' ] - } - }) - - expect(fn).not.to.be.called - getHostElement() - .click() - cy.get('.q-menu') - .get('[role="option"]') - .first() - .should('be.visible') - .as('clicked') - .click() - cy.get('@clicked') - .then(() => { - expect(fn).not.to.be.called - }) - cy.get('.q-menu') - .get('[role="option"]') - .first() - .should('be.visible') - .as('clicked') - .click() - cy.get('@clicked') - .then(() => { - // Item is added in the previous step at the end of the array, so at index 2 - expect(fn).to.be.calledWith({ index: 2, value: '1' }) - }) - }) - }) - - describe('(event): add', () => { - it('should emit event when an option is added to the selection', () => { - const fn = cy.stub() - const model = ref([ '2' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - onAdd: fn, - multiple: true, - options: [ '1', '2', '3' ] - } - }) - - expect(fn).not.to.be.called - getHostElement() - .click() - cy.get('.q-menu') - .get('[role="option"]') - .first() - .should('be.visible') - .as('clicked') - .click() - cy.get('@clicked') - .then(() => { - // Item is added in the previous step at the end of the array, so at index 2 - expect(fn).to.be.calledWith({ index: 1, value: '1' }) - }) - }) - }) - - describe('(event): new-value', () => { - it('should emit event when something is typed into the input field and enter is pressed', () => { - const fn = cy.stub() - const model = ref([ '2' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - onNewValue: fn, - multiple: true, - useInput: true, - hideDropdownIcon: true - } - }) - - expect(fn).not.to.be.called - getHostElement() - .get('input') - .type('100') - .then(() => { - expect(fn).not.to.be.called - }) - .type('{enter}') - .then(() => { - expect(fn).to.be.calledWith('100') - }) - }) - - it('should add the value to the model when the doneFn is called', () => { - const model = ref([ '2' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - onNewValue: (val, doneFn) => { - doneFn(val) - }, - multiple: true, - useInput: true, - hideDropdownIcon: true - } - }) - - getHostElement() - .get('input') - .type('100') - .type('{enter}') - .then(() => { - expect(model.value).includes('100') - }) - }) - }) - - describe('(event): filter', () => { - it('should emit event when something is typed into the input field', () => { - const fn = cy.stub() - mountQSelect({ - props: { - onFilter: fn, - useInput: true, - inputDebounce: 0 - } - }) - - expect(fn).not.to.be.called - getHostElement() - .get('input') - .type('h') - .then(() => { - expect(fn).to.be.calledWith('h') - }) - }) - }) - - describe('(event): filter-abort', () => { - it('should emit event when the filterFn has not called the doneFn yet and a new filter is requested', () => { - const fn = cy.stub() - const filterFn = cy.stub() - mountQSelect({ - props: { - onFilter: filterFn, - onFilterAbort: fn, - useInput: true, - inputDebounce: 0 - } - }) - - expect(fn).not.to.be.called - getHostElement() - .get('input') - .click() - .then(() => { - expect(filterFn).to.be.calledOnce - expect(fn).not.to.be.called - }) - .type('h') - .then(() => { - expect(fn).to.be.calledOnce - }) - }) - - it('should not emit event when the filter has called its doneFn', () => { - const fn = cy.stub() - mountQSelect({ - props: { - onFilter: (val, doneFn) => { - doneFn() - }, - onFilterAbort: fn, - useInput: true, - inputDebounce: 0 - } - }) - - expect(fn).not.to.be.called - getHostElement() - .get('input') - .click() - .then(() => { - expect(fn).not.to.be.called - }) - .type('h') - .then(() => { - expect(fn).not.to.be.called - }) - }) - }) - - describe('(event): popup-show', () => { - it('should emit event when the options are shown', () => { - const fn = cy.stub() - mountQSelect({ - props: { - onPopupShow: fn, - options: [ '1', '2', '3' ] - } - }) - - expect(fn).not.to.be.called - getHostElement() - .click() - .then(() => { - expect(fn).to.be.called - }) - }) - }) - - describe('(event): popup-hide', () => { - it('should emit event when the options are hidden', () => { - const fn = cy.stub() - mountQSelect({ - props: { - onPopupHide: fn, - options: [ '1', '2', '3' ] - } - }) - - expect(fn).not.to.be.called - getHostElement() - .click() - .then(() => { - expect(fn).not.to.be.called - }) - .type('{esc}') - .then(() => { - expect(fn).to.be.called - }) - }) - }) - - describe('(event): virtual-scroll', () => { - it.skip('', () => { - // The virtual scroll code is tested as a composable - // The property is included in the QSelect.json to add typings - // for the QSelect ref passed in this event. - // I think testing the component ref that is passed is of type QSelect is out of scope for unit tests. - }) - }) - }) - - describe('Methods', () => { - describe('(method): focus', () => { - it('should focus the component', () => { - mountQSelect() - - getHostElement() - .get('[tabindex="0"]') - .should('not.have.focus') - getHostElement() - .then(() => { - Cypress.vueWrapper.vm.focus() - }) - getHostElement() - .get('[tabindex="0"]') - .should('have.focus') - }) - }) - - describe('(method): showPopup', () => { - it('should open the popup and focus the component', () => { - mountQSelect({ - props: { - options: [ '1', '2' ] - } - }) - - getHostElement() - cy.get('.q-menu') - .should('not.exist') - .then(() => { - Cypress.vueWrapper.vm.showPopup() - }) - cy.get('.q-menu') - .should('be.visible') - getHostElement() - .get('[tabindex="0"]') - .should('have.focus') - }) - }) - - describe('(method): hidePopup', () => { - it('should hide the popup', () => { - mountQSelect({ - props: { - options: [ '1', '2' ] - } - }) - - getHostElement() - .click() - cy.get('.q-menu') - .should('be.visible') - .then(() => { - Cypress.vueWrapper.vm.hidePopup() - }) - cy.get('.q-menu') - .should('not.exist') - }) - }) - - describe('(method): removeAtIndex', () => { - it('should remove a selected option at the correct index', () => { - const options = [ '1', '2', '3', '4' ] - const model = ref([ '1', '2', '4' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - multiple: true, - options - } - }) - .then(() => { - expect(model.value.includes('4')).to.be.true - Cypress.vueWrapper.vm.removeAtIndex(2) - expect(model.value.includes('4')).to.be.false - }) - }) - }) - - describe('(method): add', () => { - it('should add a selected option', () => { - const model = ref([ '1', '2' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - multiple: true - } - }) - .then(() => { - expect(model.value.includes('100')).to.be.false - Cypress.vueWrapper.vm.add('100') - expect(model.value.includes('100')).to.be.true - }) - }) - - it('should not add a duplicate option when unique is true', () => { - const model = ref([ '1', '2' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - multiple: true - } - }) - .then(() => { - expect(model.value.length).to.be.equal(2) - Cypress.vueWrapper.vm.add('2', true) - expect(model.value.length).to.be.equal(2) - Cypress.vueWrapper.vm.add('2') - expect(model.value.length).to.be.equal(3) - }) - }) - }) - - describe('(method): toggleOption', () => { - it('should toggle an option', () => { - const model = ref([ '1', '2' ]) - mountQSelect({ - props: { - ...vModelAdapter(model), - multiple: true - } - }) - .then(() => { - expect(model.value.length).to.be.equal(2) - Cypress.vueWrapper.vm.toggleOption('2') - expect(model.value.length).to.be.equal(1) - }) - // When not using this wait this test will succeed on `open-ct` but fail on `run-ct` - .wait(50) - .then(() => { - Cypress.vueWrapper.vm.toggleOption('2') - expect(model.value.length).to.be.equal(2) - }) - }) - - // Todo: toggleOption argument keepOpen only does something when using single select. This is not clear from the docs. - // should this be consistent? E.g. use `true` as argument when multiple is true by default but make sure it can be overridden. - it('should close the menu and clear the filter', () => { - const model = ref('1') - mountQSelect({ - props: { - ...vModelAdapter(model), - options: [ '1', '2' ], - useInput: true - } - }) - - getHostElement() - .click() - .get('input') - .type('h') - cy.get('.q-menu') - .should('be.visible') - .then(() => { - Cypress.vueWrapper.vm.toggleOption('2') - }) - cy.get('.q-menu') - .should('not.exist') - cy.get('input') - .should('have.value', '') - }) - - it('should not close the menu and clear the filter when keepOpen is true', () => { - const model = ref('1') - mountQSelect({ - props: { - ...vModelAdapter(model), - options: [ '1', '2' ], - useInput: true - } - }) - - getHostElement() - .click() - .get('input') - .type('h') - cy.get('.q-menu') - .should('be.visible') - .then(() => { - Cypress.vueWrapper.vm.toggleOption('2', true) - }) - cy.get('.q-menu') - .should('be.visible') - cy.get('input') - .should('have.value', 'h') - }) - }) - - describe('(method): setOptionIndex', () => { - it('should set an option from the menu dropdown as focused', () => { - const options = [ '1', '2', '3', '4' ] - mountQSelect({ - props: { - options - } - }) - getHostElement() - .click() - .then(() => { - Cypress.vueWrapper.vm.setOptionIndex(0) - }) - .get('[role="option"]') - .first() - .should('have.class', 'q-manual-focusable--focused') - }) - }) - - describe('(method): moveOptionSelection', () => { - it('should move the optionSelection by some index offset', () => { - const options = [ '1', '2', '3', '4' ] - mountQSelect({ - props: { - options - } - }) - getHostElement() - .click() - .then(() => { - Cypress.vueWrapper.vm.setOptionIndex(0) - }) - .get('[role="option"]') - .first() - .should('have.class', 'q-manual-focusable--focused') - .then(() => { - Cypress.vueWrapper.vm.moveOptionSelection(3) - }) - .get('[role="option"]') - .last() - .should('have.class', 'q-manual-focusable--focused') - }) - }) - - describe('(method): filter', () => { - it('should filter the options list', () => { - const options = [ '1', '2', '3', '4' ] - const fn = cy.stub() - const text = 'test' - mountQSelect({ - props: { - options, - useInput: true, - onFilter: fn - } - }) - getHostElement() - .click() - .then(() => { - expect(fn).not.to.be.calledWith(text) - Cypress.vueWrapper.vm.filter(text) - expect(fn).to.be.calledWith(text) - }) - }) - }) - - describe('(method): updateMenuPosition', () => { - it.skip(' ', () => { - // Not sure in what scenario this is needed, there is also some auto repositioning going on - }) - }) - - describe('(method): updateInputValue', () => { - it('should update the input value', () => { - const options = [ '1', '2', '3', '4' ] - const fn = cy.stub() - const text = 'test' - mountQSelect({ - props: { - options, - useInput: true, - onFilter: fn - } - }) - getHostElement() - .click() - .then(() => { - expect(fn).not.to.be.calledWith(text) - Cypress.vueWrapper.vm.updateInputValue(text) - expect(fn).to.be.calledWith(text) - }) - .get('input') - .should('have.value', text) - }) - - it('should not trigger the filter when specified', () => { - const options = [ '1', '2', '3', '4' ] - const fn = cy.stub() - const text = 'test' - mountQSelect({ - props: { - options, - useInput: true, - onFilter: fn - } - }) - getHostElement() - .click() - .then(() => { - expect(fn).not.to.be.calledWith(text) - Cypress.vueWrapper.vm.updateInputValue(text, true) - expect(fn).not.to.be.calledWith(text) - }) - .get('input') - .should('have.value', text) - }) - }) - - describe('(method): isOptionSelected', () => { - it('should tell when an option is selected', () => { - const options = [ '1', '2', '3', '4' ] - const modelValue = [ '1', '2', '4' ] - mountQSelect({ - props: { - modelValue, - multiple: true, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.isOptionSelected(options[ 0 ])).to.be.true - expect(Cypress.vueWrapper.vm.isOptionSelected(options[ 2 ])).to.be.false - }) - }) - }) - - describe('(method): getEmittingOptionValue', () => { - it('should return the emit value with plain options', () => { - const options = [ '1', '2', '3', '4' ] - const modelValue = '1' - mountQSelect({ - props: { - modelValue, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getEmittingOptionValue(options[ 2 ])).to.equal(options[ 2 ]) - }) - }) - - it('should return the emit value with object options', () => { - const options = [ { label: '1', value: 1 }, { label: '2', value: 2 }, { label: '3', value: 3 } ] - const modelValue = options[ 0 ] - mountQSelect({ - props: { - modelValue, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getEmittingOptionValue(options[ 2 ])).to.equal(options[ 2 ]) - }) - }) - - it('should respect emit-value when using options', () => { - const options = [ { label: '1', value: 1 }, { label: '2', value: 2 }, { label: '3', value: 3 } ] - const modelValue = options[ 0 ] - mountQSelect({ - props: { - modelValue, - options, - emitValue: true - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getEmittingOptionValue(options[ 2 ])).to.equal(options[ 2 ].value) - }) - }) - }) - - describe('(method): getOptionValue', () => { - it('should return the option value with plain options', () => { - const options = [ '1', '2', '3', '4' ] - const modelValue = '1' - mountQSelect({ - props: { - modelValue, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getOptionValue(options[ 2 ])).to.equal(options[ 2 ]) - }) - }) - - it('should return the option value with object options (value by default)', () => { - const options = [ { label: '1', value: 1 }, { label: '2', value: 2 }, { label: '3', value: 3 } ] - const modelValue = options[ 0 ] - mountQSelect({ - props: { - modelValue, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getOptionValue(options[ 2 ])).to.equal(options[ 2 ].value) - }) - }) - - it('should respect the option-value option', () => { - const options = [ { label: '1', test: 1 }, { label: '2', test: 2 }, { label: '3', test: 3 } ] - const modelValue = options[ 0 ] - mountQSelect({ - props: { - modelValue, - options, - optionValue: 'test' - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getOptionValue(options[ 2 ])).to.equal(options[ 2 ].test) - }) - }) - }) - - describe('(method): getOptionLabel', () => { - it('should return the option label with plain options', () => { - const options = [ '1', '2', '3', '4' ] - const modelValue = '1' - mountQSelect({ - props: { - modelValue, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getOptionLabel(options[ 2 ])).to.equal(options[ 2 ]) - }) - }) - - it('should return the option label with object options (label by default)', () => { - const options = [ { label: '1', value: 1 }, { label: '2', value: 2 }, { label: '3', value: 3 } ] - const modelValue = options[ 0 ] - mountQSelect({ - props: { - modelValue, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getOptionLabel(options[ 2 ])).to.equal(options[ 2 ].label) - }) - }) - - it('should respect the option-value option', () => { - const options = [ { test: '1', value: 1 }, { test: '2', value: 2 }, { test: '3', value: 3 } ] - const modelValue = options[ 0 ] - mountQSelect({ - props: { - modelValue, - options, - optionLabel: 'test' - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.getOptionLabel(options[ 2 ])).to.equal(options[ 2 ].test) - }) - }) - }) - - describe('(method): isOptionDisabled', () => { - it('should return if an option is disabled correctly', () => { - const options = [ { label: '1', value: 1, disable: true }, { label: '2', value: 2 }, { label: '3', value: 3 } ] - const modelValue = options[ 0 ] - mountQSelect({ - props: { - modelValue, - options - } - }) - .then(() => { - expect(Cypress.vueWrapper.vm.isOptionDisabled(options[ 0 ])).to.be.true - // This currently fails: https://github.com/quasarframework/quasar/issues/12046 - // expect(Cypress.vueWrapper.vm.isOptionDisabled(options[ 1 ])).to.be.false - // expect(Cypress.vueWrapper.vm.isOptionDisabled(options[ 2 ])).to.be.false - }) - }) - }) - }) -}) diff --git a/ui/src/components/slide-transition/QSlideTransition.js b/ui/src/components/slide-transition/QSlideTransition.js index 34b22a011ce..cf2af63ab73 100644 --- a/ui/src/components/slide-transition/QSlideTransition.js +++ b/ui/src/components/slide-transition/QSlideTransition.js @@ -20,7 +20,7 @@ export default createComponent({ let timer = null, timerFallback = null, animListener, lastEvent function cleanup () { - doneFn && doneFn() + doneFn?.() doneFn = null animating = false @@ -34,7 +34,7 @@ export default createComponent({ timerFallback = null } - element !== void 0 && element.removeEventListener('transitionend', animListener) + element?.removeEventListener('transitionend', animListener) animListener = null } diff --git a/ui/src/components/stepper/StepHeader.js b/ui/src/components/stepper/StepHeader.js index f0e2958a977..b7f69a2ba10 100644 --- a/ui/src/components/stepper/StepHeader.js +++ b/ui/src/components/stepper/StepHeader.js @@ -119,7 +119,7 @@ export default createComponent({ )) function onActivate () { - blurRef.value !== null && blurRef.value.focus() + blurRef.value?.focus() isActive.value === false && props.goToPanel(props.step.name) } diff --git a/ui/src/components/table/QTable.js b/ui/src/components/table/QTable.js index 31525c50b6f..2af079dfe82 100644 --- a/ui/src/components/table/QTable.js +++ b/ui/src/components/table/QTable.js @@ -91,10 +91,14 @@ export default createComponent({ tableClass: [ String, Array, Object ], tableHeaderStyle: [ String, Array, Object ], tableHeaderClass: [ String, Array, Object ], + tableRowStyleFn: Function, + tableRowClassFn: Function, cardContainerClass: [ String, Array, Object ], cardContainerStyle: [ String, Array, Object ], cardStyle: [ String, Array, Object ], cardClass: [ String, Array, Object ], + cardStyleFn: Function, + cardClassFn: Function, hideBottom: Boolean, hideSelectedBanner: Boolean, @@ -148,7 +152,7 @@ export default createComponent({ + (props.bordered === true ? ' q-table--bordered' : '') ) - const __containerClass = computed(() => + const containerClass = computed(() => `q-table__container q-table--${ props.separator }-separator column no-wrap` + (props.grid === true ? ' q-table--grid' : cardDefaultClass.value) + (isDark.value === true ? ' q-table--dark' : '') @@ -157,13 +161,13 @@ export default createComponent({ + (inFullscreen.value === true ? ' fullscreen scroll' : '') ) - const containerClass = computed(() => - __containerClass.value + (props.loading === true ? ' q-table--loading' : '') + const rootContainerClass = computed(() => + containerClass.value + (props.loading === true ? ' q-table--loading' : '') ) watch( - () => props.tableStyle + props.tableClass + props.tableHeaderStyle + props.tableHeaderClass + __containerClass.value, - () => { hasVirtScroll.value === true && virtScrollRef.value !== null && virtScrollRef.value.reset() } + () => props.tableStyle + props.tableClass + props.tableHeaderStyle + props.tableHeaderClass + containerClass.value, + () => { hasVirtScroll.value === true && virtScrollRef.value?.reset() } ) const { @@ -382,13 +386,26 @@ export default createComponent({ selected = isRowSelected(key) if (bodySlot !== void 0) { + const cfg = { + key, + row, + pageIndex, + __trClass: selected ? 'selected' : '' + } + + if (props.tableRowStyleFn !== void 0) { + cfg.__trStyle = props.tableRowStyleFn(row) + } + + if (props.tableRowClassFn !== void 0) { + const cls = props.tableRowClassFn(row) + if (cls) { + cfg.__trClass = `${ cls } ${ cfg.__trClass }` + } + } + return bodySlot( - getBodyScope({ - key, - row, - pageIndex, - __trClass: selected ? 'selected' : '' - }) + getBodyScope(cfg) ) } @@ -451,6 +468,17 @@ export default createComponent({ } } + if (props.tableRowStyleFn !== void 0) { + data.style = props.tableRowStyleFn(row) + } + + if (props.tableRowClassFn !== void 0) { + const cls = props.tableRowClassFn(row) + if (cls) { + data.class[ cls ] = true + } + } + return h('tr', data, child) } @@ -802,30 +830,28 @@ export default createComponent({ h('div', { class: 'q-table__separator col' }) ) - if (hasOpts === true) { - child.push( - h('div', { class: 'q-table__control' }, [ - h('span', { class: 'q-table__bottom-item' }, [ - props.rowsPerPageLabel || $q.lang.table.recordsPerPage - ]), - h(QSelect, { - class: 'q-table__select inline q-table__bottom-item', - color: props.color, - modelValue: rowsPerPage, - options: computedRowsPerPageOptions.value, - displayValue: rowsPerPage === 0 - ? $q.lang.table.allRows - : rowsPerPage, - dark: isDark.value, - borderless: true, - dense: true, - optionsDense: true, - optionsCover: true, - 'onUpdate:modelValue': onPagSelection - }) - ]) - ) - } + hasOpts === true && child.push( + h('div', { class: 'q-table__control' }, [ + h('span', { class: 'q-table__bottom-item' }, [ + props.rowsPerPageLabel || $q.lang.table.recordsPerPage + ]), + h(QSelect, { + class: 'q-table__select inline q-table__bottom-item', + color: props.color, + modelValue: rowsPerPage, + options: computedRowsPerPageOptions.value, + displayValue: rowsPerPage === 0 + ? $q.lang.table.allRows + : rowsPerPage, + dark: isDark.value, + borderless: true, + dense: true, + optionsDense: true, + optionsCover: true, + 'onUpdate:modelValue': onPagSelection + }) + ]) + ) if (paginationSlot !== void 0) { control = paginationSlot(marginalsScope.value) @@ -857,6 +883,7 @@ export default createComponent({ ...btnProps, icon: navIcon.value[ 0 ], disable: isFirstPage.value, + ariaLabel: $q.lang.pagination.first, onClick: firstPage }) ) @@ -867,6 +894,7 @@ export default createComponent({ ...btnProps, icon: navIcon.value[ 1 ], disable: isFirstPage.value, + ariaLabel: $q.lang.pagination.prev, onClick: prevPage }), @@ -875,6 +903,7 @@ export default createComponent({ ...btnProps, icon: navIcon.value[ 2 ], disable: isLastPage.value, + ariaLabel: $q.lang.pagination.next, onClick: nextPage }) ) @@ -885,6 +914,7 @@ export default createComponent({ ...btnProps, icon: navIcon.value[ 3 ], disable: isLastPage.value, + ariaLabel: $q.lang.pagination.last, onClick: lastPage }) ) @@ -955,6 +985,17 @@ export default createComponent({ style: props.cardStyle } + if (props.cardStyleFn !== void 0) { + data.style = [ data.style, props.cardStyleFn(scope.row) ] + } + + if (props.cardClassFn !== void 0) { + const cls = props.cardClassFn(scope.row) + if (cls) { + data.class[ 0 ] += ` ${ cls }` + } + } + if ( props.onRowClick !== void 0 || props.onRowDblclick !== void 0 @@ -1030,7 +1071,7 @@ export default createComponent({ return () => { const child = [ getTopDiv() ] - const data = { ref: rootRef, class: containerClass.value } + const data = { ref: rootRef, class: rootContainerClass.value } if (props.grid === true) { child.push(getGridHeader()) diff --git a/ui/src/components/table/QTable.json b/ui/src/components/table/QTable.json index 0ee312501c7..43c9c9e4d80 100644 --- a/ui/src/components/table/QTable.json +++ b/ui/src/components/table/QTable.json @@ -563,6 +563,50 @@ "category": "style" }, + "table-row-style-fn": { + "type": "Function", + "desc": "CSS style to apply to the table rows (which are TR elements); For best performance, reference it from your scope and do not define it inline", + "params": { + "row": { + "type": "Object", + "desc": "The current row being processed", + "examples": [ "{ name: 'Frozen Yogurt', calories: 159 }" ] + } + }, + "returns": { + "type": "String", + "desc": "CSS style to apply to the row", + "examples": [ + "'color: blue'", + "'background-color: #ff0000; color: green'" + ] + }, + "category": "style", + "addedIn": "v2.18" + }, + + "table-row-class-fn": { + "type": "Function", + "desc": "CSS class(es) to apply the table rows (which are TR elements); For best performance, reference it from your scope and do not define it inline", + "params": { + "row": { + "type": "Object", + "desc": "The current row being processed", + "examples": [ "{ name: 'Frozen Yogurt', calories: 159 }" ] + } + }, + "returns": { + "type": "String", + "desc": "CSS class(es) to apply to the row, space separated", + "examples": [ + "'my-special-class'", + "'my-class my-second-class'" + ] + }, + "category": "style", + "addedIn": "v2.18" + }, + "card-container-style": { "type": [ "String", "Array", "Object" ], "tsType": "VueStyleProp", @@ -608,6 +652,50 @@ "category": "style" }, + "card-style-fn": { + "type": "Function", + "desc": "(Grid mode only) CSS style to apply to the row/record card; Has no effect when the 'item' slot is used; For best performance, reference it from your scope and do not define it inline", + "params": { + "row": { + "type": "Object", + "desc": "The current row/record being processed", + "examples": [ "{ name: 'Frozen Yogurt', calories: 159 }" ] + } + }, + "returns": { + "type": "String", + "desc": "CSS style to apply to the row/record", + "examples": [ + "'color: blue'", + "'background-color: #ff0000; color: green'" + ] + }, + "category": "style", + "addedIn": "v2.18" + }, + + "card-class-fn": { + "type": "Function", + "desc": "(Grid mode only) CSS class(es) to apply the row/record card; Has no effect when the 'item' slot is used; For best performance, reference it from your scope and do not define it inline", + "params": { + "row": { + "type": "Object", + "desc": "The current row/record being processed", + "examples": [ "{ name: 'Frozen Yogurt', calories: 159 }" ] + } + }, + "returns": { + "type": "String", + "desc": "CSS class(es) to apply to the row, space separated", + "examples": [ + "'my-special-class'", + "'my-class my-second-class'" + ] + }, + "category": "style", + "addedIn": "v2.18" + }, + "title-class": { "type": [ "String", "Array", "Object" ], "tsType": "VueClassProp", @@ -896,6 +984,12 @@ "__trClass": { "type": "String", "desc": "Internal prop passed down to QTr (if used)" + }, + "__trStyle": { + "type": "String", + "required": false, + "desc": "Internal prop passed down to QTr (if used)", + "addedIn": "v2.18" } } }, @@ -1848,6 +1942,10 @@ "type": "String", "desc": "The suggested icon name (following Quasar convention)", "examples": [ "'warning'" ] + }, + "filter": { + "type": [ "String", "Object" ], + "desc": "String/Object to filter table with (the 'filter' prop)" } } } diff --git a/ui/src/components/table/QTr.js b/ui/src/components/table/QTr.js index 6aa5bbf376c..8f58b8f0dc0 100644 --- a/ui/src/components/table/QTr.js +++ b/ui/src/components/table/QTr.js @@ -18,6 +18,9 @@ export default createComponent({ + (props.noHover === true ? ' q-tr--no-hover' : '') ) - return () => h('tr', { class: classes.value }, hSlot(slots.default)) + return () => h('tr', { + style: props.props?.__trStyle, + class: classes.value + }, hSlot(slots.default)) } }) diff --git a/ui/src/components/table/__tests__/QTable.cy.js b/ui/src/components/table/__tests__/QTable.cy.js deleted file mode 100644 index 0d228702aa0..00000000000 --- a/ui/src/components/table/__tests__/QTable.cy.js +++ /dev/null @@ -1,635 +0,0 @@ -describe('Table API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): grid', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: behavior|content', () => { - describe('(prop): grid-header', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): loading', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: column', () => { - describe('(prop): columns', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): visible-columns', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: content', () => { - describe('(prop): icon-first-page', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): icon-prev-page', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): icon-next-page', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): icon-last-page', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): title', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): hide-header', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): hide-bottom', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): hide-selected-banner', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): hide-no-data', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): hide-pagination', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): separator', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): wrap-cells', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): no-data-label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): no-results-label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): loading-label', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: expansion', () => { - describe('(prop): expanded', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: filter', () => { - describe('(prop): filter', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): filter-method', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: general', () => { - describe('(prop): rows', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): row-key', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: pagination', () => { - describe('(prop): rows-per-page-label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): pagination-label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): pagination', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): rows-per-page-options', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: selection', () => { - describe('(prop): selected-rows-label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): selection', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): selected', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: sorting', () => { - describe('(prop): binary-state-sort', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): column-sort-order', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): sort-method', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): color', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): dense', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): dark', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): flat', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): bordered', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): square', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): table-style', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): table-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): table-header-style', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): table-header-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): card-container-style', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): card-container-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): card-style', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): card-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): title-class', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: virtual-scroll', () => { - describe('(prop): virtual-scroll', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): virtual-scroll-slice-size', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): virtual-scroll-slice-ratio-before', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): virtual-scroll-slice-ratio-after', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): virtual-scroll-item-size', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): virtual-scroll-sticky-size-start', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): virtual-scroll-sticky-size-end', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: virtual-scroll|content', () => { - describe('(prop): table-colspan', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): loading', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): item', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): body', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): body-cell', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): body-cell-[name]', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): header', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): header-cell', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): header-cell-[name]', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): body-selection', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): header-selection', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): top-row', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): bottom-row', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): top', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): bottom', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): pagination', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): top-left', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): top-right', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): top-selection', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(slot): no-data', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Events', () => { - describe('(event): row-click', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): row-dblclick', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): row-contextmenu', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): request', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): selection', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): update:pagination', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): update:selected', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): update:expanded', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): virtual-scroll', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Methods', () => { - describe('(method): requestServerInteraction', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): setPagination', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): firstPage', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): prevPage', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): nextPage', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): lastPage', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): isRowSelected', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): clearSelection', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): isRowExpanded', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): setExpanded', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): sort', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): toggleFullscreen', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): resetVirtualScroll', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): scrollTo', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/components/table/__tests__/QTd.cy.js b/ui/src/components/table/__tests__/QTd.cy.js deleted file mode 100644 index 6b5f3a6558f..00000000000 --- a/ui/src/components/table/__tests__/QTd.cy.js +++ /dev/null @@ -1,35 +0,0 @@ -describe('Td API', () => { - describe('Props', () => { - describe('Category: content', () => { - describe('(prop): auto-width', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: general', () => { - describe('(prop): props', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): no-hover', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): default', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/components/table/__tests__/QTh.cy.js b/ui/src/components/table/__tests__/QTh.cy.js deleted file mode 100644 index 59ebaf586a6..00000000000 --- a/ui/src/components/table/__tests__/QTh.cy.js +++ /dev/null @@ -1,27 +0,0 @@ -describe('Th API', () => { - describe('Props', () => { - describe('Category: content', () => { - describe('(prop): auto-width', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: general', () => { - describe('(prop): props', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): default', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/components/table/__tests__/QTr.cy.js b/ui/src/components/table/__tests__/QTr.cy.js deleted file mode 100644 index 04c6c8db2e2..00000000000 --- a/ui/src/components/table/__tests__/QTr.cy.js +++ /dev/null @@ -1,27 +0,0 @@ -describe('Tr API', () => { - describe('Props', () => { - describe('Category: general', () => { - describe('(prop): props', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): no-hover', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): default', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/components/table/table-sort.js b/ui/src/components/table/table-sort.js index ccd3ce2100d..c1df1b0d412 100644 --- a/ui/src/components/table/table-sort.js +++ b/ui/src/components/table/table-sort.js @@ -87,7 +87,7 @@ export function useTableSort (props, computedPagination, colList, setPagination) } else { const def = colList.value.find(def => def.name === col) - if (def !== void 0 && def.sortOrder) { + if (def?.sortOrder) { sortOrder = def.sortOrder } } diff --git a/ui/src/components/tabs/QTabs.js b/ui/src/components/tabs/QTabs.js index 1b619fec210..e9c51f786ea 100644 --- a/ui/src/components/tabs/QTabs.js +++ b/ui/src/components/tabs/QTabs.js @@ -424,7 +424,7 @@ export default createComponent({ function updateActiveRoute () { let name = null, bestScore = { matchedLen: 0, queryDiff: 9999, hrefLen: 0 } - const list = tabDataList.filter(tab => tab.routeData !== void 0 && tab.routeData.hasRouterLink.value === true) + const list = tabDataList.filter(tab => tab.routeData?.hasRouterLink.value === true) const { hash: currentHash, query: currentQuery } = proxy.$route const currentQueryLen = Object.keys(currentQuery).length @@ -632,7 +632,7 @@ export default createComponent({ function cleanup () { animateTimer !== null && clearTimeout(animateTimer) stopAnimScroll() - unwatchRoute !== void 0 && unwatchRoute() + unwatchRoute?.() } let hadRouteWatcher, hadActivated diff --git a/ui/src/components/tabs/__tests__/QTab.cy.js b/ui/src/components/tabs/__tests__/QTab.cy.js deleted file mode 100644 index aec94afe889..00000000000 --- a/ui/src/components/tabs/__tests__/QTab.cy.js +++ /dev/null @@ -1,79 +0,0 @@ -describe('Tab API', () => { - describe('Props', () => { - describe('Category: content', () => { - describe('(prop): icon', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): alert', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): alert-icon', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): no-caps', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: general', () => { - describe('(prop): name', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): tabindex', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: state', () => { - describe('(prop): disable', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): content-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): ripple', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): default', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/components/tabs/__tests__/QTabs.cy.js b/ui/src/components/tabs/__tests__/QTabs.cy.js deleted file mode 100644 index f55a3e8d1b8..00000000000 --- a/ui/src/components/tabs/__tests__/QTabs.cy.js +++ /dev/null @@ -1,147 +0,0 @@ -describe('Tabs API', () => { - describe('Props', () => { - describe('Category: content', () => { - describe('(prop): vertical', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): outside-arrows', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): mobile-arrows', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): align', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): left-icon', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): right-icon', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): stretch', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): shrink', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): switch-indicator', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): narrow-indicator', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): inline-label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): no-caps', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: content|behavior', () => { - describe('(prop): breakpoint', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: model', () => { - describe('(prop): model-value', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): active-color', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): active-bg-color', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): indicator-color', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): content-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): active-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): dense', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): default', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Events', () => { - describe('(event): update:model-value', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/components/tabs/use-tab.js b/ui/src/components/tabs/use-tab.js index 0e3d9782b05..cbd8e4a4fa5 100644 --- a/ui/src/components/tabs/use-tab.js +++ b/ui/src/components/tabs/use-tab.js @@ -99,13 +99,13 @@ export default function (props, slots, emit, routeData) { )) function onClick (e, keyboard) { - if (keyboard !== true && blurTargetRef.value !== null) { - blurTargetRef.value.focus() + if (keyboard !== true && e?.qAvoidFocus !== true) { + blurTargetRef.value?.focus() } if (props.disable === true) { // we should hinder native navigation though - if (routeData !== void 0 && routeData.hasRouterLink.value === true) { + if (routeData?.hasRouterLink.value === true) { stopAndPrevent(e) } return @@ -140,7 +140,7 @@ export default function (props, slots, emit, routeData) { if ( hardError === void 0 && ( softError === void 0 - || (softError.message !== void 0 && softError.message.startsWith('Avoided redundant navigation') === true) + || (softError.message?.startsWith('Avoided redundant navigation') === true) ) ) { $tabs.updateModel({ name: props.name }) diff --git a/ui/src/components/time/QTime.js b/ui/src/components/time/QTime.js index aade80786ec..37fb083342e 100644 --- a/ui/src/components/time/QTime.js +++ b/ui/src/components/time/QTime.js @@ -264,7 +264,7 @@ export default createComponent({ for (let val = start, index = start; val <= end; val += step, index++) { const actualVal = val + offset, - disable = values !== void 0 && values.includes(actualVal) === false, + disable = values?.includes(actualVal) === false, label = view.value === 'hour' && val === 0 ? (computedFormat24h.value === true ? '00' : '12') : val diff --git a/ui/src/components/tree/QTree.js b/ui/src/components/tree/QTree.js index 0d8b23cbe6f..a9266198957 100644 --- a/ui/src/components/tree/QTree.js +++ b/ui/src/components/tree/QTree.js @@ -334,7 +334,7 @@ export default createComponent({ node[ props.childrenKey ] = Array.isArray(children) === true ? children : [] nextTick(() => { const localMeta = meta.value[ key ] - if (localMeta && localMeta.isParent === true) { + if (localMeta?.isParent === true) { localSetExpanded(key, true) } }) @@ -627,8 +627,7 @@ export default createComponent({ } function blur (key) { - const blurTarget = blurTargets[ key ] - blurTarget && blurTarget.focus() + blurTargets[ key ]?.focus() } function onClick (node, meta, e, keyboard) { diff --git a/ui/src/components/uploader/__tests__/QUploader.cy.js b/ui/src/components/uploader/__tests__/QUploader.cy.js deleted file mode 100644 index 2e3ceb7dc20..00000000000 --- a/ui/src/components/uploader/__tests__/QUploader.cy.js +++ /dev/null @@ -1,191 +0,0 @@ -import QUploader from '../QUploader' - -describe('Uploader API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): auto-upload', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): hide-upload-btn', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: content', () => { - describe('(prop): label', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): no-thumbnails', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: state', () => { - describe('(prop): disable', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): readonly', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): color', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): text-color', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): dark', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): square', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): flat', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): bordered', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Slots', () => { - describe('(slot): header', () => { - it('renders correctly using the scope', () => { - // Taken from ui/dev/src/pages/form/uploader.vue - const headerSlot = ` - -` - - cy.mount(QUploader, { - slots: { - header: headerSlot - } - }) - }) - }) - - describe('(slot): list', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Events', () => { - describe('(event): added', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): removed', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): start', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(event): finish', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Methods', () => { - describe('(method): upload', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): abort', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): reset', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): removeUploadedFiles', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): removeQueuedFiles', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): removeFile', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/components/virtual-scroll/use-virtual-scroll.js b/ui/src/components/virtual-scroll/use-virtual-scroll.js index 87376495d4c..673545ab589 100644 --- a/ui/src/components/virtual-scroll/use-virtual-scroll.js +++ b/ui/src/components/virtual-scroll/use-virtual-scroll.js @@ -40,7 +40,7 @@ const setOverflowAnchor = __QUASAR_SSR__ || window.getComputedStyle(document.bod const el = children[ index ] - if (el && el.dataset) { + if (el?.dataset) { el.dataset.qVsAnchor = '' } }) @@ -422,7 +422,7 @@ export function useVirtualScroll ({ contentEl.addEventListener('focusout', onBlurRefocusFn) setTimeout(() => { - contentEl !== null && contentEl.removeEventListener('focusout', onBlurRefocusFn) + contentEl?.removeEventListener('focusout', onBlurRefocusFn) }) } @@ -533,7 +533,7 @@ export function useVirtualScroll ({ } function onBlurRefocusFn () { - contentRef.value !== null && contentRef.value !== void 0 && contentRef.value.focus() + contentRef.value?.focus() } function localResetVirtualScroll (toIndex, fullReset) { diff --git a/ui/src/composables/__tests__/FieldWrapper.vue b/ui/src/composables/__tests__/FieldWrapper.vue deleted file mode 100644 index bf7fb667a9b..00000000000 --- a/ui/src/composables/__tests__/FieldWrapper.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/ui/src/composables/__tests__/use-anchor.cy.js b/ui/src/composables/__tests__/use-anchor.cy.js deleted file mode 100644 index 807114957ad..00000000000 --- a/ui/src/composables/__tests__/use-anchor.cy.js +++ /dev/null @@ -1,98 +0,0 @@ -import WrapperOne from '../../../components/menu/__tests__/WrapperOne.vue' - -describe('use-anchor API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): target', () => { - it('should use another target using a CSS selector', () => { - cy.mount(WrapperOne, { - props: { - target: '.other-target' - } - }) - - // Click on parent element should not show menu - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('not.exist') - - // Click on other target should show menu - cy.dataCy('other-target') - .click() - cy.dataCy('menu') - .should('exist') - }) - - it('should not show when target is false', () => { - cy.mount(WrapperOne, { - props: { - target: false - } - }) - - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('not.exist') - }) - }) - - describe('(prop): no-parent-event', () => { - it('should not show when clicking parent with no-parent-event true', () => { - cy.mount(WrapperOne, { - props: { - 'no-parent-event': true - } - }) - - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('not.exist') - }) - - it('should show when clicking parent with no-parent-event false', () => { - cy.mount(WrapperOne, { - props: { - 'no-parent-event': false - } - }) - - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('exist') - }) - }) - - describe('(prop): context-menu', () => { - it('should not show when left clicking parent', () => { - cy.mount(WrapperOne, { - props: { - 'context-menu': true - } - }) - - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('not.exist') - }) - - it('should show when right clicking parent', () => { - cy.mount(WrapperOne, { - props: { - 'context-menu': true - } - }) - - cy.dataCy('wrapper') - .rightclick() - cy.dataCy('menu') - .should('exist') - }) - }) - }) - }) -}) diff --git a/ui/src/composables/__tests__/use-field.cy.js b/ui/src/composables/__tests__/use-field.cy.js deleted file mode 100644 index cd391296b17..00000000000 --- a/ui/src/composables/__tests__/use-field.cy.js +++ /dev/null @@ -1,547 +0,0 @@ -import FieldWrapper from './FieldWrapper.vue' -import { ref } from 'vue' -import { vModelAdapter } from '@quasar/quasar-app-extension-testing-e2e-cypress' -import Icons from '../../../../icon-set/material-icons.mjs' - -function mountQFieldWrapper (options) { - return cy.mount(FieldWrapper, options) -} - -function getHostElement () { - return cy.get('.select-root') -} - -const selectOptions = [ 'Option 1', 'Option 2' ] - -describe('use-field API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): autofocus', () => { - it('should autofocus on component', () => { - mountQFieldWrapper({ - props: { - autofocus: true - } - }) - - getHostElement() - .get('.q-field--focused') - .should('exist') - .get('input') - .should('have.focus') - }) - }) - - describe('(prop): for', () => { - it('should set the for attribute for the value and the id of the control', () => { - const forProp = 'hello-there' - - mountQFieldWrapper({ - props: { - for: forProp - } - }) - - getHostElement().should('have.attr', 'for', forProp) - getHostElement().get('input').should('have.id', forProp) - }) - }) - - describe('(prop): name', () => { - it('should use the value of the for prop as control name if name is not set', () => { - const forProp = 'hello-there' - const model = ref(selectOptions[ 0 ]) - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - for: forProp - } - }) - - getHostElement().get('select').should('have.attr', 'name', forProp) - }) - - it('should set the name of the control', () => { - const name = 'hello-there' - const model = ref(selectOptions[ 0 ]) - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - name - } - }) - - getHostElement().get('select').should('have.attr', 'name', name) - }) - }) - }) - - describe('Category: behavior|content', () => { - describe('(prop): loading', () => { - it('should set the component into a loading state', () => { - mountQFieldWrapper({ - props: { - loading: true - } - }) - - getHostElement().get('.q-spinner').should('exist') - }) - }) - - describe('(prop): clearable', () => { - it('should append a clear icon', () => { - const model = ref(selectOptions[ 0 ]) - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - clearable: true, - options: selectOptions - } - }) - - getHostElement().find('input').should('exist').should('have.value', model.value) - getHostElement().get('button[type="button"]').contains(Icons.field.clear).click() - getHostElement().get('input').should('have.value', '') - }) - }) - }) - - describe('Category: content', () => { - describe('(prop): label', () => { - it('should show the label when supplied', () => { - const label = 'Select something' - cy.mount(FieldWrapper, { - props: { - label - } - }) - cy.get('.select-root') - .should('contain.text', label) - }) - - it('should show the label centered when not focused', () => { - const label = 'Select something' - cy.mount(FieldWrapper, { - props: { - label - } - }) - cy.get('.select-root') - .get(`div:contains(${ label })`) - .should('exist') - .should('not.have.class', 'q-field--float') - }) - - it('should show the label stacked when focused', () => { - const label = 'Select something' - cy.mount(FieldWrapper, { - props: { - label - } - }) - cy.get('.select-root [tabindex="0"]') - .focus() - cy.get(`.select-root:contains(${ label })`) - .should('exist') - .should('have.class', 'q-field--float') - }) - }) - - describe('(prop): stack-label', () => { - it('should show the label stacked', () => { - const label = 'Select something' - cy.mount(FieldWrapper, { - props: { - label, - stackLabel: true - } - }) - cy.get(`.select-root:contains(${ label })`) - .should('exist') - .should('have.class', 'q-field--float') - }) - }) - - describe('(prop): hint', () => { - it('should show a hint text', () => { - const hint = 'Select something' - cy.mount(FieldWrapper, { - props: { - hint - } - }) - cy.get(`.select-root:contains(${ hint })`) - .should('exist') - }) - }) - - describe('(prop): hide-hint', () => { - it('should not show a hint text when not focused', () => { - const hint = 'Select something' - cy.mount(FieldWrapper, { - props: { - hint, - hideHint: true - } - }) - cy.get(`.select-root:contains(${ hint })`) - .should('not.exist') - }) - it('should show a hint text when focused', () => { - const hint = 'Select something' - cy.mount(FieldWrapper, { - props: { - hint, - hideHint: true - } - }) - cy.get('.select-root [tabindex="0"]') - .focus() - cy.get(`.select-root:contains(${ hint })`) - .should('exist') - }) - }) - - describe('(prop): prefix', () => { - it('should display a prefix', () => { - const prefix = 'Hello there!' - mountQFieldWrapper({ - props: { - prefix - } - }) - - getHostElement().get('.q-field__prefix').should('exist').should('contain', prefix) - }) - }) - - describe('(prop): suffix', () => { - it('should display a suffix', () => { - const suffix = 'Hello there!' - mountQFieldWrapper({ - props: { - suffix - } - }) - - getHostElement().get('.q-field__suffix').should('exist').should('contain', suffix) - }) - }) - - describe('(prop): clear-icon', () => { - it('should display custom clear-icon when one is set', () => { - const model = ref(selectOptions[ 0 ]) - const clearIcon = 'custom-clear-icon' - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - clearable: true, - clearIcon - } - }) - - getHostElement().get('.q-field__append').get('button').should('contain', clearIcon) - }) - }) - - describe('(prop): label-slot', () => { - it('should force the use of the label slot even when a label prop is set', () => { - const model = ref(selectOptions[ 0 ]) - const labelSlot = 'Hello world' - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - labelSlot: true, - label: 'Hello there' - }, - slots: { - label: () => labelSlot - } - }) - - getHostElement().find('.q-field__label').should('contain.text', labelSlot) - }) - }) - - describe('(prop): bottom-slots', () => { - it('should use a bottom error slot', () => { - const model = ref(selectOptions[ 0 ]) - const bottomSlot = 'Hello there' - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - bottomSlotSlots: true, - error: true - }, - slots: { - error: () => bottomSlot - } - }) - - getHostElement().find('.q-field__bottom') - .should('contain.text', bottomSlot) - }) - - it('should use a bottom hint slot', () => { - const model = ref(selectOptions[ 0 ]) - const bottomSlot = 'Hello there' - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - bottomSlots: true - }, - slots: { - hint: () => bottomSlot - } - }) - - getHostElement().find('.q-field__bottom').should('contain.text', bottomSlot) - }) - - it('should use a bottom counter slot', () => { - const model = ref(selectOptions[ 0 ]) - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - counter: true - } - }) - - getHostElement().find('.q-field__bottom').should('contain.text', model.value.length) - }) - }) - - describe('(prop): counter', () => { - it('should show an automatic counter on bottom right', () => { - const model = ref(selectOptions[ 0 ]) - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - counter: true - } - }) - - getHostElement().get('.q-field__counter').should('contain', model.value.length) - }) - }) - }) - - describe('Category: state', () => { - describe('(prop): disable', () => { - it('should put the component on disable state', () => { - mountQFieldWrapper({ - props: { - disable: true - } - }) - - getHostElement().should('have.class', 'q-field--disabled') - }) - }) - - describe('(prop): readonly', () => { - it('should put the component on readonly state', () => { - mountQFieldWrapper({ - props: { - readonly: true - } - }) - - getHostElement().should('have.class', 'q-field--readonly') - }) - }) - }) - - describe('Category: style', () => { - describe('(prop): label-color', () => { - it('should display a label color', () => { - const label = 'Hello there!' - mountQFieldWrapper({ - props: { - label, - labelColor: 'red' - } - }) - - getHostElement().get('.q-field__label.text-red').should('contain', label) - }) - }) - - describe('(prop): color', () => { - it('should set a color on the component', () => { - mountQFieldWrapper() - getHostElement().get('.q-field__control.text-red').should('not.exist') - - getHostElement().get('input').then(() => { - Cypress.vueWrapper.setProps({ color: 'red' }) - - getHostElement().get('.q-field__control.text-red').should('exist') - }) - }) - }) - - describe('(prop): bg-color', () => { - it('should display a background color', () => { - mountQFieldWrapper({ - props: { - bgColor: 'red' - } - }) - - getHostElement().get('.q-field__control.bg-red').should('exist') - }) - }) - - describe('(prop): hide-bottom-space', () => { - it.skip(' ', () => { - // - }) - }) - - const fieldLooks = [ 'item-aligned', 'dark', 'filled', 'outlined', 'borderless', 'standout', 'rounded', 'square', 'dense' ] - for (const style of fieldLooks) { - describe(`(prop): ${ style }`, () => { - it(`should apply ${ style } design style`, () => { - mountQFieldWrapper({ - props: { - [ style ]: true - } - }) - - getHostElement().get(`.q-field--${ style }`).should('exist') - }) - }) - } - }) - }) - - describe('Slots', () => { - const slots = [ 'prepend', 'append', 'before', 'after', 'label' ] - - describe('(slot): default', () => { - it('should use the default slot', () => { - const model = ref(selectOptions[ 0 ]) - const labelSlot = 'Hello world' - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - labelSlot: true - }, - slots: { - default: () => labelSlot - } - }) - - getHostElement().should('contain.text', labelSlot) - }) - }) - - for (const slot of slots) { - describe(`(slot): ${ slot }`, () => { - it(`should append a '${ slot }' slot`, () => { - const model = ref(selectOptions[ 0 ]) - const labelSlot = 'Hello world' - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options: selectOptions, - labelSlot: true - }, - slots: { - [ slot ]: () => labelSlot - } - }) - - getHostElement().get(`.q-field__${ slot }`).should('contain.text', labelSlot) - }) - }) - } - }) - - describe('Events', () => { - describe('(event): clear', () => { - it('should emit the clear event when the clear button is clicked', () => { - const model = ref(selectOptions[ 0 ]) - const fn = cy.stub() - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - clearable: true, - options: selectOptions, - onClear: fn - } - }) - - getHostElement().get('button[type="button"]') - .contains(Icons.field.clear).click() - .then(() => { - expect(fn).to.be.calledWith() - }) - }) - }) - }) - - describe('Methods', () => { - describe('(method): focus', () => { - it('should focus the component', () => { - mountQFieldWrapper() - - getHostElement() - .get('input') - .should('not.have.focus') - getHostElement() - .then(() => { - Cypress.vueWrapper.vm.focusMethod() - }) - getHostElement() - .get('input') - .should('have.focus') - }) - }) - - describe('(method): blur', () => { - it('should blur the component', () => { - mountQFieldWrapper() - - getHostElement() - .get('input').focus() - getHostElement() - .get('.q-field--focused') - .as('focused-element') - .should('exist') - - cy.get('@focused-element') - .get('input') - .should('have.focus') - - getHostElement() - .then(() => { - Cypress.vueWrapper.vm.blur() - }) - - cy.get('@focused-element').should('not.exist') - getHostElement().get('input').should('not.have.focus') - }) - }) - }) -}) diff --git a/ui/src/composables/__tests__/use-file.cy.js b/ui/src/composables/__tests__/use-file.cy.js deleted file mode 100644 index 6e1a691598c..00000000000 --- a/ui/src/composables/__tests__/use-file.cy.js +++ /dev/null @@ -1,69 +0,0 @@ -describe('use-file API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): multiple', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): accept', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): capture', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): max-file-size', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): max-total-size', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): max-files', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): filter', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Events', () => { - describe('(event): rejected', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Methods', () => { - describe('(method): pickFiles', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): addFiles', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/composables/__tests__/use-fullscreen.cy.js b/ui/src/composables/__tests__/use-fullscreen.cy.js deleted file mode 100644 index f397b839eef..00000000000 --- a/ui/src/composables/__tests__/use-fullscreen.cy.js +++ /dev/null @@ -1,37 +0,0 @@ -describe('use-fullscreen API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): fullscreen', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): no-route-fullscreen-exit', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) - - describe('Methods', () => { - describe('(method): toggleFullscreen', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): setFullscreen', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(method): exitFullscreen', () => { - it.skip(' ', () => { - // - }) - }) - }) -}) diff --git a/ui/src/composables/__tests__/use-model-toggle.cy.js b/ui/src/composables/__tests__/use-model-toggle.cy.js deleted file mode 100644 index 7dc701c39b5..00000000000 --- a/ui/src/composables/__tests__/use-model-toggle.cy.js +++ /dev/null @@ -1,350 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import { vModelAdapter } from '@quasar/quasar-app-extension-testing-e2e-cypress' -import { ref } from 'vue' -import WrapperOne from '../../../components/menu/__tests__/WrapperOne.vue' -import WrapperTwo from '../../../components/menu/__tests__/WrapperTwo.vue' - -describe('use-model-toggle API', () => { - describe('Props', () => { - describe('Category: model', () => { - describe('(prop): model-value', () => { - it('should open the dialog when modifying the model-value', () => { - const model = ref(false) - cy.mount(WrapperOne, { - props: { - ...vModelAdapter(model) - } - }) - cy.dataCy('wrapper') - cy.dataCy('menu') - .should('not.exist') - .then(() => { - model.value = true - cy.dataCy('menu') - .should('exist') - }) - }) - - it('should close the dialog when modifying the model-value', () => { - const model = ref(true) - cy.mount(WrapperOne, { - props: { - ...vModelAdapter(model) - } - }) - cy.dataCy('wrapper') - cy.dataCy('menu') - .should('exist') - .then(() => { - model.value = false - cy.dataCy('menu') - .should('not.exist') - }) - }) - }) - }) - }) - - describe('Events', () => { - describe('(event): update:model-value', () => { - it('should emit @update:model-value event when state changes', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - 'onUpdate:modelValue': fn - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('exist') - .then(() => { - expect(fn).to.be.called - }) - }) - }) - - describe('(event): show', () => { - it('should emit @show event when menu is triggered by parent', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onShow: fn - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - .click() - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.dataCy('menu') - .should('exist') - .wait(300) // Await menu animation - .then(() => { - expect(fn).to.be.called - }) - }) - - it('should emit @show event when component is triggered with the show() method', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onShow: fn - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - cy.dataCy('method-show') - .click({ force: true }) // Element is hidden to prevent clogging the window - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.dataCy('menu') - .should('exist') - .wait(300) // Await menu animation - .then(() => { - expect(fn).to.be.called - }) - }) - }) - - describe('(event): before-show', () => { - it('should emit @before-show event when menu is triggered by parent', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onBeforeShow: fn - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('exist') - .then(() => { - expect(fn).to.be.called - }) - }) - - it('should emit @before-show event when component is triggered with the show() method', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onBeforeShow: fn - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - cy.dataCy('method-show') - .click({ force: true }) // Element is hidden to prevent clogging the window - cy.dataCy('menu') - .should('exist') - .then(() => { - expect(fn).to.be.called - }) - }) - }) - - describe('(event): hide', () => { - it('should emit @hide event when menu is triggered by parent', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onHide: fn, - // After the dialog has been closed, the hide event is fired only after transitionDuration. - // The default value for transitionDuration is 300ms. Hence, we cannot depend on the dialog closing - // to conclude that the event has been fired. So let's set it to 0 so that it fires immediately. - transitionDuration: 0 - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('exist') - .then(() => { - expect(fn).not.to.be.called - }) - - // We are clicking x distance from the left to ensure it always clicks on the body - // For some reason it some times does not work without it - cy.get('body') - .click(499, 0) - cy.dataCy('menu') - .should('not.exist') - .then(() => { - expect(fn).to.be.called - }) - }) - - it('should emit @hide event when component is triggered with the hide() method', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onHide: fn, - // After the dialog has been closed, the hide event is fired only after transitionDuration. - // The default value for transitionDuration is 300ms. Hence, we cannot depend on the dialog closing - // to conclude that the event has been fired. So let's set it to 0 so that it fires immediately. - transitionDuration: 0 - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - cy.dataCy('method-show') - .click({ force: true }) // Element is hidden to prevent clogging the window - cy.dataCy('menu') - .should('exist') - .then(() => { - expect(fn).not.to.be.called - }) - cy.dataCy('method-hide') - .click({ force: true }) - cy.dataCy('menu') - .should('not.exist') // Element is hidden to prevent clogging the window - .then(() => { - expect(fn).to.be.called - }) - }) - - it('should emit @hide event after transition duration', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onHide: fn, - transitionDuration: 500 - } - }) - - expect(fn).not.to.be.called - cy.dataCy('method-show') - .click({ force: true }) - - cy.dataCy('wrapper') - cy.dataCy('method-hide') - .click({ force: true }) - - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.dataCy('menu') - .wait(300) - .then(() => { - expect(fn).not.to.be.called - }) - .wait(500) - .then(() => { - expect(fn).to.be.called - }) - }) - }) - - describe('(event): before-hide', () => { - it('should emit @before-hide event when menu is triggered by parent', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onBeforeHide: fn - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('exist') - .then(() => { - expect(fn).not.to.be.called - }) - cy.get('body') - .click(499, 0) - cy.get('body') - .then(() => { - expect(fn).to.be.called - }) - cy.dataCy('menu') - .should('not.exist') - }) - - it('should emit @before-hide event when component is triggered with the show() method', () => { - const fn = cy.stub() - cy.mount(WrapperOne, { - props: { - onBeforeHide: fn - } - }) - - expect(fn).not.to.be.called - cy.dataCy('wrapper') - cy.dataCy('method-show') - .click({ force: true }) // Element is hidden to prevent clogging the window - cy.dataCy('menu') - .should('exist') - .then(() => { - expect(fn).not.to.be.called - }) - cy.dataCy('method-hide') - .click({ force: true }) - cy.dataCy('method-hide') - .then(() => { - expect(fn).to.be.called - }) - cy.dataCy('menu') - .should('not.exist') - }) - }) - }) - - describe('Methods', () => { - describe('(method): show', () => { - it('should trigger the menu to show', () => { - cy.mount(WrapperOne) - - cy.dataCy('wrapper') - cy.dataCy('method-show') - .click({ force: true }) - cy.dataCy('menu') - .should('exist') - }) - }) - - describe('(method): hide', () => { - it('should trigger the menu to hide', () => { - cy.mount(WrapperOne) - - cy.dataCy('wrapper') - .click() - cy.dataCy('menu') - .should('exist') - cy.dataCy('method-hide') - .click({ force: true }) - cy.dataCy('menu') - .should('not.exist') - }) - }) - - describe('(method): toggle', () => { - it('should toggle the menu', () => { - cy.mount(WrapperTwo) - - cy.dataCy('wrapper') - .then(() => { - // Need to call method from wrapper - // Click a button closes the menu - Cypress.vueWrapper.vm.toggle() - }) - cy.dataCy('menu') - .should('exist') - .then(() => { - // Need to call method from wrapper - // Click a button closes the menu - Cypress.vueWrapper.vm.toggle() - }) - cy.dataCy('menu') - .should('not.exist') - }) - }) - }) -}) diff --git a/ui/src/composables/__tests__/use-portal.cy.js b/ui/src/composables/__tests__/use-portal.cy.js deleted file mode 100644 index f5160569561..00000000000 --- a/ui/src/composables/__tests__/use-portal.cy.js +++ /dev/null @@ -1,4 +0,0 @@ -describe('use-portal API', () => { - describe('Props', () => { - }) -}) diff --git a/ui/src/composables/__tests__/use-router-link.cy.js b/ui/src/composables/__tests__/use-router-link.cy.js deleted file mode 100644 index 12a052fae5a..00000000000 --- a/ui/src/composables/__tests__/use-router-link.cy.js +++ /dev/null @@ -1,55 +0,0 @@ -describe('use-router-link API', () => { - describe('Props', () => { - describe('Category: navigation', () => { - describe('(prop): to', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): exact', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): replace', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): active-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): exact-active-class', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): href', () => { - it.skip(' ', () => { - // - }) - }) - - describe('(prop): target', () => { - it.skip(' ', () => { - // - }) - }) - }) - - describe('Category: state', () => { - describe('(prop): disable', () => { - it.skip(' ', () => { - // - }) - }) - }) - }) -}) diff --git a/ui/src/composables/__tests__/use-validate.cy.js b/ui/src/composables/__tests__/use-validate.cy.js deleted file mode 100644 index 8c4bbef979e..00000000000 --- a/ui/src/composables/__tests__/use-validate.cy.js +++ /dev/null @@ -1,257 +0,0 @@ -import FieldWrapper from './FieldWrapper.vue' -import Icons from '../../../../icon-set/material-icons.mjs' -import { ref } from 'vue' -import { vModelAdapter } from '@quasar/quasar-app-extension-testing-e2e-cypress' - -function mountQFieldWrapper (options) { - return cy.mount(FieldWrapper, options) -} - -function getHostElement () { - return cy.get('.select-root') -} - -describe('use-validate API', () => { - describe('Props', () => { - describe('Category: behavior', () => { - describe('(prop): error', () => { - it('should mark the field as having an error', () => { - mountQFieldWrapper() - getHostElement().should('not.have.class', 'q-field--error') - - getHostElement().then(() => { - Cypress.vueWrapper.setProps({ error: true }) - }) - - getHostElement().should('have.class', 'q-field--error') - }) - }) - - describe('(prop): rules', () => { - it('should validate value using custom validation logic', () => { - const errorMessage = 'Selected value should be greater than 10 characters' - const model = ref('Option 1') - const options = [ 'Option 1', 'Option 2' ] - - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options, - rules: [ val => val.length > 10 || errorMessage ] - } - }) - - getHostElement().then(() => { - model.value = 'Option 2' - }) - getHostElement().get('.q-field__messages').should('contain.text', errorMessage) - }) - - it('should validate email using inbuilt validation logic', () => { - const errorMessage = 'Enter a valid email address' - const model = ref('Option 1') - const options = [ 'Option 1', 'Option 2' ] - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options, - rules: [ (val, rules) => rules.email(val) || errorMessage ] - } - }) - getHostElement().then(() => { - model.value = 'Option 2' - }) - getHostElement().get('.q-field__messages').should('contain.text', errorMessage) - }) - }) - - describe('(prop): reactive-rules', () => { - it('should trigger validation when there\'s a change of rules', () => { - const errorMessage = 'Error message' - const model = ref('Option 1') - const options = [ 'Option 1', 'Option 2' ] - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options - } - }) - - getHostElement().then(() => { - Cypress.vueWrapper.setProps({ rules: [ (value) => value.length < 3 || errorMessage ] }) - - Cypress.vueWrapper.vm.focusMethod() - Cypress.vueWrapper.vm.blur() - }) - getHostElement().get('.q-field__messages').should('not.exist') - - getHostElement().then(() => { - Cypress.vueWrapper.setProps({ reactiveRules: true, rules: [ (value, rules) => rules.email(value) || errorMessage ] }) - }) - getHostElement().get('.q-field__messages').should('contain.text', errorMessage) - }) - }) - - describe('(prop): lazy-rules', () => { - it('should validate the input only when component loses focus', () => { - const errorMessage = 'Use a max 3 of characters' - const model = ref('Option 1') - const options = [ 'Option 1', 'Option 2' ] - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options, - lazyRules: true, - rules: [ () => errorMessage ] - } - }) - - getHostElement().select('Option 2') - getHostElement().get('.q-field__messages').should('not.contain', errorMessage) - - getHostElement().then(() => { - Cypress.vueWrapper.vm.blur() - getHostElement().get('.q-field__messages').should('contain.text', errorMessage) - }) - }) - }) - }) - - describe('Category: content', () => { - describe('(prop): error-message', () => { - it('should show an error-message when error is true', () => { - const message = 'Please select something' - cy.mount(FieldWrapper, { - props: { - error: true, - errorMessage: message - } - }) - cy.get('.select-root') - .should('include.text', message) - }) - - it('should not show an error-message when error is false', () => { - const message = 'Please select something' - cy.mount(FieldWrapper, { - props: { - error: false, - errorMessage: message - } - }) - cy.get('.select-root') - .should('not.include.text', message) - }) - }) - - describe('(prop): no-error-icon', () => { - it('should not show an error icon when error is true', () => { - cy.mount(FieldWrapper, { - props: { - error: true, - noErrorIcon: true - } - }) - cy.get('.select-root') - .get(`i:contains(${ Icons.field.error })`) - .should('not.exist') - }) - - it('should show an error icon when error is true an no-error-icon is false', () => { - cy.mount(FieldWrapper, { - props: { - error: true, - noErrorIcon: false - } - }) - cy.get('.select-root') - .get(`i:contains(${ Icons.field.error })`) - .should('exist') - }) - }) - }) - - describe('Category: model', () => { - describe('(prop): model-value', () => { - it('should correctly update the model value', () => { - const model = ref() - const options = [ 'Option 1', 'Option 2' ] - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options - } - }) - getHostElement().get('input').should('not.have.value', options[ 0 ]) - - getHostElement().then(() => { - model.value = options[ 0 ] - }) - - getHostElement().get('input').should('have.value', options[ 0 ]) - }) - }) - }) - }) - - describe('Methods', () => { - describe('(method): resetValidation', () => { - it('should reset validation', () => { - const errorMessage = 'Use a max 3 of characters' - const model = ref('Option 1') - const options = [ 'Option 1', 'Option 2' ] - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options, - rules: [ val => val.length <= 3 || errorMessage ], - lazyRules: 'ondemand' - } - }) - - getHostElement().then(() => { - model.value = 'Option 2' - }) - getHostElement().get('.q-field__messages').should('not.contain', errorMessage) - - getHostElement().then(() => { - Cypress.vueWrapper.vm.validate() - getHostElement().get('.q-field__messages').should('contain', errorMessage) - }) - - getHostElement().then(() => { - Cypress.vueWrapper.vm.resetValidation() - getHostElement().get('.q-field__messages').should('not.contain', errorMessage) - }) - }) - }) - - describe('(method): validate', () => { - it('should validate the input only when component\'s validate() method is called', () => { - const errorMessage = 'Use a max 3 of characters' - const model = ref('Option 1') - const options = [ 'Option 1', 'Option 2' ] - mountQFieldWrapper({ - props: { - ...vModelAdapter(model), - options, - rules: [ val => val.length <= 3 || errorMessage ], - lazyRules: 'ondemand' - } - }) - - getHostElement().then(() => { - model.value = 'Option 2' - }) - getHostElement().get('.q-field__messages').should('not.contain', errorMessage) - - getHostElement().get('input').then(() => { - Cypress.vueWrapper.vm.blur() - getHostElement().get('.q-field__messages').should('not.contain', errorMessage) - Cypress.vueWrapper.vm.validate() - getHostElement().get('.q-field__messages').should('contain', errorMessage) - }) - }) - }) - }) -}) diff --git a/ui/src/composables/private.use-field/use-field.js b/ui/src/composables/private.use-field/use-field.js index 04e0e98a3c5..a2e015b4a64 100644 --- a/ui/src/composables/private.use-field/use-field.js +++ b/ui/src/composables/private.use-field/use-field.js @@ -265,12 +265,12 @@ export default function (state) { function focusHandler () { const el = document.activeElement - let target = state.targetRef !== void 0 && state.targetRef.value + let target = state.targetRef?.value if (target && (el === null || el.id !== state.targetUid.value)) { target.hasAttribute('tabindex') === true || (target = target.querySelector('[tabindex]')) - if (target && target !== el) { - target.focus({ preventScroll: true }) + if (target !== el) { + target?.focus({ preventScroll: true }) } } } @@ -318,7 +318,7 @@ export default function (state) { emit('blur', e) } - then !== void 0 && then() + then?.() }) } @@ -327,7 +327,7 @@ export default function (state) { stopAndPrevent(e) if ($q.platform.is.mobile !== true) { - const el = (state.targetRef !== void 0 && state.targetRef.value) || state.rootRef.value + const el = state.targetRef?.value || state.rootRef.value el.focus() } else if (state.rootRef.value.contains(document.activeElement) === true) { diff --git a/ui/src/composables/private.use-file/use-file.js b/ui/src/composables/private.use-file/use-file.js index 8cbe4d1c123..4926f356752 100644 --- a/ui/src/composables/private.use-file/use-file.js +++ b/ui/src/composables/private.use-file/use-file.js @@ -19,7 +19,10 @@ function filterFiles (files, rejectedFiles, failedPropValidation, filterFn) { } function stopAndPreventDrag (e) { - e && e.dataTransfer && (e.dataTransfer.dropEffect = 'copy') + if (e?.dataTransfer) { + e.dataTransfer.dropEffect = 'copy' + } + stopAndPrevent(e) } @@ -69,13 +72,13 @@ export default function ({ e = { target: null } } - if (e.target !== null && e.target.matches('input[type="file"]') === true) { + if (e.target?.matches('input[type="file"]') === true) { // stop propagation if it's not a real pointer event e.clientX === 0 && e.clientY === 0 && stop(e) } else { const input = getFileInput() - input && input !== e.target && input.click(e) + if (input !== e.target) input?.click(e) } } } diff --git a/ui/src/composables/private.use-model-toggle/use-model-toggle.js b/ui/src/composables/private.use-model-toggle/use-model-toggle.js index c2f2c7e868b..672ff183796 100644 --- a/ui/src/composables/private.use-model-toggle/use-model-toggle.js +++ b/ui/src/composables/private.use-model-toggle/use-model-toggle.js @@ -41,8 +41,8 @@ export default function ({ function show (evt) { if ( - props.disable === true - || (evt !== void 0 && evt.qAnchorHandled === true) + (props.disable === true) + || (evt?.qAnchorHandled === true) || (canShow !== void 0 && canShow(evt) !== true) ) return diff --git a/ui/src/composables/private.use-refocus-target/use-refocus-target.js b/ui/src/composables/private.use-refocus-target/use-refocus-target.js index cbfd52f7ba6..07f3a14ca6a 100644 --- a/ui/src/composables/private.use-refocus-target/use-refocus-target.js +++ b/ui/src/composables/private.use-refocus-target/use-refocus-target.js @@ -18,18 +18,19 @@ export default function (props, rootRef) { function refocusTarget (e) { const root = rootRef.value - if (e !== void 0 && e.type.indexOf('key') === 0) { + if (e?.qAvoidFocus === true) return + + if (e?.type.indexOf('key') === 0) { if ( - root !== null - && document.activeElement !== root - && root.contains(document.activeElement) === true + document.activeElement !== root + && root?.contains(document.activeElement) === true ) { root.focus() } } else if ( refocusRef.value !== null - && (e === void 0 || (root !== null && root.contains(e.target) === true)) + && ((e === void 0) || (root?.contains(e.target) === true)) ) { refocusRef.value.focus() } diff --git a/ui/src/composables/private.use-validate/use-validate.js b/ui/src/composables/private.use-validate/use-validate.js index 6d52d7703e7..00a23c8079f 100644 --- a/ui/src/composables/private.use-validate/use-validate.js +++ b/ui/src/composables/private.use-validate/use-validate.js @@ -201,7 +201,7 @@ export default function (focused, innerLoading) { const debouncedValidate = debounce(validate, 0) onBeforeUnmount(() => { - unwatchRules !== void 0 && unwatchRules() + unwatchRules?.() debouncedValidate.cancel() }) diff --git a/ui/src/directives/intersection/Intersection.js b/ui/src/directives/intersection/Intersection.js index 56e358fe394..c7997685f84 100644 --- a/ui/src/directives/intersection/Intersection.js +++ b/ui/src/directives/intersection/Intersection.js @@ -28,7 +28,7 @@ function update (el, ctx, value) { if (changed === true) { ctx.cfg = cfg - ctx.observer !== void 0 && ctx.observer.unobserve(el) + ctx.observer?.unobserve(el) ctx.observer = new IntersectionObserver(([ entry ]) => { if (typeof ctx.handler === 'function') { @@ -62,7 +62,7 @@ function destroy (el) { const ctx = el.__qvisible if (ctx !== void 0) { - ctx.observer !== void 0 && ctx.observer.unobserve(el) + ctx.observer?.unobserve(el) delete el.__qvisible } } diff --git a/ui/src/directives/morph/Morph.js b/ui/src/directives/morph/Morph.js index 51a2b65abac..5d0fc939c91 100644 --- a/ui/src/directives/morph/Morph.js +++ b/ui/src/directives/morph/Morph.js @@ -44,7 +44,7 @@ function trigger (group) { }, ...to.opts, onEnd (dir, aborted) { - to.opts.onEnd !== void 0 && to.opts.onEnd(dir, aborted) + to.opts.onEnd?.(dir, aborted) if (aborted === true) return @@ -206,7 +206,7 @@ export default createDirective(__QUASAR_SSR_SERVER__ group.queue = group.queue.filter(item => item !== ctx) if (group.queue.length === 0) { - group.cancel !== void 0 && group.cancel() + group.cancel?.() delete morphGroups[ ctx.group ] } } diff --git a/ui/src/directives/mutation/Mutation.js b/ui/src/directives/mutation/Mutation.js index c0d767faa28..82f5e7d80f0 100644 --- a/ui/src/directives/mutation/Mutation.js +++ b/ui/src/directives/mutation/Mutation.js @@ -12,7 +12,7 @@ const defaultCfg = { function update (el, ctx, value) { ctx.handler = value - ctx.observer !== void 0 && ctx.observer.disconnect() + ctx.observer?.disconnect() ctx.observer = new MutationObserver(list => { if (typeof ctx.handler === 'function') { @@ -30,7 +30,7 @@ function destroy (el) { const ctx = el.__qmutation if (ctx !== void 0) { - ctx.observer !== void 0 && ctx.observer.disconnect() + ctx.observer?.disconnect() delete el.__qmutation } } diff --git a/ui/src/directives/touch-hold/TouchHold.js b/ui/src/directives/touch-hold/TouchHold.js index 60e834c84b6..07ddec134b9 100644 --- a/ui/src/directives/touch-hold/TouchHold.js +++ b/ui/src/directives/touch-hold/TouchHold.js @@ -106,7 +106,7 @@ export default createDirective(__QUASAR_SSR_SERVER__ cleanEvt(ctx, 'temp') // delay needed otherwise selection still occurs - ctx.styleCleanup !== void 0 && ctx.styleCleanup(ctx.triggered) + ctx.styleCleanup?.(ctx.triggered) if (ctx.triggered === true) { evt !== void 0 && stopAndPrevent(evt) @@ -166,7 +166,7 @@ export default createDirective(__QUASAR_SSR_SERVER__ cleanEvt(ctx, 'temp') ctx.timer !== void 0 && clearTimeout(ctx.timer) - ctx.styleCleanup !== void 0 && ctx.styleCleanup() + ctx.styleCleanup?.() delete el.__qtouchhold } diff --git a/ui/src/directives/touch-pan/TouchPan.js b/ui/src/directives/touch-pan/TouchPan.js index e47a5be9333..f13989ad4ae 100644 --- a/ui/src/directives/touch-pan/TouchPan.js +++ b/ui/src/directives/touch-pan/TouchPan.js @@ -355,7 +355,7 @@ export default createDirective(__QUASAR_SSR_SERVER__ client.is.firefox === true && preventDraggable(el, false) if (abort === true) { - ctx.styleCleanup !== void 0 && ctx.styleCleanup() + ctx.styleCleanup?.() if (ctx.event.detected !== true && ctx.initialEvent !== void 0) { ctx.initialEvent.target.dispatchEvent(ctx.initialEvent.event) @@ -426,7 +426,7 @@ export default createDirective(__QUASAR_SSR_SERVER__ cleanEvt(ctx, 'temp') client.is.firefox === true && preventDraggable(el, false) - ctx.styleCleanup !== void 0 && ctx.styleCleanup() + ctx.styleCleanup?.() delete el.__qtouchpan } diff --git a/ui/src/directives/touch-repeat/TouchRepeat.js b/ui/src/directives/touch-repeat/TouchRepeat.js index 0c86adc9e55..4357d91b829 100644 --- a/ui/src/directives/touch-repeat/TouchRepeat.js +++ b/ui/src/directives/touch-repeat/TouchRepeat.js @@ -186,8 +186,8 @@ export default createDirective(__QUASAR_SSR_SERVER__ end (evt) { if (ctx.event === void 0) return - ctx.styleCleanup !== void 0 && ctx.styleCleanup(true) - evt !== void 0 && ctx.event.repeatCount > 0 && stopAndPrevent(evt) + ctx.styleCleanup?.(true) + if ((evt !== void 0) && (ctx.event.repeatCount > 0)) stopAndPrevent(evt) cleanEvt(ctx, 'temp') @@ -248,7 +248,7 @@ export default createDirective(__QUASAR_SSR_SERVER__ cleanEvt(ctx, 'main') cleanEvt(ctx, 'temp') - ctx.styleCleanup !== void 0 && ctx.styleCleanup() + ctx.styleCleanup?.() delete el.__qtouchrepeat } diff --git a/ui/src/directives/touch-swipe/TouchSwipe.js b/ui/src/directives/touch-swipe/TouchSwipe.js index e012b670a0f..1183b7a86c5 100644 --- a/ui/src/directives/touch-swipe/TouchSwipe.js +++ b/ui/src/directives/touch-swipe/TouchSwipe.js @@ -221,8 +221,8 @@ export default createDirective(__QUASAR_SSR_SERVER__ cleanEvt(ctx, 'temp') client.is.firefox === true && preventDraggable(el, false) - ctx.styleCleanup !== void 0 && ctx.styleCleanup(true) - evt !== void 0 && ctx.event.dir !== false && stopAndPrevent(evt) + ctx.styleCleanup?.(true) + if ((evt !== void 0) && (ctx.event.dir !== false)) stopAndPrevent(evt) ctx.event = void 0 } @@ -268,7 +268,7 @@ export default createDirective(__QUASAR_SSR_SERVER__ cleanEvt(ctx, 'temp') client.is.firefox === true && preventDraggable(el, false) - ctx.styleCleanup !== void 0 && ctx.styleCleanup() + ctx.styleCleanup?.() delete el.__qtouchswipe } diff --git a/ui/src/plugins/lang/Lang.test.js b/ui/src/plugins/lang/Lang.test.js index e7a41f0308e..f2467117015 100644 --- a/ui/src/plugins/lang/Lang.test.js +++ b/ui/src/plugins/lang/Lang.test.js @@ -61,6 +61,12 @@ describe('[Lang API]', () => { pagination: expect.any(Function), columns: expect.any(String) }, + pagination: { + first: expect.any(String), + last: expect.any(String), + next: expect.any(String), + prev: expect.any(String) + }, editor: { url: expect.any(String), bold: expect.any(String), @@ -173,6 +179,12 @@ describe('[Lang API]', () => { pagination: (start, end, total) => start + '-' + end + ' of ' + total, columns: 'Columns' }, + pagination: { + first: expect.any(String), + last: expect.any(String), + next: expect.any(String), + prev: expect.any(String) + }, editor: { url: 'URL', bold: 'Bold', diff --git a/ui/src/plugins/loading/Loading.js b/ui/src/plugins/loading/Loading.js index bd5940f225f..d8f3431135a 100644 --- a/ui/src/plugins/loading/Loading.js +++ b/ui/src/plugins/loading/Loading.js @@ -34,7 +34,7 @@ const originalDefaults = { const defaults = { ...originalDefaults } function registerProps (opts) { - if (opts && opts.group !== void 0 && activeGroups[ opts.group ] !== void 0) { + if (opts?.group !== void 0 && activeGroups[ opts.group ] !== void 0) { return Object.assign(activeGroups[ opts.group ], opts) } diff --git a/ui/src/plugins/notify/Notify.js b/ui/src/plugins/notify/Notify.js index 5396d7e2ed4..541861a6b84 100644 --- a/ui/src/plugins/notify/Notify.js +++ b/ui/src/plugins/notify/Notify.js @@ -149,7 +149,7 @@ function addNotification (config, $q, originalApi) { ? defaults.actions : [] ).concat( - notifTypes[ config.type ] !== void 0 && Array.isArray(notifTypes[ config.type ].actions) === true + Array.isArray(notifTypes[ config.type ]?.actions) === true ? notifTypes[ config.type ].actions : [] ) diff --git a/ui/src/plugins/private.body/Body.js b/ui/src/plugins/private.body/Body.js index fc9e9c90a49..b8cceb32641 100644 --- a/ui/src/plugins/private.body/Body.js +++ b/ui/src/plugins/private.body/Body.js @@ -100,7 +100,7 @@ export default { const { $q, ssrContext } = opts const cls = getBodyClasses($q.platform, $q.config) - if ($q.config.screen !== void 0 && $q.config.screen.bodyClass === true) { + if ($q.config.screen?.bodyClass === true) { cls.push('screen--xs') } diff --git a/ui/src/plugins/private.history/History.js b/ui/src/plugins/private.history/History.js index 100264b0a80..58f7cec3578 100644 --- a/ui/src/plugins/private.history/History.js +++ b/ui/src/plugins/private.history/History.js @@ -51,7 +51,7 @@ export default { const qConf = $q.config[ cordova === true ? 'cordova' : 'capacitor' ] - if (qConf !== void 0 && qConf.backButton === false) return + if (qConf?.backButton === false) return // if the '@capacitor/app' plugin is not installed // then we got nothing to do diff --git a/ui/src/plugins/screen/Screen.js b/ui/src/plugins/screen/Screen.js index ccf6262eea8..84e60a4a707 100644 --- a/ui/src/plugins/screen/Screen.js +++ b/ui/src/plugins/screen/Screen.js @@ -71,7 +71,7 @@ export default createReactivePlugin({ visualViewport.height * visualViewport.scale + window.innerHeight - scrollingElement.clientHeight ] - const classes = $q.config.screen !== void 0 && $q.config.screen.bodyClasses === true + const classes = $q.config.screen?.bodyClasses === true this.__update = force => { const [ w, h ] = getSize() diff --git a/ui/src/utils/morph/morph.js b/ui/src/utils/morph/morph.js index 68bd7b11881..9e3111c5360 100644 --- a/ui/src/utils/morph/morph.js +++ b/ui/src/utils/morph/morph.js @@ -256,7 +256,7 @@ export default function morph (_options) { // we clean the clone of the initial element elFromClone.remove() - elFromTween !== void 0 && elFromTween.remove() + elFromTween?.remove() options.hideFromClone === true && elFromClone.classList.remove('q-morph--internal') @@ -301,7 +301,7 @@ export default function morph (_options) { // we clean the clone of the initial element elFromClone.remove() - elFromTween !== void 0 && elFromTween.remove() + elFromTween?.remove() options.hideFromClone === true && elFromClone.classList.remove('q-morph--internal') @@ -516,12 +516,15 @@ export default function morph (_options) { elTo.style.cssText = elToStyleSaved elTo.className = elToClassSaved } - elToClone.parentNode === elToParent && elToParent.insertBefore(elTo, elToClone) + + if (elToClone.parentNode === elToParent) { + elToParent.insertBefore(elTo, elToClone) + } // we clean the spacers elFromClone.remove() elToClone.remove() - elFromTween !== void 0 && elFromTween.remove() + elFromTween?.remove() // cancel will be no longer available cancel = () => false @@ -677,9 +680,9 @@ export default function morph (_options) { }) const cleanup = abort => { - animationFromClone !== void 0 && animationFromClone.cancel() - animationFromTween !== void 0 && animationFromTween.cancel() - animationToClone !== void 0 && animationToClone.cancel() + animationFromClone?.cancel() + animationFromTween?.cancel() + animationToClone?.cancel() animationTo.cancel() animationTo.removeEventListener('finish', cleanup) @@ -721,9 +724,9 @@ export default function morph (_options) { endElementTo = endElementTo !== true - animationFromClone !== void 0 && animationFromClone.reverse() - animationFromTween !== void 0 && animationFromTween.reverse() - animationToClone !== void 0 && animationToClone.reverse() + animationFromClone?.reverse() + animationFromTween?.reverse() + animationToClone?.reverse() animationTo.reverse() return true diff --git a/ui/src/utils/open-url/open-url.js b/ui/src/utils/open-url/open-url.js index 4a1541fe4e2..52832c6b220 100644 --- a/ui/src/utils/open-url/open-url.js +++ b/ui/src/utils/open-url/open-url.js @@ -22,10 +22,10 @@ function openWindow (url, reject, windowFeatures) { let open = window.open if (Platform.is.cordova === true) { - if (cordova !== void 0 && cordova.InAppBrowser !== void 0 && cordova.InAppBrowser.open !== void 0) { + if (cordova?.InAppBrowser?.open !== void 0) { open = cordova.InAppBrowser.open } - else if (navigator !== void 0 && navigator.app !== void 0) { + else if (navigator?.app !== void 0) { return navigator.app.loadUrl(url, { openExternal: true }) @@ -39,7 +39,7 @@ function openWindow (url, reject, windowFeatures) { return win } else { - reject && reject() + reject?.() } } diff --git a/ui/src/utils/private.dialog/create-dialog.js b/ui/src/utils/private.dialog/create-dialog.js index ff6ef516f10..8a0f4b33799 100644 --- a/ui/src/utils/private.dialog/create-dialog.js +++ b/ui/src/utils/private.dialog/create-dialog.js @@ -57,14 +57,14 @@ export default function (DefaultComponent, supportsCustomComponent, parentApp) { const el = createGlobalNode(false, 'dialog') const applyState = cmd => { - if (dialogRef.value !== null && dialogRef.value[ cmd ] !== void 0) { + if (dialogRef.value?.[ cmd ] !== void 0) { dialogRef.value[ cmd ]() return } const target = vm.$.subTree - if (target && target.component) { + if (target?.component) { // account for "script setup" way of declaring component if (target.component.proxy && target.component.proxy[ cmd ]) { target.component.proxy[ cmd ]() diff --git a/ui/src/utils/private.portal/portal.js b/ui/src/utils/private.portal/portal.js index 4023a60c584..6e8801cc49a 100644 --- a/ui/src/utils/private.portal/portal.js +++ b/ui/src/utils/private.portal/portal.js @@ -25,7 +25,7 @@ export function closePortalMenus (proxy, evt) { // and hide it too const parent = getParentProxy(proxy) - if (parent !== void 0 && parent.$options.name === 'QPopupProxy') { + if (parent?.$options.name === 'QPopupProxy') { proxy.hide(evt) return parent }