mirror of https://github.com/vector-im/riot-web
Add a high contrast theme (a variant of the light theme) (#7036)
* Enable choosing a high contrast variant of the Light theme * Updates to high contrast theme to match design and show focus * Adjust the outline-offset to match designs * Don't draw an outline around the active tab * Prevent cropping of outlines on buttons * Use the correct colour for links * Change light grey text to be darker in high contrast theme * Use a darker text colour in admin panel * Adjust background colours of back button and font sliderpull/21833/head
parent
8170697bbf
commit
abbc39cdec
|
@ -200,10 +200,10 @@
|
||||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||||
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||||
|
@import "./views/right_panel/_ThreadPanel.scss";
|
||||||
@import "./views/right_panel/_UserInfo.scss";
|
@import "./views/right_panel/_UserInfo.scss";
|
||||||
@import "./views/right_panel/_VerificationPanel.scss";
|
@import "./views/right_panel/_VerificationPanel.scss";
|
||||||
@import "./views/right_panel/_WidgetCard.scss";
|
@import "./views/right_panel/_WidgetCard.scss";
|
||||||
@import "./views/right_panel/_ThreadPanel.scss";
|
|
||||||
@import "./views/room_settings/_AliasSettings.scss";
|
@import "./views/room_settings/_AliasSettings.scss";
|
||||||
@import "./views/rooms/_AppsDrawer.scss";
|
@import "./views/rooms/_AppsDrawer.scss";
|
||||||
@import "./views/rooms/_Autocomplete.scss";
|
@import "./views/rooms/_Autocomplete.scss";
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
//// Reference: https://www.figma.com/file/RnLKnv09glhxGIZtn8zfmh/UI-Themes-%26-Accessibility?node-id=321%3A65847
|
//// Reference: https://www.figma.com/file/RnLKnv09glhxGIZtn8zfmh/UI-Themes-%26-Accessibility?node-id=321%3A65847
|
||||||
$accent: #268075;
|
$accent: #268075;
|
||||||
$alert: #D62C25;
|
$alert: #D62C25;
|
||||||
$notice-primary-color: #D61C25;
|
|
||||||
$links: #0A6ECA;
|
$links: #0A6ECA;
|
||||||
$secondary-content: #5E6266;
|
$secondary-content: #5E6266;
|
||||||
$tertiary-content: #5E6266; // Same as secondary
|
$tertiary-content: $secondary-content;
|
||||||
$quaternary-content: #5E6266; // Same as secondary
|
$quaternary-content: $secondary-content;
|
||||||
|
$quinary-content: $secondary-content;
|
||||||
|
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2);
|
||||||
|
|
||||||
$username-variant1-color: #0A6ECA;
|
$username-variant1-color: #0A6ECA;
|
||||||
$username-variant2-color: #AC3BA8;
|
$username-variant2-color: #AC3BA8;
|
||||||
|
@ -18,9 +19,13 @@ $username-variant8-color: #3E810A;
|
||||||
|
|
||||||
$accent-color: $accent;
|
$accent-color: $accent;
|
||||||
$accent-color-50pct: rgba($accent-color, 0.5);
|
$accent-color-50pct: rgba($accent-color, 0.5);
|
||||||
|
$accent-color-alt: $links;
|
||||||
|
$input-border-color: $secondary-content;
|
||||||
$input-darker-bg-color: $quinary-content;
|
$input-darker-bg-color: $quinary-content;
|
||||||
|
$input-darker-fg-color: $secondary-content;
|
||||||
$input-lighter-fg-color: $input-darker-fg-color;
|
$input-lighter-fg-color: $input-darker-fg-color;
|
||||||
$input-valid-border-color: $accent-color;
|
$input-valid-border-color: $accent-color;
|
||||||
|
$input-focused-border-color: $accent-color;
|
||||||
$button-bg-color: $accent-color;
|
$button-bg-color: $accent-color;
|
||||||
$resend-button-divider-color: $input-darker-bg-color;
|
$resend-button-divider-color: $input-darker-bg-color;
|
||||||
$icon-button-color: $quaternary-content;
|
$icon-button-color: $quaternary-content;
|
||||||
|
@ -41,12 +46,14 @@ $voice-record-stop-border-color: $quinary-content;
|
||||||
$voice-record-icon-color: $tertiary-content;
|
$voice-record-icon-color: $tertiary-content;
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
$eventbubble-reply-color: $quaternary-content;
|
$eventbubble-reply-color: $quaternary-content;
|
||||||
|
$notice-primary-color: $alert;
|
||||||
$warning-color: $notice-primary-color; // red
|
$warning-color: $notice-primary-color; // red
|
||||||
$pinned-unread-color: $notice-primary-color;
|
$pinned-unread-color: $notice-primary-color;
|
||||||
$button-danger-bg-color: $notice-primary-color;
|
$button-danger-bg-color: $notice-primary-color;
|
||||||
$mention-user-pill-bg-color: $warning-color;
|
$mention-user-pill-bg-color: $warning-color;
|
||||||
$input-invalid-border-color: $warning-color;
|
$input-invalid-border-color: $warning-color;
|
||||||
$event-highlight-fg-color: $warning-color;
|
$event-highlight-fg-color: $warning-color;
|
||||||
|
$roomtopic-color: $secondary-content;
|
||||||
|
|
||||||
@define-mixin mx_DialogButton_danger {
|
@define-mixin mx_DialogButton_danger {
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
|
@ -64,3 +71,38 @@ $event-highlight-fg-color: $warning-color;
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton:focus {
|
||||||
|
outline: 2px solid $accent-color;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BasicMessageComposer .mx_BasicMessageComposer_inputEmpty > :first-child::before {
|
||||||
|
color: $secondary-content;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TextualEvent {
|
||||||
|
color: $secondary-content;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dialog, .mx_MatrixChat_wrapper {
|
||||||
|
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=text]::placeholder,
|
||||||
|
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search]::placeholder,
|
||||||
|
.mx_textinput input::placeholder {
|
||||||
|
color: $input-darker-fg-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserMenu_contextMenu .mx_UserMenu_contextMenu_header .mx_UserMenu_contextMenu_themeButton {
|
||||||
|
background-color: $roomlist-button-bg-color !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_FontScalingPanel_fontSlider {
|
||||||
|
background-color: $roomlist-button-bg-color !important;
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { enumerateThemes } from "../../../theme";
|
import { enumerateThemes, findHighContrastTheme, findNonHighContrastTheme, isHighContrastTheme } from "../../../theme";
|
||||||
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
|
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
@ -159,7 +159,37 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
||||||
this.setState({ customThemeUrl: e.target.value });
|
this.setState({ customThemeUrl: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
private renderHighContrastCheckbox(): React.ReactElement<HTMLDivElement> {
|
||||||
|
if (
|
||||||
|
!this.state.useSystemTheme && (
|
||||||
|
findHighContrastTheme(this.state.theme) ||
|
||||||
|
isHighContrastTheme(this.state.theme)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return <div>
|
||||||
|
<StyledCheckbox
|
||||||
|
checked={isHighContrastTheme(this.state.theme)}
|
||||||
|
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
|
||||||
|
>
|
||||||
|
{ _t( "Use high contrast" ) }
|
||||||
|
</StyledCheckbox>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private highContrastThemeChanged(checked: boolean): void {
|
||||||
|
let newTheme: string;
|
||||||
|
if (checked) {
|
||||||
|
newTheme = findHighContrastTheme(this.state.theme);
|
||||||
|
} else {
|
||||||
|
newTheme = findNonHighContrastTheme(this.state.theme);
|
||||||
|
}
|
||||||
|
if (newTheme) {
|
||||||
|
this.onThemeChange(newTheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<HTMLDivElement> {
|
||||||
const themeWatcher = new ThemeWatcher();
|
const themeWatcher = new ThemeWatcher();
|
||||||
let systemThemeSection: JSX.Element;
|
let systemThemeSection: JSX.Element;
|
||||||
if (themeWatcher.isSystemThemeSupported()) {
|
if (themeWatcher.isSystemThemeSupported()) {
|
||||||
|
@ -210,7 +240,8 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// XXX: replace any type here
|
// XXX: replace any type here
|
||||||
const themes = Object.entries<any>(enumerateThemes())
|
const themes = Object.entries<any>(enumerateThemes())
|
||||||
.map(p => ({ id: p[0], name: p[1] })); // convert pairs to objects for code readability
|
.map(p => ({ id: p[0], name: p[1] })) // convert pairs to objects for code readability
|
||||||
|
.filter(p => !isHighContrastTheme(p.id));
|
||||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||||
.sort((a, b) => compare(a.name, b.name));
|
.sort((a, b) => compare(a.name, b.name));
|
||||||
|
@ -229,12 +260,21 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
||||||
className: "mx_ThemeSelector_" + t.id,
|
className: "mx_ThemeSelector_" + t.id,
|
||||||
}))}
|
}))}
|
||||||
onChange={this.onThemeChange}
|
onChange={this.onThemeChange}
|
||||||
value={this.state.useSystemTheme ? undefined : this.state.theme}
|
value={this.apparentSelectedThemeId()}
|
||||||
outlined
|
outlined
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{ this.renderHighContrastCheckbox() }
|
||||||
{ customThemeForm }
|
{ customThemeForm }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apparentSelectedThemeId() {
|
||||||
|
if (this.state.useSystemTheme) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const nonHighContrast = findNonHighContrastTheme(this.state.theme);
|
||||||
|
return nonHighContrast ? nonHighContrast : this.state.theme;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -579,6 +579,7 @@
|
||||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
|
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
|
||||||
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
|
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
|
"Light high contrast": "Light high contrast",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
"%(displayName)s is typing …": "%(displayName)s is typing …",
|
"%(displayName)s is typing …": "%(displayName)s is typing …",
|
||||||
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
|
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
|
||||||
|
@ -1293,6 +1294,7 @@
|
||||||
"Invalid theme schema.": "Invalid theme schema.",
|
"Invalid theme schema.": "Invalid theme schema.",
|
||||||
"Error downloading theme information.": "Error downloading theme information.",
|
"Error downloading theme information.": "Error downloading theme information.",
|
||||||
"Theme added!": "Theme added!",
|
"Theme added!": "Theme added!",
|
||||||
|
"Use high contrast": "Use high contrast",
|
||||||
"Custom theme URL": "Custom theme URL",
|
"Custom theme URL": "Custom theme URL",
|
||||||
"Add theme": "Add theme",
|
"Add theme": "Add theme",
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
|
|
31
src/theme.ts
31
src/theme.ts
|
@ -21,6 +21,9 @@ import SettingsStore from "./settings/SettingsStore";
|
||||||
import ThemeWatcher from "./settings/watchers/ThemeWatcher";
|
import ThemeWatcher from "./settings/watchers/ThemeWatcher";
|
||||||
|
|
||||||
export const DEFAULT_THEME = "light";
|
export const DEFAULT_THEME = "light";
|
||||||
|
const HIGH_CONTRAST_THEMES = {
|
||||||
|
"light": "light-high-contrast",
|
||||||
|
};
|
||||||
|
|
||||||
interface IFontFaces {
|
interface IFontFaces {
|
||||||
src: {
|
src: {
|
||||||
|
@ -42,9 +45,37 @@ interface ICustomTheme {
|
||||||
is_dark?: boolean; // eslint-disable-line camelcase
|
is_dark?: boolean; // eslint-disable-line camelcase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a non-high-contrast theme, find the corresponding high-contrast one
|
||||||
|
* if it exists, or return undefined if not.
|
||||||
|
*/
|
||||||
|
export function findHighContrastTheme(theme: string) {
|
||||||
|
return HIGH_CONTRAST_THEMES[theme];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a high-contrast theme, find the corresponding non-high-contrast one
|
||||||
|
* if it exists, or return undefined if not.
|
||||||
|
*/
|
||||||
|
export function findNonHighContrastTheme(hcTheme: string) {
|
||||||
|
for (const theme in HIGH_CONTRAST_THEMES) {
|
||||||
|
if (HIGH_CONTRAST_THEMES[theme] === hcTheme) {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide whether the supplied theme is high contrast.
|
||||||
|
*/
|
||||||
|
export function isHighContrastTheme(theme: string) {
|
||||||
|
return Object.values(HIGH_CONTRAST_THEMES).includes(theme);
|
||||||
|
}
|
||||||
|
|
||||||
export function enumerateThemes(): {[key: string]: string} {
|
export function enumerateThemes(): {[key: string]: string} {
|
||||||
const BUILTIN_THEMES = {
|
const BUILTIN_THEMES = {
|
||||||
"light": _t("Light"),
|
"light": _t("Light"),
|
||||||
|
"light-high-contrast": _t("Light high contrast"),
|
||||||
"dark": _t("Dark"),
|
"dark": _t("Dark"),
|
||||||
};
|
};
|
||||||
const customThemes = SettingsStore.getValue("custom_themes");
|
const customThemes = SettingsStore.getValue("custom_themes");
|
||||||
|
|
Loading…
Reference in New Issue