Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ There are several options that can be passed in as a second parameter, like the
showError('This is an error shown without a timeout', { timeout: -1 })
```

Ordinary toasts (success, error, warning, info, and bare messages) use the user-configured toast timeout
from the theming capabilities (`theming.toastTimeout`) when available, and fall back to
`TOAST_DEFAULT_TIMEOUT` (7 seconds) otherwise.

Loading toasts stay permanent until hidden manually, and undo toasts keep their fixed undo duration.

A full list of available options can be found in the [documentation](https://nextcloud-libraries.github.io/nextcloud-dialogs/).

### FilePicker
Expand Down
103 changes: 103 additions & 0 deletions lib/toast.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { afterEach, beforeEach, expect, test, vi } from 'vitest'
import { showMessage, TOAST_DEFAULT_TIMEOUT, TOAST_PERMANENT_TIMEOUT, ToastType } from './toast.ts'

const getCapabilities = vi.hoisted(() => vi.fn())

vi.mock('@nextcloud/capabilities', () => ({
getCapabilities,
}))

vi.mock('toastify-js', () => ({
default: vi.fn((options: { duration?: number }) => ({
options,
showToast: vi.fn(),
hideToast: vi.fn(),
})),
}))

import Toastify from 'toastify-js'

beforeEach(() => {
getCapabilities.mockReset()
vi.mocked(Toastify).mockClear()
})

afterEach(() => {
document.body.innerHTML = ''
})

test('uses default timeout when capabilities are missing', () => {
getCapabilities.mockImplementation(() => {
throw new Error('no capabilities')
})

showMessage('hello')

expect(Toastify).toHaveBeenCalledWith(expect.objectContaining({
duration: TOAST_DEFAULT_TIMEOUT,
}))
})

test('uses toastTimeout from theming capabilities', () => {
getCapabilities.mockReturnValue({
theming: {
toastTimeout: 15_000,
},
})

showMessage('hello')

expect(Toastify).toHaveBeenCalledWith(expect.objectContaining({
duration: 15_000,
}))
})

test('allows permanent timeout from capabilities', () => {
getCapabilities.mockReturnValue({
theming: {
toastTimeout: TOAST_PERMANENT_TIMEOUT,
},
})

showMessage('hello')

expect(Toastify).toHaveBeenCalledWith(expect.objectContaining({
duration: TOAST_PERMANENT_TIMEOUT,
}))
})

test('falls back for invalid capability values', () => {
getCapabilities.mockReturnValue({
theming: {
toastTimeout: 0,
},
})

showMessage('hello')

expect(Toastify).toHaveBeenCalledWith(expect.objectContaining({
duration: TOAST_DEFAULT_TIMEOUT,
}))
})

test('does not override loading toast duration', () => {
getCapabilities.mockReturnValue({
theming: {
toastTimeout: 30_000,
},
})

showMessage('loading', {
type: ToastType.LOADING,
timeout: TOAST_PERMANENT_TIMEOUT,
})

expect(Toastify).toHaveBeenCalledWith(expect.objectContaining({
duration: TOAST_PERMANENT_TIMEOUT,
}))
})
44 changes: 43 additions & 1 deletion lib/toast.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is only used internally so the value specified on server should be provided in the capabilities of theming and then used in the implementation here

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { getCapabilities } from '@nextcloud/capabilities'
import Toastify from 'toastify-js'
import LoaderSvg from '../styles/loader.svg?raw'
import { t } from './utils/l10n.js'
Expand Down Expand Up @@ -41,6 +42,37 @@ export const TOAST_DEFAULT_TIMEOUT = 7000
/** Timeout value to show a toast permanently */
export const TOAST_PERMANENT_TIMEOUT = -1

type ThemingCapabilities = {
theming?: {
toastTimeout?: number
}
}

/**
* Whether a timeout value is valid for ordinary toasts.
*
* @param timeout Timeout in milliseconds
*/
function isValidToastTimeout(timeout: number): boolean {
return timeout === TOAST_PERMANENT_TIMEOUT || timeout > 0
}

/**
* Resolve the user-configured toast timeout from theming capabilities.
* Falls back to {@link TOAST_DEFAULT_TIMEOUT} when unset or invalid.
*/
function getToastTimeout(): number {
try {
const timeout = (getCapabilities() as ThemingCapabilities)?.theming?.toastTimeout
if (typeof timeout === 'number' && isValidToastTimeout(timeout)) {
return timeout
}
} catch {
// Capabilities may be unavailable outside the browser (e.g. unit tests).
}
return TOAST_DEFAULT_TIMEOUT
}

/**
* Type of a toast
*
Expand Down Expand Up @@ -104,7 +136,7 @@ export interface ToastOptions {
*/
export function showMessage(data: string | Node, options?: ToastOptions): Toast {
options = {
timeout: TOAST_DEFAULT_TIMEOUT,
timeout: getToastTimeout(),
isHTML: false,
type: undefined,
// An undefined selector defaults to the body element
Expand All @@ -115,6 +147,16 @@ export function showMessage(data: string | Node, options?: ToastOptions): Toast
...options,
}

// Accessibility preference overrides ordinary/app-specific timeouts.
// Loading and undo toasts keep their forced durations; permanent stays permanent.
if (
options.type !== ToastType.LOADING
&& options.type !== ToastType.UNDO
&& options.timeout !== TOAST_PERMANENT_TIMEOUT
) {
options.timeout = getToastTimeout()
}

if (typeof data === 'string' && !options.isHTML) {
// fime mae sure that text is extracted
const element = document.createElement('div')
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@nextcloud/auth": "^2.6.0",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/browser-storage": "^0.5.0",
"@nextcloud/capabilities": "^1.2.1",
"@nextcloud/event-bus": "^3.3.3",
"@nextcloud/files": "^4.0.0",
"@nextcloud/initial-state": "^3.0.0",
Expand Down