diff --git a/app/composables/useColors.ts b/app/composables/useColors.ts index 7a5248627f..87bbb64c5d 100644 --- a/app/composables/useColors.ts +++ b/app/composables/useColors.ts @@ -1,5 +1,18 @@ -import { computed, shallowRef, type ComputedRef, type Ref, type ShallowRef, unref } from 'vue' -import { useMutationObserver, useResizeObserver, useSupported } from '@vueuse/core' +import { + computed, + shallowRef, + type ComputedRef, + type Ref, + type ShallowRef, + unref, + watch, +} from 'vue' +import { + useMutationObserver, + useResizeObserver, + useSupported, + usePreferredDark, +} from '@vueuse/core' type CssVariableSource = HTMLElement | null | undefined | Ref @@ -36,6 +49,8 @@ export function useColors( options: { watchHtmlAttributes?: boolean; watchResize?: boolean } = {}, ): { colors: ComputedRef> } { const recomputeToken = shallowRef(0) + const isPreferredDark = usePreferredDark() + const invalidateColors = () => { recomputeToken.value += 1 } @@ -44,6 +59,8 @@ export function useColors( () => typeof window !== 'undefined' && typeof document !== 'undefined', ) + watch(isPreferredDark, invalidateColors) + const colors = computed>(() => { void recomputeToken.value const resolvedElement = resolveElement(element) diff --git a/test/unit/app/composables/use-colors.spec.ts b/test/unit/app/composables/use-colors.spec.ts index 6d8369a6c1..4ad3594446 100644 --- a/test/unit/app/composables/use-colors.spec.ts +++ b/test/unit/app/composables/use-colors.spec.ts @@ -1,12 +1,18 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { computed, shallowRef } from 'vue' +import { computed, nextTick, shallowRef, ref, type Ref } from 'vue' import { useColors } from '~/composables/useColors' const useSupportedMock = vi.hoisted(() => vi.fn()) const useMutationObserverMock = vi.hoisted(() => vi.fn()) const useResizeObserverMock = vi.hoisted(() => vi.fn()) +const vueUseMockState = vi.hoisted(() => ({ + preferredDark: undefined as unknown as Ref, +})) + vi.mock('@vueuse/core', () => { + vueUseMockState.preferredDark = ref(false) + return { useSupported: (callback: () => boolean) => { useSupportedMock(callback) @@ -14,12 +20,14 @@ vi.mock('@vueuse/core', () => { }, useMutationObserver: useMutationObserverMock, useResizeObserver: useResizeObserverMock, + usePreferredDark: () => vueUseMockState.preferredDark, } }) describe('useColors', () => { beforeEach(() => { vi.clearAllMocks() + vueUseMockState.preferredDark.value = false }) afterEach(() => { @@ -57,4 +65,77 @@ describe('useColors', () => { const { colors } = useColors(elementReference) expect(colors.value).toEqual({}) }) + + it('recomputes colors when preferred dark mode changes', async () => { + const styleValues = { + accent: '#FF0000', + } + + vi.stubGlobal('window', {}) + vi.stubGlobal('document', { + documentElement: {}, + }) + + vi.stubGlobal('getComputedStyle', () => ({ + getPropertyValue: (variableName: string) => { + if (variableName === '--accent') { + return styleValues.accent + } + + return '' + }, + })) + + const elementReference = shallowRef({} as HTMLElement) + const { colors } = useColors(elementReference) + expect(colors.value.accent).toBe('#FF0000') + styleValues.accent = '#00FF00' + vueUseMockState.preferredDark.value = true + await nextTick() + + expect(colors.value.accent).toBe('#00FF00') + }) + + it('attaches an html mutation observer when enabled', () => { + vi.stubGlobal('window', {}) + vi.stubGlobal('document', { + documentElement: {}, + }) + const elementReference = shallowRef(null) + useColors(elementReference, { + watchHtmlAttributes: true, + }) + expect(useMutationObserverMock).toHaveBeenCalledTimes(1) + expect(useMutationObserverMock).toHaveBeenCalledWith( + document.documentElement, + expect.any(Function), + { + attributes: true, + attributeFilter: ['class', 'style', 'data-theme', 'data-bg-theme'], + }, + ) + }) + + it('falls back to document element when no element is provided', () => { + vi.stubGlobal('window', {}) + vi.stubGlobal('document', { + documentElement: {}, + }) + vi.stubGlobal('getComputedStyle', (element: HTMLElement) => { + expect(element).toBe(document.documentElement) + + return { + getPropertyValue: (variableName: string) => { + if (variableName === '--accent') { + return 'red' + } + + return '' + }, + } + }) + const elementReference = shallowRef(null) + const { colors } = useColors(elementReference) + expect(colors.value.accent).toBe('red') + }) })