mirror of https://github.com/zulip/zulip.git
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.
This commit is contained in:
parent
f4d58f1ba6
commit
ad013a94b3
|
@ -3,6 +3,7 @@ import tippy from "tippy.js";
|
||||||
|
|
||||||
import render_personal_menu from "../templates/personal_menu.hbs";
|
import render_personal_menu from "../templates/personal_menu.hbs";
|
||||||
|
|
||||||
|
import * as channel from "./channel";
|
||||||
import * as narrow from "./narrow";
|
import * as narrow from "./narrow";
|
||||||
import * as people from "./people";
|
import * as people from "./people";
|
||||||
import * as popover_menus from "./popover_menus";
|
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 * as popovers from "./popovers";
|
||||||
import {current_user} from "./state_data";
|
import {current_user} from "./state_data";
|
||||||
import {parse_html} from "./ui_util";
|
import {parse_html} from "./ui_util";
|
||||||
|
import {user_settings} from "./user_settings";
|
||||||
import * as user_status from "./user_status";
|
import * as user_status from "./user_status";
|
||||||
|
|
||||||
export function initialize() {
|
export function initialize() {
|
||||||
|
@ -40,6 +42,26 @@ export function initialize() {
|
||||||
appendTo: document.body,
|
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) => {
|
$popper.one("click", ".personal-menu-clear-status", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const me = current_user.user_id;
|
const me = current_user.user_id;
|
||||||
|
|
|
@ -14,6 +14,7 @@ import * as message_lists from "./message_lists";
|
||||||
import * as muted_users from "./muted_users";
|
import * as muted_users from "./muted_users";
|
||||||
import {page_params} from "./page_params";
|
import {page_params} from "./page_params";
|
||||||
import * as people from "./people";
|
import * as people from "./people";
|
||||||
|
import * as settings_config from "./settings_config";
|
||||||
import * as settings_data from "./settings_data";
|
import * as settings_data from "./settings_data";
|
||||||
import * as starred_messages from "./starred_messages";
|
import * as starred_messages from "./starred_messages";
|
||||||
import {current_user, realm} from "./state_data";
|
import {current_user, realm} from "./state_data";
|
||||||
|
@ -185,6 +186,10 @@ export function get_personal_menu_content_context() {
|
||||||
status_text,
|
status_text,
|
||||||
status_emoji_info,
|
status_emoji_info,
|
||||||
user_time: people.get_user_time(my_user_id),
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1195,3 +1195,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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -140,6 +140,9 @@
|
||||||
*/
|
*/
|
||||||
--stream-subscriber-list-max-height: 100%;
|
--stream-subscriber-list-max-height: 100%;
|
||||||
|
|
||||||
|
/* Gap between tabs in the tab picker */
|
||||||
|
--grid-gap-tab-picker: 2px;
|
||||||
|
|
||||||
/* Colors used across the app */
|
/* Colors used across the app */
|
||||||
--color-date: hsl(0deg 0% 15% / 75%);
|
--color-date: hsl(0deg 0% 15% / 75%);
|
||||||
--color-background-private-message-header: hsl(46deg 35% 93%);
|
--color-background-private-message-header: hsl(46deg 35% 93%);
|
||||||
|
@ -195,6 +198,7 @@
|
||||||
--color-background-tab-picker-selected-tab: hsl(0deg 0% 100%);
|
--color-background-tab-picker-selected-tab: hsl(0deg 0% 100%);
|
||||||
--color-outline-tab-picker-tab-option: hsl(0deg 0% 0% / 30%);
|
--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-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-popover: hsl(0deg 0% 100%);
|
||||||
--color-background-alert-word: hsl(18deg 100% 84%);
|
--color-background-alert-word: hsl(18deg 100% 84%);
|
||||||
--color-buddy-list-highlighted-user: hsl(120deg 12.3% 71.4% / 38%);
|
--color-buddy-list-highlighted-user: hsl(120deg 12.3% 71.4% / 38%);
|
||||||
|
@ -497,6 +501,7 @@
|
||||||
--color-background-tab-picker-selected-tab: hsl(0deg 0% 100% / 7%);
|
--color-background-tab-picker-selected-tab: hsl(0deg 0% 100% / 7%);
|
||||||
--color-outline-tab-picker-tab-option: hsl(0deg 0% 100% / 12%);
|
--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-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-background-alert-word: hsl(22deg 70% 35%);
|
||||||
--color-buddy-list-highlighted-user: hsl(136deg 25% 73% / 20%);
|
--color-buddy-list-highlighted-user: hsl(136deg 25% 73% / 20%);
|
||||||
|
|
||||||
|
|
|
@ -1488,3 +1488,14 @@ ul.navbar-dropdown-menu-outer-list {
|
||||||
border: 1px solid var(--color-hotkey-hint);
|
border: 1px solid var(--color-hotkey-hint);
|
||||||
margin-left: auto;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -84,6 +84,27 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="navbar-dropdown-menu-outer-list-item">
|
||||||
|
<ul class="navbar-dropdown-menu-inner-list">
|
||||||
|
<li class="navbar-dropdown-menu-inner-list-item">
|
||||||
|
<div id="theme-switcher" class="tab-picker">
|
||||||
|
<input type="radio" id="select-automatic-theme" class="tab-option" name="theme-select" data-theme-code="{{color_scheme_values.automatic.code}}" {{#if (eq user_color_scheme color_scheme_values.automatic.code)}}checked{{/if}} />
|
||||||
|
<label class="tab-option-content tippy-zulip-delayed-tooltip" for="select-automatic-theme" aria-label="{{t 'Select automatic theme' }}" data-tooltip-template-id="automatic-theme-template" tabindex="0">
|
||||||
|
<i class="zulip-icon zulip-icon-monitor" aria-hidden="true"></i>
|
||||||
|
</label>
|
||||||
|
<input type="radio" id="select-light-theme" class="tab-option" name="theme-select" data-theme-code="{{color_scheme_values.day.code}}" {{#if (eq user_color_scheme color_scheme_values.day.code)}}checked{{/if}} />
|
||||||
|
<label class="tab-option-content tippy-zulip-delayed-tooltip" for="select-light-theme" aria-label="{{t 'Select light theme' }}" data-tippy-content="{{t 'Light theme' }}" tabindex="0">
|
||||||
|
<i class="zulip-icon zulip-icon-sun" aria-hidden="true"></i>
|
||||||
|
</label>
|
||||||
|
<input type="radio" id="select-dark-theme" class="tab-option" name="theme-select" data-theme-code="{{color_scheme_values.night.code}}" {{#if (eq user_color_scheme color_scheme_values.night.code)}}checked{{/if}} />
|
||||||
|
<label class="tab-option-content tippy-zulip-delayed-tooltip" for="select-dark-theme" aria-label="{{t 'Select dark theme' }}" data-tippy-content="{{t 'Dark theme' }}" tabindex="0">
|
||||||
|
<i class="zulip-icon zulip-icon-moon" aria-hidden="true"></i>
|
||||||
|
</label>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li class="navbar-dropdown-menu-outer-list-item">
|
<li class="navbar-dropdown-menu-outer-list-item">
|
||||||
<ul class="navbar-dropdown-menu-inner-list">
|
<ul class="navbar-dropdown-menu-inner-list">
|
||||||
<li class="link-item navbar-dropdown-menu-inner-list-item">
|
<li class="link-item navbar-dropdown-menu-inner-list-item">
|
||||||
|
|
|
@ -85,6 +85,12 @@
|
||||||
{{t 'Help menu' }}
|
{{t 'Help menu' }}
|
||||||
{{tooltip_hotkey_hints "G" "←"}}
|
{{tooltip_hotkey_hints "G" "←"}}
|
||||||
</template>
|
</template>
|
||||||
|
<template id="automatic-theme-template">
|
||||||
|
<div>
|
||||||
|
<div>{{t "Automatic theme" }}</div>
|
||||||
|
<div class="tooltip-inner-content italic">{{t "Follows system settings." }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template id="all-message-tooltip-template">
|
<template id="all-message-tooltip-template">
|
||||||
<div class="views-tooltip-container" data-view-code="all_messages">
|
<div class="views-tooltip-container" data-view-code="all_messages">
|
||||||
<div>{{t 'All messages' }}</div>
|
<div>{{t 'All messages' }}</div>
|
||||||
|
|
Loading…
Reference in New Issue