diff --git a/package-lock.json b/package-lock.json index 41774c1c1c..01fb3b2ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@nextcloud/logger": "^3.0.3", "@nextcloud/router": "^3.1.0", "@nextcloud/sharing": "^0.4.0", + "@nextcloud/vue-select": "^4.0.0", "@vuepic/vue-datepicker": "^11.0.3", "@vueuse/components": "^14.2.1", "@vueuse/core": "^14.2.1", @@ -51,8 +52,7 @@ "unist-builder": "^4.0.0", "unist-util-visit": "^5.1.0", "vue": "^3.5.18", - "vue-router": "^5.0.4", - "vue-select": "^4.0.0-beta.6" + "vue-router": "^5.0.4" }, "devDependencies": { "@babel/plugin-syntax-import-assertions": "^7.28.6", @@ -3695,6 +3695,18 @@ "vite": "^7.1.10" } }, + "node_modules/@nextcloud/vue-select": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-4.0.0.tgz", + "integrity": "sha512-f0ZW5ySRmtgMoqYWvGOZFBEqS7iAt7t6Zh9yoXN7/GIiKz3csig3AO5WhV6THOtLBHOkO+PnOQdZq6MjiCNeEw==", + "license": "MIT", + "engines": { + "node": "^22 || ^24" + }, + "peerDependencies": { + "vue": "^3" + } + }, "node_modules/@nextcloud/webpack-vue-config": { "version": "6.0.1", "resolved": "git+ssh://git@github.com/nextcloud/webpack-vue-config.git#118624106ca61b2c2678ad8d693a110bbb8396cc", @@ -26468,15 +26480,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/vue-select": { - "version": "4.0.0-beta.6", - "resolved": "https://registry.npmjs.org/vue-select/-/vue-select-4.0.0-beta.6.tgz", - "integrity": "sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==", - "license": "MIT", - "peerDependencies": { - "vue": "3.x" - } - }, "node_modules/vue-styleguidist": { "version": "4.72.4", "resolved": "https://registry.npmjs.org/vue-styleguidist/-/vue-styleguidist-4.72.4.tgz", diff --git a/package.json b/package.json index cb869550eb..ef918eb71d 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@nextcloud/logger": "^3.0.3", "@nextcloud/router": "^3.1.0", "@nextcloud/sharing": "^0.4.0", + "@nextcloud/vue-select": "^4.0.0", "@vuepic/vue-datepicker": "^11.0.3", "@vueuse/components": "^14.2.1", "@vueuse/core": "^14.2.1", @@ -117,8 +118,7 @@ "unist-builder": "^4.0.0", "unist-util-visit": "^5.1.0", "vue": "^3.5.18", - "vue-router": "^5.0.4", - "vue-select": "^4.0.0-beta.6" + "vue-router": "^5.0.4" }, "devDependencies": { "@babel/plugin-syntax-import-assertions": "^7.28.6", diff --git a/src/components/NcSelect/NcSelect.vue b/src/components/NcSelect/NcSelect.vue index 17bb64b255..b212773289 100644 --- a/src/components/NcSelect/NcSelect.vue +++ b/src/components/NcSelect/NcSelect.vue @@ -395,8 +395,8 @@ import { offset, shift, } from '@floating-ui/dom' +import { VueSelect } from '@nextcloud/vue-select' import { h, warn } from 'vue' -import VueSelect from 'vue-select' import ChevronDown from 'vue-material-design-icons/ChevronDown.vue' import Close from 'vue-material-design-icons/Close.vue' import NcEllipsisedOption from '../NcEllipsisedOption/NcEllipsisedOption.vue' @@ -405,11 +405,6 @@ import { t } from '../../l10n.ts' import { createElementId } from '../../utils/createElementId.ts' import { isLegacy } from '../../utils/legacy.ts' -// TODO: Use @nextcloud/vue-select once a vue 3 version is available. -// Until then, all @nextcloud/vue-select specific improvements won't be available. -// E.g. the `limit` prop has no effect, currently. -import 'vue-select/dist/vue-select.css' - export default { name: 'NcSelect', diff --git a/tests/component/components/NcSelectUsers/NcSelectUsers.spec.ts b/tests/component/components/NcSelectUsers/NcSelectUsers.spec.ts index 96394f99ca..67eabe548a 100644 --- a/tests/component/components/NcSelectUsers/NcSelectUsers.spec.ts +++ b/tests/component/components/NcSelectUsers/NcSelectUsers.spec.ts @@ -9,8 +9,8 @@ import UserSelect from './UserSelect.story.vue' test('has options', async ({ mount, page }) => { const component = await mount(UserSelect) - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').click() + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').click() expect(await page.getByRole('option').all()).toHaveLength(3) await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible() @@ -21,8 +21,8 @@ test('has options', async ({ mount, page }) => { test('can filter by name', async ({ mount, page }) => { const component = await mount(UserSelect) - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').fill('Em') + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').fill('Em') await expect(page.getByRole('option', { name: 'Emma' })).toBeVisible() expect(await page.getByRole('option').all()).toHaveLength(1) @@ -31,8 +31,8 @@ test('can filter by name', async ({ mount, page }) => { test('can filter by mail', async ({ mount, page }) => { const component = await mount(UserSelect) - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').fill('olivia@example') + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').fill('olivia@example') await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible() expect(await page.getByRole('option').all()).toHaveLength(1) @@ -49,18 +49,18 @@ test( async ({ mount, page }) => { const component = await mount(UserSelect) - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').fill('O. <') + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').fill('O. <') // should not exist right now as neither Name no email provided await expect(page.getByText('No results')).toBeVisible() expect(await page.getByRole('option', { name: 'Olivia' }).all()).toHaveLength(0) - await component.getByRole('searchbox').fill('O. ') + await component.getByRole('combobox').fill('O. ') // now it should match the email await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible() expect(await page.getByRole('option').all()).toHaveLength(1) @@ -75,8 +75,8 @@ test('can select option', async ({ mount, page }) => { }, }) - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').click() + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').click() await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible() await page.getByRole('option', { name: 'Olivia' }).click() @@ -86,8 +86,8 @@ test('can select option', async ({ mount, page }) => { id: '0-olivia', }) - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').click() + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').click() await expect(page.getByRole('option', { name: 'John' })).toBeVisible() await page.getByRole('option', { name: 'John' }).click() @@ -109,14 +109,14 @@ test('can select multiple option', async ({ mount, page }) => { }, }) - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').click() + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').click() await expect(page.getByRole('option', { name: 'Olivia' })).toBeVisible() await page.getByRole('option', { name: 'Olivia' }).click() - await expect(component.getByRole('searchbox')).toBeVisible() - await component.getByRole('searchbox').click() + await expect(component.getByRole('combobox')).toBeVisible() + await component.getByRole('combobox').click() await expect(page.getByRole('option', { name: 'John' })).toBeVisible() await page.getByRole('option', { name: 'John' }).click() diff --git a/vitest.config.ts b/vitest.config.ts index 63d21f2d8c..cdc6a02aad 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -28,6 +28,15 @@ export default async (env) => { 'node_modules/**', 'docs/**', ], + // @nextcloud/vue-select's ESM bundle imports its own CSS inline + // (`import './assets/*.css'` at the top of dist/index.mjs). Routing + // it through Vite's transform pipeline lets the CSS import be + // handled instead of reaching Node's native ESM loader. + server: { + deps: { + inline: ['@nextcloud/vue-select'], + }, + }, }, }) }