Skip to content
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

feat: add isDarkTheme functions and composables #5698

Merged
merged 3 commits into from
Nov 6, 2024
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
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
72 changes: 72 additions & 0 deletions docs/composables/useIsDarkTheme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

```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<HTMLElement> = document.body): DeepReadonly<Ref<boolean>>

/**
* 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<Ref<boolean>>
```

## Example

```vue
<template>
<div>
<div :style="{ backgroundColor: isDarkTheme ? 'black' : 'white' }">
Is dark theme enabled? {{ isDarkTheme }}
</div>
<NcButton @click="switchTheme">Switch theme</NcButton>
</div>
</template>

<script>
export default {
setup() {
const isDarkTheme = useIsDarkTheme()

// For documentation only. Do not use in production.
function switchTheme() {
if (isDarkTheme.value) {
document.body.setAttribute('data-theme-light', '')
document.body.removeAttribute('data-theme-dark')
document.body.setAttribute('data-themes', 'light')
} else {
document.body.setAttribute('data-theme-dark', '')
document.body.removeAttribute('data-theme-light')
document.body.setAttribute('data-themes', 'dark')
}
}

return {
isDarkTheme,
switchTheme,
}
},
}
</script>
```
36 changes: 36 additions & 0 deletions docs/functions/isDarkTheme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

```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
```
1 change: 1 addition & 0 deletions src/composables/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
40 changes: 40 additions & 0 deletions src/composables/useIsDarkTheme/index.ts
Original file line number Diff line number Diff line change
@@ -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'
susnux marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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<Ref<boolean>>} - computed boolean whether the dark theme is enabled
*/
export function useIsDarkThemeElement(el: HTMLElement = document.body): DeepReadonly<Ref<boolean>> {
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<Ref<boolean>>} - computed boolean whether the dark theme is enabled
*/
export const useIsDarkTheme = createSharedComposable(() => useIsDarkThemeElement())
1 change: 1 addition & 0 deletions src/functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
32 changes: 32 additions & 0 deletions src/functions/isDarkTheme/index.ts
Original file line number Diff line number Diff line change
@@ -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()
9 changes: 9 additions & 0 deletions styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -151,6 +155,11 @@ module.exports = async () => {
{
name: 'useHotKey',
content: 'docs/composables/useHotKey.md',

},
{
name: 'useIsDarkTheme',
content: 'docs/composables/useIsDarkTheme.md',
},
],
},
Expand Down
3 changes: 3 additions & 0 deletions styleguide/global.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/*.cy.ts"],
"compilerOptions": {
"allowImportingTsExtensions": true,
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not work, just do not import with .ts extension in Typescript files (in JS files it work).

Suggested change
"allowImportingTsExtensions": true,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This works, I missed that styleguidist uses a different config

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have 46 places with .ts imports in this repo

Copy link
Contributor

Choose a reason for hiding this comment

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

We have 46 places with .ts imports in this repo

This works as long as it is in JavaScript files and not Typescript.
Importing with .ts extension in Typescript is invalid when emitting is enabled, as the output will be invalid.
So as long as we use a bundler everything works, but e.g. tsc would not work, because Typescript does not rewrite import extensions, so you either have to import Typescript files with .js or without any extension.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Importing with .ts extension in Typescript is invalid when emitting is enabled, as the output will be invalid.
So as long as we use a bundler everything works, but e.g. tsc would not work, because Typescript does not rewrite import extensions, so you either have to import Typescript files with .js or without any extension.

We almost never use tsc as a compiler (except notify_push) and in general, in modern TypeScript usage it's rarely used as a compiler/language and not as a static analyzer (type checker).

Specifically, in this repo, we already had @babel/typescript-preset to work with TS as a static analyzer, and only the webpack config was not correct.

"allowSyntheticDefaultImports": true,
"moduleResolution": "Bundler",
"target": "ESNext",
Expand Down
6 changes: 0 additions & 6 deletions tsconfig.webpack.json

This file was deleted.

6 changes: 0 additions & 6 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,6 @@ webpackRules.RULE_TS = {
test: /\.tsx?$/,
use: [
'babel-loader',
{
loader: 'ts-loader',
options: {
configFile: 'tsconfig.webpack.json',
},
},
],
}

Expand Down
Loading