From d469d37d14405cf65fd4e96668ee5ed9aa844ff0 Mon Sep 17 00:00:00 2001 From: Sayam Samal Date: Sat, 9 Mar 2024 03:24:17 +0530 Subject: [PATCH] personal_menu: Add theme switcher to personal menu popover. This commit introduces a theme switcher feature within the personal menu popover. The implementation begins with the development of a tab picker, which has the following features: * Utilization of radio buttons to emulate the tab picker. Radio input buttons provides the native way in HTML to select one value out of a set. * Support for both horizontal (default) and vertical orientations. Vertical orientation can be achieved by appending the `.tab-picker-vertical` class. * Respects the `prefers-reduced-motion` option set by the user. Disables the sliding tab animation to improve accessibility. Additionally, the theme switcher component incorporates error handling mechanisms. In the event of a server/network error, the tab slider reverts to the previous theme option after a delay of 500ms. This behavior visually communicates the occurrence of an error to the user, improving the UX. Fixes: #22803. --- web/src/personal_menu_popover.js | 22 +++++ web/src/popover_menus_data.js | 5 ++ web/styles/app_components.css | 129 ++++++++++++++++++++++++++++ web/styles/app_variables.css | 5 ++ web/styles/popovers.css | 11 +++ web/templates/personal_menu.hbs | 21 +++++ web/templates/tooltip_templates.hbs | 6 ++ 7 files changed, 199 insertions(+) diff --git a/web/src/personal_menu_popover.js b/web/src/personal_menu_popover.js index 47930b6612..d7aed65ce5 100644 --- a/web/src/personal_menu_popover.js +++ b/web/src/personal_menu_popover.js @@ -3,6 +3,7 @@ import tippy from "tippy.js"; import render_personal_menu from "../templates/personal_menu.hbs"; +import * as channel from "./channel"; import * as narrow from "./narrow"; import * as people from "./people"; import * as popover_menus from "./popover_menus"; @@ -10,6 +11,7 @@ import * as popover_menus_data from "./popover_menus_data"; import * as popovers from "./popovers"; import {current_user} from "./state_data"; import {parse_html} from "./ui_util"; +import {user_settings} from "./user_settings"; import * as user_status from "./user_status"; export function initialize() { @@ -40,6 +42,26 @@ export function initialize() { appendTo: document.body, }); + $popper.on("change", "input[name='theme-select']", (e) => { + const new_theme_code = $(e.currentTarget).attr("data-theme-code"); + channel.patch({ + url: "/json/settings", + data: {color_scheme: new_theme_code}, + error() { + // NOTE: The additional delay allows us to visually communicate + // that an error occurred due to which we are reverting back + // to the previously used value. + setTimeout(() => { + const prev_theme_code = user_settings.color_scheme; + $(e.currentTarget) + .parent() + .find(`input[data-theme-code="${prev_theme_code}"]`) + .prop("checked", true); + }, 500); + }, + }); + }); + $popper.one("click", ".personal-menu-clear-status", (e) => { e.preventDefault(); const me = current_user.user_id; diff --git a/web/src/popover_menus_data.js b/web/src/popover_menus_data.js index 5dcba1b6ba..c850100307 100644 --- a/web/src/popover_menus_data.js +++ b/web/src/popover_menus_data.js @@ -14,6 +14,7 @@ import * as message_lists from "./message_lists"; import * as muted_users from "./muted_users"; import {page_params} from "./page_params"; import * as people from "./people"; +import * as settings_config from "./settings_config"; import * as settings_data from "./settings_data"; import * as starred_messages from "./starred_messages"; import {current_user, realm} from "./state_data"; @@ -185,6 +186,10 @@ export function get_personal_menu_content_context() { status_text, status_emoji_info, user_time: people.get_user_time(my_user_id), + + // user color scheme + user_color_scheme: user_settings.color_scheme, + color_scheme_values: settings_config.color_scheme_values, }; } diff --git a/web/styles/app_components.css b/web/styles/app_components.css index 200a540fbb..425a5c9bc9 100644 --- a/web/styles/app_components.css +++ b/web/styles/app_components.css @@ -1179,3 +1179,132 @@ div.overlay { } } } + +/** + * Use the "tab-picker-vertical" class in conjunction + * for a vertical tab picker. + */ +.tab-picker { + position: relative; + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + gap: var(--grid-gap-tab-picker); + box-sizing: border-box; + border-radius: 5px; + /* Using max-content ensures equal width tabs. */ + min-width: max-content; + padding: 1px; + background-color: var(--color-background-tab-picker-container); + font-size: 16px; + + input[type="radio"] { + display: none; + } + + .tab-option-content { + z-index: 2; + display: flex; + justify-content: center; + align-items: center; + /* Avoids layout shift while showing a border when pressed. */ + border: 1px solid transparent; + border-radius: 4px; + box-sizing: border-box; + padding: 3px 7px; + color: var(--color-text-dropdown-menu); + + & .zulip-icon { + /* Clear inherited position. */ + position: static; + color: var(--color-icon-purple); + } + } + + .tab-option:not(:checked) + .tab-option-content { + &:hover, + &:focus-visible { + background-color: var( + --color-background-tab-picker-tab-option-hover + ); + } + + &:active { + background-color: var( + --color-background-tab-picker-tab-option-active + ); + } + } + + .tab-option { + &:nth-of-type(1):checked ~ .slider { + --tab-selected-tab-picker: 0; + } + + &:nth-of-type(2):checked ~ .slider { + --tab-selected-tab-picker: 1; + } + + &:nth-of-type(3):checked ~ .slider { + --tab-selected-tab-picker: 2; + } + + &:nth-of-type(4):checked ~ .slider { + --tab-selected-tab-picker: 3; + } + + &:nth-of-type(5):checked ~ .slider { + --tab-selected-tab-picker: 4; + } + /* If a tab picker with more than 5 tabs is required, + extend this using the same pattern as above. */ + } + + .slider { + position: absolute; + z-index: 1; + inset: 0; + grid-column: 1 / 2; + grid-row: 1 / 2; + /* Move along X-axis: own width + gap between two tabs. */ + transform: translateX( + calc( + (100% + var(--grid-gap-tab-picker)) * + var(--tab-selected-tab-picker) + ) + ); + transition: transform 0.25s cubic-bezier(0.64, 0, 0.78, 0); + box-sizing: border-box; + border: 1px solid var(--color-outline-tab-picker-tab-option); + border-radius: 4px; + background-color: var(--color-background-tab-picker-selected-tab); + } + + /* Detect if a user has enabled a setting on their device + to minimize the amount of non-essential motion. */ + @media (prefers-reduced-motion) { + .slider { + transition: none; + } + + .tab-option:not(:checked) + .tab-option-content:active { + border-color: var(--color-outline-tab-picker-tab-option); + } + } + + /* Related to vertical tab picker. */ + &.tab-picker-vertical { + grid-auto-flow: row; + grid-auto-rows: 1fr; + + .slider { + /* Move along Y-axis: own height + gap between two tabs. */ + transform: translateY( + calc( + (100% + var(--grid-gap-tab-picker)) * + var(--tab-selected-tab-picker) + ) + ); + } + } +} diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index 735a37f2c5..4eb3d9bd15 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -140,6 +140,9 @@ */ --stream-subscriber-list-max-height: 100%; + /* Gap between tabs in the tab picker */ + --grid-gap-tab-picker: 2px; + /* Colors used across the app */ --color-date: hsl(0deg 0% 15% / 75%); --color-background-private-message-header: hsl(46deg 35% 93%); @@ -195,6 +198,7 @@ --color-background-tab-picker-selected-tab: hsl(0deg 0% 100%); --color-outline-tab-picker-tab-option: hsl(0deg 0% 0% / 30%); --color-background-tab-picker-tab-option-hover: hsl(0deg 0% 100% / 60%); + --color-background-tab-picker-tab-option-active: hsl(0deg 0% 100% / 35%); --color-background-popover: hsl(0deg 0% 100%); --color-background-alert-word: hsl(18deg 100% 84%); --color-buddy-list-highlighted-user: hsl(120deg 12.3% 71.4% / 38%); @@ -500,6 +504,7 @@ --color-background-tab-picker-selected-tab: hsl(0deg 0% 100% / 7%); --color-outline-tab-picker-tab-option: hsl(0deg 0% 100% / 12%); --color-background-tab-picker-tab-option-hover: hsl(0deg 0% 100% / 5%); + --color-background-tab-picker-tab-option-active: hsl(0deg 0% 100% / 3%); --color-background-alert-word: hsl(22deg 70% 35%); --color-buddy-list-highlighted-user: hsl(136deg 25% 73% / 20%); --color-border-sidebar: hsl(0deg 0% 0% / 20%); diff --git a/web/styles/popovers.css b/web/styles/popovers.css index e03ba6e515..d190dbdff3 100644 --- a/web/styles/popovers.css +++ b/web/styles/popovers.css @@ -1481,3 +1481,14 @@ ul.navbar-dropdown-menu-outer-list { border: 1px solid var(--color-hotkey-hint); margin-left: auto; } + +#theme-switcher { + margin: 0 10px; + + .tab-option-content:focus-visible { + border-radius: 4px; + /* Override the default focus style */ + outline: 1px solid var(--color-outline-focus) !important; + outline-offset: -1px; + } +} diff --git a/web/templates/personal_menu.hbs b/web/templates/personal_menu.hbs index 8671d4615c..5d85d8256f 100644 --- a/web/templates/personal_menu.hbs +++ b/web/templates/personal_menu.hbs @@ -84,6 +84,27 @@ {{/if}} +