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

Add the UI toggle for changing theme #4840

Merged
merged 21 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
58cb461
Add color mode to ui store
zackkrida Aug 26, 2024
027b0b3
Merge branch 'main' into feat-4308-frontend-color-mode-ui-state
zackkrida Aug 29, 2024
71c6dd2
Merge branch 'main' into feat-4308-frontend-color-mode-ui-state
zackkrida Aug 29, 2024
c378847
Discard changes to frontend/src/components/VFooter/VFooter.vue
zackkrida Aug 29, 2024
6d85e59
Apply suggestions from code review
zackkrida Aug 29, 2024
103c3e4
Fix linting issues
zackkrida Aug 29, 2024
c34ac7a
Add assets for the sun and moon icon
dhruvkb Aug 29, 2024
f93183a
Add strings pertaining to theme switching
dhruvkb Aug 29, 2024
a8ac4da
Add prop to hide the selected choice
dhruvkb Aug 29, 2024
655e59e
Create a theme selector
dhruvkb Aug 29, 2024
9991034
Add theme selector to footer
dhruvkb Aug 29, 2024
17207df
Hook up the theme selector to the UI store functionality
dhruvkb Aug 30, 2024
da176fd
Remove the "force_dark_mode" feature flag as it is redundant
dhruvkb Aug 30, 2024
a3f775f
Avoid snapshot changes
dhruvkb Aug 30, 2024
d04a8ab
Merge branch 'main' of https://github.com/WordPress/openverse into da…
dhruvkb Aug 30, 2024
a4efc3a
Make the theme selector client-only
dhruvkb Aug 30, 2024
147b039
Fix overly large select field in Safari
dhruvkb Aug 30, 2024
e9ea67e
Remove "overflow-hidden" class from `VSelectField`
dhruvkb Aug 31, 2024
4eafee9
Add handling for OS color mode to the composable
dhruvkb Sep 3, 2024
6dc18b5
Use computed props from `useDarkMode`
dhruvkb Sep 3, 2024
735ac71
Fix types
dhruvkb Sep 3, 2024
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
11 changes: 1 addition & 10 deletions frontend/feat/feature-flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,13 @@
"supportsQuery": false,
"storage": "session"
},
"force_dark_mode": {
"status": {
"staging": "switchable",
"production": "disabled"
},
"defaultState": "off",
"description": "Force the site to render in dark mode.",
"storage": "session"
},
"dark_mode_ui_toggle": {
"status": {
"staging": "switchable",
"production": "disabled"
},
"defaultState": "off",
"description": "Display the UI toggle to change the site color theme.",
"description": "Display the UI toggle to change the site color theme and respect system preferences.",
"storage": "cookie"
}
},
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/assets/svg/raw/moon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions frontend/src/assets/svg/raw/sun.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontend/src/assets/svg/sprite/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 26 additions & 5 deletions frontend/src/components/VFooter/VFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import useResizeObserver from "~/composables/use-resize-observer"
import { SCREEN_SIZES } from "~/constants/screens"

import { useUiStore } from "~/stores/ui"
import { useFeatureFlagStore } from "~/stores/feature-flag"

import type { SelectFieldProps } from "~/components/VSelectField/VSelectField.vue"
import VLink from "~/components/VLink.vue"
import VBrand from "~/components/VBrand/VBrand.vue"
import VLanguageSelect from "~/components/VLanguageSelect/VLanguageSelect.vue"
import VThemeSelect from "~/components/VThemeSelect/VThemeSelect.vue"
import VPageLinks from "~/components/VHeader/VPageLinks.vue"
import VWordPressLink from "~/components/VHeader/VWordPressLink.vue"

Expand All @@ -25,6 +27,7 @@ export default defineComponent({
VWordPressLink,
VPageLinks,
VLanguageSelect,
VThemeSelect,
VLink,
VBrand,
},
Expand All @@ -50,6 +53,11 @@ export default defineComponent({

const isContentMode = computed(() => props.mode === "content")

const featureFlagStore = useFeatureFlagStore()
const showThemeSwitcher = computed(() =>
featureFlagStore.isOn("dark_mode_ui_toggle")
)

/** JS-based responsiveness */
const footerEl = ref<HTMLElement | null>(null)
const initialWidth = SCREEN_SIZES[uiStore.breakpoint]
Expand All @@ -76,6 +84,7 @@ export default defineComponent({
isContentMode,
allPages,
currentPage,
showThemeSwitcher,

footerEl,
variantNames,
Expand Down Expand Up @@ -110,11 +119,23 @@ export default defineComponent({

<!-- Locale chooser and WordPress affiliation graphic -->
<div class="locale-and-wp flex flex-col justify-between">
<VLanguageSelect
v-bind="languageProps"
class="language max-w-full border-secondary"
/>
<VWordPressLink mode="light" />
<template v-if="showThemeSwitcher">
<VWordPressLink mode="light" />
<div class="flex flex-row items-center gap-6">
<VLanguageSelect
v-bind="languageProps"
class="language max-w-full border-secondary"
/>
<VThemeSelect class="border-secondary" />
</div>
</template>
<template v-else>
<VLanguageSelect
v-bind="languageProps"
class="language max-w-full border-secondary"
/>
<VWordPressLink mode="light" />
</template>
</div>
</footer>
</template>
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/components/VSelectField/VSelectField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ const props = withDefaults(
fieldId: string
labelText: string
choices: Choice[]
showSelected?: boolean
}>(),
{
modelValue: "",
blankText: "",
showSelected: true,
}
)

Expand Down Expand Up @@ -85,8 +87,11 @@ const splitAttrs = computed(() => {
<select
:id="fieldId"
v-model="selectValue"
class="flex h-[calc(theme(spacing.10)_-_2_*_theme(borderWidth.DEFAULT))] w-full appearance-none truncate bg-tx pe-10"
:class="hasStartContent ? 'ps-10' : 'ps-2'"
class="flex h-[calc(theme(spacing.10)_-_2_*_theme(borderWidth.DEFAULT))] appearance-none truncate bg-tx pe-10"
:class="[
showSelected ? 'w-full' : 'w-0',
hasStartContent ? 'ps-10' : 'ps-2',
]"
:name="fieldName"
v-bind="splitAttrs.nonClassAttrs"
:aria-label="labelText"
Expand Down
100 changes: 100 additions & 0 deletions frontend/src/components/VThemeSelect/VThemeSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!--
Present users with a way to change the app theme between three options:
light, dark and system.
-->

<script setup lang="ts">
import { useI18n } from "#imports"

import { computed, type ComputedRef } from "vue"
import { usePreferredColorScheme } from "@vueuse/core"

import { useUiStore } from "~/stores/ui"

import VIcon from "~/components/VIcon/VIcon.vue"
import VSelectField, {
type Choice,
} from "~/components/VSelectField/VSelectField.vue"

/**
* `ActualColorMode` is the evaluated form of `ColorMode`.
*
* It's value is the same as `ColorMode` for light and dark color modes
* but for the system color mode, it evaluates to either depending on
* the user's system theme.
*/
type ActualColorMode = "light" | "dark"

const i18n = useI18n({ useScope: "global" })
const uiStore = useUiStore()

const THEME_ICON_NAME = Object.freeze({
light: "sun",
dark: "moon",
})

const THEME_TEXT = {
light: i18n.t(`theme.choices.light`),
dark: i18n.t(`theme.choices.dark`),
}

const colorMode = computed({
get: () => uiStore.colorMode,
set: (value) => {
uiStore.setColorMode(value)
},
})

/**
* Get the user's preferred color scheme at the OS level. If the user
* has not set an OS level preference, we fall back to light mode.
*/
const preferredColorScheme: ComputedRef<ActualColorMode> = computed(() => {
const pref = usePreferredColorScheme()
return pref.value === "no-preference" ? "light" : pref.value
})

/**
* Get the user's preferred color scheme at the app level. If the user
* has set the color mode to "system", we fall back to the OS level
* preference.
*/
const actualColorMode: ComputedRef<ActualColorMode> = computed(() =>
colorMode.value === "system" ? preferredColorScheme.value : colorMode.value
)

/**
* The icon always reflects the actual theme applied to the site.
* Therefore, it must be based on the value of `actualColorMode`.
*/
const currentThemeIcon = computed(() => THEME_ICON_NAME[actualColorMode.value])

/**
* The choices are computed because the text for the color mode choice
* "system" is dynamic and reflects the user's preferred color scheme at
* the OS-level.
*/
const choices: ComputedRef<Choice[]> = computed(() => {
const systemText = `${i18n.t(`theme.choices.system`)} (${THEME_TEXT[preferredColorScheme.value]})`
return [
{ key: "light", text: THEME_TEXT.light },
{ key: "dark", text: THEME_TEXT.dark },
{ key: "system", text: systemText },
]
})
</script>

<template>
<VSelectField
v-model="colorMode"
field-id="theme"
:choices="choices"
:blank-text="$t('theme.theme')"
:label-text="$t('theme.theme')"
:show-selected="false"
>
<template #start>
<VIcon :name="currentThemeIcon" />
</template>
</VSelectField>
</template>
36 changes: 27 additions & 9 deletions frontend/src/composables/use-dark-mode.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import { computed } from "#imports"
import { computed, useUiStore } from "#imports"

import { useFeatureFlagStore } from "~/stores/feature-flag"

export const DARK_MODE_CLASS = "dark-mode"
export const LIGHT_MODE_CLASS = "light-mode"

/**
* TODO: Replace with the user's actual dark mode preference.
* Dark mode detection will be based on user preference,
* overwritten by the "force_dark_mode" feature flag.
* Determines the dark mode setting based on user preference or feature flag.
*
* When dark mode toggling is disabled, the site is in "light mode".
*
* When the "dark_mode_ui_toggle" flag is enabled, the site will respect
* the user system preference by default.
*
*/
export function useDarkMode() {
const uiStore = useUiStore()
const featureFlagStore = useFeatureFlagStore()

const isDarkMode = computed(() => featureFlagStore.isOn("force_dark_mode"))
const cssClass = computed(() =>
isDarkMode.value ? DARK_MODE_CLASS : LIGHT_MODE_CLASS
const darkModeToggleable = computed(() =>
featureFlagStore.isOn("dark_mode_ui_toggle")
)

const colorMode = computed(() => {
if (darkModeToggleable.value) {
return uiStore.colorMode
}
return "light"
})

const cssClass = computed(() => {
return {
light: LIGHT_MODE_CLASS,
dark: DARK_MODE_CLASS,
system: "",
}[colorMode.value]
})

return {
isDarkMode,
/** The CSS class representing the current color mode. */
colorMode,
cssClass,
}
}
8 changes: 8 additions & 0 deletions frontend/src/locales/scripts/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,14 @@
language: {
language: "Language",
},
theme: {
theme: "Theme",
choices: {
dark: "Dark",
light: "Light",
system: "System",
},
},
recentSearches: {
heading: "Recent searches",
clear: {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/preferences.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { definePageMeta } from "#imports"
import { computed } from "vue"

import { useFeatureFlagStore } from "~/stores/feature-flag"
import { isFlagName } from "~/types/feature-flag"
import { SWITCHABLE, ON, OFF } from "~/constants/feature-flag"

import VContentPage from "~/components/VContentPage.vue"
Expand Down Expand Up @@ -33,6 +34,9 @@ const handleChange = ({
name: string
checked?: boolean
}) => {
if (!isFlagName(name)) {
return
}
featureFlagStore.toggleFeature(name, checked ? ON : OFF)
}

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/stores/feature-flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export const useFeatureFlagStore = defineStore(FEATURE_FLAG, {
* @param name - the name of the flag to toggle
* @param targetState - the desired state of the feature flag
*/
toggleFeature(name: string, targetState: FeatureState) {
toggleFeature(name: FlagName, targetState: FeatureState) {
if (!isFlagName(name)) {
throw new Error(`Toggling invalid feature flag: ${name}`)
}
Expand All @@ -315,7 +315,7 @@ export const useFeatureFlagStore = defineStore(FEATURE_FLAG, {
storage.value = this.flags.analytics.state === ON ? null : true
},

isSwitchable(name: string) {
isSwitchable(name: FlagName) {
if (!isFlagName(name)) {
throw new Error(`Invalid feature flag accessed: ${name}`)
}
Expand All @@ -328,7 +328,7 @@ export const useFeatureFlagStore = defineStore(FEATURE_FLAG, {
*
* @returns `true` if the flag is on, false otherwise
*/
isOn(name: string) {
isOn(name: FlagName) {
if (!isFlagName(name)) {
throw new Error(`Invalid feature flag accessed: ${name}`)
}
Expand Down
Loading