Skip to content

Fix Rollup failed to resolve import $app/environment #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/link-check-config.json

This file was deleted.

17 changes: 8 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
"@playwright/test": "^1.52.0",
"@stylistic/eslint-plugin": "^4.2.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.21.0",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/package": "2.3.11",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/node": "^22.15.18",
"@vitest/coverage-v8": "^3.1.3",
"eslint": "^9.26.0",
"eslint-plugin-svelte": "^3.6.0",
"@types/node": "^22.15.20",
"@vitest/coverage-v8": "^3.1.4",
"eslint": "^9.27.0",
"eslint-plugin-svelte": "^3.8.2",
"globals": "^16.1.0",
"hastscript": "^9.0.1",
"highlight.js": "^11.11.1",
Expand All @@ -47,17 +47,16 @@
"prettier-plugin-svelte": "^3.4.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"svelte": "^5.30.1",
"svelte": "^5.32.1",
"svelte-check": "^4.2.1",
"svelte-multiselect": "11.0.0-rc.1",
"svelte-preprocess": "^6.0.3",
"svelte-toc": "^0.6.0",
"svelte-toc": "^0.6.1",
"svelte-zoo": "^0.4.18",
"svelte2tsx": "^0.7.39",
"typescript": "5.8.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5",
"vitest": "^3.1.3"
"vitest": "^3.1.4"
},
"keywords": [
"svelte",
Expand Down
13 changes: 5 additions & 8 deletions src/lib/MultiSelect.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script lang="ts">
import { browser } from '$app/environment'
import { tick } from 'svelte'
import { flip } from 'svelte/animate'
import type { FocusEventHandler, KeyboardEventHandler } from 'svelte/elements'
Expand Down Expand Up @@ -489,13 +488,11 @@
form_input?.setCustomValidity(``)
})

function portal(
node: HTMLElement,
params: { target_node: HTMLElement | null; active?: boolean },
) {
type PortalParams = { target_node: HTMLElement | null; active?: boolean }
function portal(node: HTMLElement, params: PortalParams) {
let { target_node, active } = params
if (!active) return
let render_in_place = !browser || !document.body.contains(node)
let render_in_place = typeof window === `undefined` || !document.body.contains(node)

if (!render_in_place) {
document.body.appendChild(node)
Expand All @@ -521,9 +518,9 @@
})

return {
update(params: { target_node: HTMLElement | null }) {
update(params: PortalParams) {
target_node = params.target_node
render_in_place = !browser || !document.body.contains(node)
render_in_place = typeof window === `undefined` || !document.body.contains(node)
if (open && !render_in_place && target_node) tick().then(update_position)
else if (!open || !target_node) node.hidden = true
},
Expand Down
176 changes: 141 additions & 35 deletions tests/MultiSelect.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test } from '@playwright/test'
import { foods } from '../src/site/options'
import {
colors as demo_colors,
languages as demo_languages,
foods,
} from '../src/site/options'

// to run tests in this file, use `npm run test:e2e`

Expand Down Expand Up @@ -618,65 +622,167 @@ test(`dragging selected options across each other changes their order`, async ({
})

test.describe(`portal feature`, () => {
test(`dropdown renders in document.body when portal is active in modal`, async ({
test(`foods dropdown in modal 1 renders in body when portal is active`, async ({
page,
}) => {
await page.goto(`/modal`, { waitUntil: `networkidle` })

// Open the first modal
await page
.getByRole(`button`, { name: `Open Modal 1 (Vertical Selects)` })
.click()

// Locate the MultiSelect input for foods within the first modal
const foods_multiselect_input = page.locator(
`div.modal-content.modal-1 div.multiselect input[placeholder='Choose foods...']`,
const modal_1_content = page.locator(`div.modal-content.modal-1`)
const foods_input = modal_1_content.locator(
`div.multiselect input[placeholder='Choose foods...']`,
)
// Wait for the modal to be fully visible and input to be available
await foods_multiselect_input.waitFor({ state: `visible` })
await foods_input.click() // Open dropdown

// Click the input to open the dropdown
await foods_multiselect_input.click()
// Options list should be portalled to body and visible
const portalled_foods_options = page.locator(
`body > ul.options[aria-expanded="true"]:has(li:has-text("${foods[0]}"))`,
)
await expect(portalled_foods_options).toBeVisible()

// Identify the specific portalled options list for foods
// It should be in the body and contain the first food item
const foods_options_list_in_body = page.locator(
`body > ul.options:has(li:has-text("${foods[0]}"))`,
// Options list should not be a direct child of the multiselect wrapper in the modal
const foods_multiselect_wrapper = modal_1_content.locator(
`div.multiselect:has(input[placeholder='Choose foods...'])`,
)
await foods_options_list_in_body.waitFor({ state: `visible` })
await expect(foods_options_list_in_body).toHaveAttribute(
await expect(
foods_multiselect_wrapper.locator(`> ul.options`),
).not.toBeAttached()

// Select an option
await portalled_foods_options.locator(`li:has-text("${foods[0]}")`).click()
await expect(portalled_foods_options).toBeHidden() // Dropdown should close

await expect(
modal_1_content.getByRole(`button`, { name: `Remove ${foods[0]}` }),
).toBeVisible()

await page.keyboard.press(`Escape`) // Close any remaining popups/dropdowns
await page.getByRole(`button`, { name: `Close Modal 1` }).click()
await expect(modal_1_content).toBeHidden()
})

test(`dropdown renders within component when portal is inactive (/ui page)`, async ({
page,
}) => {
await page.goto(`/ui`, { waitUntil: `networkidle` })

const foods_multiselect = page.locator(`#foods`)
await foods_multiselect.locator(`input[autocomplete]`).click() // Open dropdown

// Options list should be a child of the multiselect wrapper and visible
const foods_options_in_component = foods_multiselect.locator(`ul.options`)
await expect(foods_options_in_component).toBeVisible()
await expect(foods_options_in_component).toHaveAttribute(
`aria-expanded`,
`true`,
) // Confirm it's open
)

// Options list should NOT be portalled to the body
const portalled_foods_options = page.locator(
// More specific selector to avoid accidental matches if body > ul.options exists for other reasons
`body > ul.options[aria-expanded="true"]:has(li:has-text("${foods[0]}"))`,
)
await expect(portalled_foods_options).not.toBeAttached()
})

test(`colors dropdown in modal 1 renders in body when portal is active`, async ({
page,
}) => {
await page.goto(`/modal`, { waitUntil: `networkidle` })

await page
.getByRole(`button`, { name: `Open Modal 1 (Vertical Selects)` })
.click()

const modal_1_content = page.locator(`div.modal-content.modal-1`)
const colors_input = modal_1_content.locator(
`div.multiselect input[placeholder='Choose colors...']`,
)
await colors_input.click() // Open dropdown

// Assert that the options list is NOT a direct child of the multiselect wrapper within the modal
const multiselect_wrapper_in_modal = page.locator(
`div.modal-content.modal-1 div.multiselect:has(input[placeholder='Choose foods...'])`,
// Options list should be portalled to body and visible
const portalled_colors_options = page.locator(
`body > ul.options[aria-expanded="true"]:has(li:has-text("${demo_colors[0]}"))`,
)
await expect(portalled_colors_options).toBeVisible()

// Options list should not be a direct child of the multiselect wrapper in the modal
const colors_multiselect_wrapper = modal_1_content.locator(
`div.multiselect:has(input[placeholder='Choose colors...'])`,
)
await expect(
multiselect_wrapper_in_modal.locator(`> ul.options`),
colors_multiselect_wrapper.locator(`> ul.options`),
).not.toBeAttached()

// Select an option from this specific portalled dropdown
await foods_options_list_in_body
.locator(`li:has-text("${foods[0]}")`)
// Select an option
await portalled_colors_options
.locator(`li:has-text("${demo_colors[0]}")`)
.click()
await expect(portalled_colors_options).toBeHidden() // Dropdown should close

await expect(
modal_1_content.getByRole(`button`, { name: `Remove ${demo_colors[0]}` }),
).toBeVisible()

// Assert this specific dropdown is now hidden
await expect(foods_options_list_in_body).toBeHidden({ timeout: 3000 })
await page.keyboard.press(`Escape`) // Close any remaining popups/dropdowns
await page.getByRole(`button`, { name: `Close Modal 1` }).click()
await expect(modal_1_content).toBeHidden()
})

// Explicitly wait for the selected item to appear before asserting visibility
test(`languages dropdown in modal 2 renders in body when portal is active`, async ({
page,
}) => {
await page.goto(`/modal`, { waitUntil: `networkidle` })

await page
.getByRole(`button`, { name: `Open Modal 2 (Horizontal Selects)` })
.click()

const modal_2_content = page.locator(`div.modal-content.modal-2`)
const languages_input = modal_2_content.locator(
`div.multiselect input[placeholder='Choose languages...']`,
)
await languages_input.click() // Open dropdown

// Options list should be portalled to body and visible
const portalled_languages_options = page.locator(
`body > ul.options[aria-expanded="true"]:has(li:has-text("${demo_languages[0]}"))`,
)
await expect(portalled_languages_options).toBeVisible()

// Options list should not be a direct child of the multiselect wrapper in the modal
const languages_multiselect_wrapper = modal_2_content.locator(
`div.multiselect:has(input[placeholder='Choose languages...'])`,
)
await expect(
languages_multiselect_wrapper.locator(`> ul.options`),
).not.toBeAttached()

// Select an option, ensuring exact match
await portalled_languages_options
.getByRole(`option`, { name: demo_languages[0], exact: true })
.click()
// Dropdown should remain visible on desktop by default
await expect(portalled_languages_options).toBeVisible()
// And the selected option should no longer be in the options list (if duplicates=false)
await expect(
page.getByRole(`button`, {
name: `Remove 🍇 Grapes`,
portalled_languages_options.getByRole(`option`, {
name: demo_languages[0],
exact: true,
}),
).toBeVisible()
).not.toBeAttached()

// click escape to close the dropdown
await page.keyboard.press(`Escape`)
await expect(
modal_2_content.getByRole(`button`, {
name: `Remove ${demo_languages[0]}`,
}),
).toBeVisible()

// Close the modal
await page.getByRole(`button`, { name: `Close Modal 1` }).click()
await expect(page.locator(`div.modal-content.modal-1`)).toBeHidden()
await page.keyboard.press(`Escape`) // Close any remaining popups/dropdowns
await page.getByRole(`button`, { name: `Close Modal 2` }).click()
await expect(modal_2_content).toBeHidden()
})
})