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

[next] feat: add isDarkTheme functions and composables #6196

Draft
wants to merge 3 commits into
base: next
Choose a base branch
from
Draft
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
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'

/**
* 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.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -143,6 +147,11 @@ module.exports = async () => {
{
name: 'useHotKey',
content: 'docs/composables/useHotKey.md',

},
{
name: 'useIsDarkTheme',
content: 'docs/composables/useIsDarkTheme.md',
},
],
},
Expand Down
2 changes: 2 additions & 0 deletions styleguide/global.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ window.emojiAddRecent = emojiAddRecent
window.getCurrentSkinTone = getCurrentSkinTone
window.setCurrentSkinTone = setCurrentSkinTone
window.usernameToColor = usernameToColor
// Exported composables
window.useIsDarkTheme = useIsDarkTheme

window.NextcloudVueDocs = {
tags: '<?xml version="1.0"?><d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"><d:response><d:href>/remote.php/dav/systemtags/</d:href><d:propstat><d:prop><oc:id/><oc:display-name/><oc:user-visible/><oc:user-assignable/><oc:can-assign/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/systemtags/7</d:href><d:propstat><d:prop><oc:id>7</oc:id><oc:display-name>tag1</oc:display-name><oc:user-visible>true</oc:user-visible><oc:user-assignable>true</oc:user-assignable><oc:can-assign>true</oc:can-assign></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/systemtags/2</d:href><d:propstat><d:prop><oc:id>2</oc:id><oc:display-name>tag2</oc:display-name><oc:user-visible>false</oc:user-visible><oc:user-assignable>true</oc:user-assignable><oc:can-assign>true</oc:can-assign></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/systemtags/3</d:href><d:propstat><d:prop><oc:id>3</oc:id><oc:display-name>tag3</oc:display-name><oc:user-visible>true</oc:user-visible><oc:user-assignable>true</oc:user-assignable><oc:can-assign>true</oc:can-assign></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/systemtags/4</d:href><d:propstat><d:prop><oc:id>4</oc:id><oc:display-name>important</oc:display-name><oc:user-visible>true</oc:user-visible><oc:user-assignable>true</oc:user-assignable><oc:can-assign>true</oc:can-assign></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/systemtags/1</d:href><d:propstat><d:prop><oc:id>1</oc:id><oc:display-name>secret</oc:display-name><oc:user-visible>true</oc:user-visible><oc:user-assignable>false</oc:user-assignable><oc:can-assign>true</oc:can-assign></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/systemtags/5</d:href><d:propstat><d:prop><oc:id>5</oc:id><oc:display-name>test</oc:display-name><oc:user-visible>true</oc:user-visible><oc:user-assignable>false</oc:user-assignable><oc:can-assign>true</oc:can-assign></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/systemtags/6</d:href><d:propstat><d:prop><oc:id>6</oc:id><oc:display-name>test2</oc:display-name><oc:user-visible>false</oc:user-visible><oc:user-assignable>false</oc:user-assignable><oc:can-assign>true</oc:can-assign></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>',
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

"compilerOptions": {
"allowJs": true,
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true,
"target": "ESNext",
"module": "ESNext",
Expand Down
Loading