diff --git a/REUSE.toml b/REUSE.toml index 249358c752..5ea62877d1 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -24,7 +24,7 @@ SPDX-FileCopyrightText = "2020-2024 Nextcloud translators" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["tsconfig.json", "tsconfig.webpack.json", "cypress/tsconfig.json", "tests/tsconfig.json"] +path = ["tsconfig.json", "cypress/tsconfig.json", "tests/tsconfig.json"] precedence = "aggregate" SPDX-FileCopyrightText = "2022-2024 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "CC0-1.0" 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.js b/styleguide.config.js index fe86b0dd94..99b1dc3f3d 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -129,6 +129,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', @@ -151,6 +155,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 1c20d3bccf..741bc116fe 100644 --- a/styleguide/global.requires.js +++ b/styleguide/global.requires.js @@ -11,6 +11,7 @@ import usernameToColor from '../src/functions/usernameToColor/index.js' import Tooltip from './../src/directives/Tooltip/index.js' import Focus from './../src/directives/Focus/index.js' import Linkify from './../src/directives/Linkify/index.js' +import { useIsDarkTheme } from '../src/composables/index.js' import axios from '@nextcloud/axios' @@ -166,6 +167,8 @@ window.emojiAddRecent = emojiAddRecent window.getCurrentSkinTone = getCurrentSkinTone window.setCurrentSkinTone = setCurrentSkinTone window.usernameToColor = usernameToColor +// Exported composables +window.useIsDarkTheme = useIsDarkTheme // Directives Vue.directive('Tooltip', Tooltip) diff --git a/tsconfig.json b/tsconfig.json index 90201cee16..62fd35358a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "include": ["./src/**/*.ts"], "exclude": ["./src/**/*.cy.ts"], "compilerOptions": { + "allowImportingTsExtensions": true, "allowSyntheticDefaultImports": true, "moduleResolution": "Bundler", "target": "ESNext", diff --git a/tsconfig.webpack.json b/tsconfig.webpack.json deleted file mode 100644 index b9471fedc3..0000000000 --- a/tsconfig.webpack.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": false, - } -} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index f42b25ba37..5c92f3a082 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,12 +45,6 @@ webpackRules.RULE_TS = { test: /\.tsx?$/, use: [ 'babel-loader', - { - loader: 'ts-loader', - options: { - configFile: 'tsconfig.webpack.json', - }, - }, ], }