diff --git a/docs/composables/useIsDarkTheme.md b/docs/composables/useIsDarkTheme.md new file mode 100644 index 0000000000..391f441621 --- /dev/null +++ b/docs/composables/useIsDarkTheme.md @@ -0,0 +1,72 @@ + + +```ts static +import { + useIsDarkTheme, + useIsDarkThemeElement, +} from '@nextcloud/vue/dist/Composables/useIsDarkTheme.js' +``` + +Same as `isDarkTheme` functions, but with reactivity. + +## Definition + +```ts static +/** + * Check whether the dark theme is enabled on a specific element. + * If you need to check an entire page, use `useIsDarkTheme` instead for better performance. + * Reacts on element attributes change and system theme change. + * @param el - The element to check for the dark theme enabled on, default is document.body + * @return - computed boolean whether the dark theme is enabled + */ +declare function useIsDarkThemeElement(el: MaybeRef = document.body): DeepReadonly> + +/** + * Shared composable to check whether the dark theme is enabled on the page. + * Reacts on body data-theme-* attributes change and system theme change. + * @return - computed boolean whether the dark theme is enabled + */ +declare function useIsDarkTheme(): DeepReadonly> +``` + +## Example + +```vue + + + +``` \ No newline at end of file diff --git a/docs/functions/isDarkTheme.md b/docs/functions/isDarkTheme.md new file mode 100644 index 0000000000..809e8ef566 --- /dev/null +++ b/docs/functions/isDarkTheme.md @@ -0,0 +1,36 @@ + + +```ts static +import { + isDarkTheme, + checkIfDarkTheme, +} from '@nextcloud/vue/dist/Functions/isDarkTheme.js' +``` + +Check whether the dark theme is enabled in Nextcloud. + +You should not use `window.matchMedia.('(prefers-color-scheme: dark)')`. It checks for the user's system theme, but Nextcloud Dark theme could be enabled even on the light system theme. + +You should not use `[data-themes*=dark]` or `[data-theme-dark]` attributes on the body. It checks for explicitly set dark theme, but a user may use the system or custom theme. + +## Definitions + +```ts static +/** + * Check whether the dark theme is used on a specific element + * @param el - Element to check for dark theme, which is used for `data-theme-*` checking (default is `document.body`) + * @return - Whether the dark theme is enabled via Nextcloud theme + */ +declare function checkIfDarkTheme(el: HTMLElement = document.body): boolean; + +/** + * Whether the dark theme is enabled in Nextcloud. + * The variable is defined on page load and not reactive. + * Use `checkIfDarkTheme` if you need to check it at a specific moment. + * Use `useDarkTheme` if you need a reactive variable in a Vue component. + */ +declare var isDarkTheme +``` diff --git a/src/composables/index.js b/src/composables/index.js index 016a4d0496..0124c91b84 100644 --- a/src/composables/index.js +++ b/src/composables/index.js @@ -7,3 +7,4 @@ export * from './useIsFullscreen/index.js' export * from './useIsMobile/index.js' export * from './useFormatDateTime.ts' export * from './useHotKey/index.js' +export * from './useIsDarkTheme/index.ts' diff --git a/src/composables/useIsDarkTheme/index.ts b/src/composables/useIsDarkTheme/index.ts new file mode 100644 index 0000000000..4ee3aa6e91 --- /dev/null +++ b/src/composables/useIsDarkTheme/index.ts @@ -0,0 +1,40 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { DeepReadonly, Ref } from 'vue' +import { ref, readonly, watch } from 'vue' +import { createSharedComposable, usePreferredDark, useMutationObserver } from '@vueuse/core' +import { checkIfDarkTheme } from '../../functions/isDarkTheme/index.ts' + +/** + * Check whether the dark theme is enabled on a specific element. + * If you need to check an entire page, use `useIsDarkTheme` instead for better performance. + * Reacts on element attributes change and system theme change. + * @param el - The element to check for the dark theme enabled on (default is `document.body`) + * @return {DeepReadonly>} - computed boolean whether the dark theme is enabled + */ +export function useIsDarkThemeElement(el: HTMLElement = document.body): DeepReadonly> { + const isDarkTheme = ref(checkIfDarkTheme(el)) + const isDarkSystemTheme = usePreferredDark() + + /** Update the isDarkTheme */ + function updateIsDarkTheme() { + isDarkTheme.value = checkIfDarkTheme(el) + } + + // Watch for element change to handle data-theme* attributes change + useMutationObserver(el, updateIsDarkTheme, { attributes: true }) + // Watch for system theme change for the default theme + watch(isDarkSystemTheme, updateIsDarkTheme, { immediate: true }) + + return readonly(isDarkTheme) +} + +/** + * Shared composable to check whether the dark theme is enabled on the page. + * Reacts on body data-theme-* attributes change and system theme change. + * @return {DeepReadonly>} - computed boolean whether the dark theme is enabled + */ +export const useIsDarkTheme = createSharedComposable(() => useIsDarkThemeElement()) diff --git a/src/functions/index.js b/src/functions/index.js index eb332181ce..807f98ef08 100644 --- a/src/functions/index.js +++ b/src/functions/index.js @@ -6,4 +6,5 @@ export * from './a11y/index.ts' export * from './emoji/index.ts' export * from './reference/index.js' +export * from './isDarkTheme/index.ts' export { default as usernameToColor } from './usernameToColor/index.js' diff --git a/src/functions/isDarkTheme/index.ts b/src/functions/isDarkTheme/index.ts new file mode 100644 index 0000000000..7d176b9486 --- /dev/null +++ b/src/functions/isDarkTheme/index.ts @@ -0,0 +1,32 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Check whether the dark theme is used on a specific element + * @param el - Element to check for dark theme, which is used for `data-theme-*` checking (default is `document.body`) + * @return {boolean} - Whether the dark theme is enabled via Nextcloud theme + */ +export function checkIfDarkTheme(el: HTMLElement = document.body): boolean { + // Nextcloud uses --background-invert-if-dark for dark theme filters in CSS + // Values: + // - 'invert(100%)' for dark theme + // - 'no' for light theme + // This is the most reliable way to check for dark theme, including custom themes + const backgroundInvertIfDark = window.getComputedStyle(el).getPropertyValue('--background-invert-if-dark') + if (backgroundInvertIfDark !== undefined) { + return backgroundInvertIfDark === 'invert(100%)' + } + + // There is no theme? Fallback to the light theme + return false +} + +/** + * Whether the dark theme is enabled in Nextcloud. + * The variable is defined on page load and not reactive. + * Use `checkIfDarkTheme` if you need to check it at a specific moment. + * Use `useDarkTheme` if you need a reactive variable in a Vue component. + */ +export const isDarkTheme = checkIfDarkTheme() diff --git a/styleguide.config.cjs b/styleguide.config.cjs index b1928e0454..b2275b55dd 100644 --- a/styleguide.config.cjs +++ b/styleguide.config.cjs @@ -121,6 +121,10 @@ module.exports = async () => { name: 'emoji', content: 'docs/functions/emoji.md', }, + { + name: 'isDarkTheme', + content: 'docs/functions/isDarkTheme.md', + }, { name: 'usernameToColor', content: 'docs/functions/usernameToColor.md', @@ -143,6 +147,11 @@ module.exports = async () => { { name: 'useHotKey', content: 'docs/composables/useHotKey.md', + + }, + { + name: 'useIsDarkTheme', + content: 'docs/composables/useIsDarkTheme.md', }, ], }, diff --git a/styleguide/global.requires.js b/styleguide/global.requires.js index 0083853bc1..69ee8cb033 100644 --- a/styleguide/global.requires.js +++ b/styleguide/global.requires.js @@ -157,6 +157,8 @@ window.emojiAddRecent = emojiAddRecent window.getCurrentSkinTone = getCurrentSkinTone window.setCurrentSkinTone = setCurrentSkinTone window.usernameToColor = usernameToColor +// Exported composables +window.useIsDarkTheme = useIsDarkTheme window.NextcloudVueDocs = { tags: '/remote.php/dav/systemtags/HTTP/1.1 404 Not Found/remote.php/dav/systemtags/77tag1truetruetrueHTTP/1.1 200 OK/remote.php/dav/systemtags/22tag2falsetruetrueHTTP/1.1 200 OK/remote.php/dav/systemtags/33tag3truetruetrueHTTP/1.1 200 OK/remote.php/dav/systemtags/44importanttruetruetrueHTTP/1.1 200 OK/remote.php/dav/systemtags/11secrettruefalsetrueHTTP/1.1 200 OK/remote.php/dav/systemtags/55testtruefalsetrueHTTP/1.1 200 OK/remote.php/dav/systemtags/66test2falsefalsetrueHTTP/1.1 200 OK', diff --git a/tsconfig.json b/tsconfig.json index 75cff7068b..89e3e01f99 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "compilerOptions": { "allowJs": true, + "allowImportingTsExtensions": true, "allowSyntheticDefaultImports": true, "target": "ESNext", "module": "ESNext",