Use browser's font size instead of hardcoded 16px as root font size (#12246)

* WIP Use browser font size instead of hardcoded 16px

* Add font migration to v3

* Remove custom font size input

* Use a dropdown instead of a slider

* Add margin to the font size dropdown

* Fix `UpdateFontSizeDelta` action typo

* Fix `fontScale`in `Call.ts`

* Rename `baseFontSizeV3` to `fontSizeDelta`

* Update playwright test

* Add `default` next to the browser font size

* Remove remaining `TODO`

* Remove falsy `private`

* Improve doc

* Update snapshots after develop merge

* Remove commented import
This commit is contained in:
Florian Duros
2024-02-21 12:23:07 +01:00
committed by GitHub
parent 36a8d503df
commit 6d55ce0217
17 changed files with 453 additions and 366 deletions

View File

@@ -14,29 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from "react";
import React from "react";
import EventTilePreview from "../elements/EventTilePreview";
import Field from "../elements/Field";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsStore from "../../../settings/SettingsStore";
import Slider from "../elements/Slider";
import { FontWatcher } from "../../../settings/watchers/FontWatcher";
import { IValidationResult, IFieldState } from "../elements/Validation";
import { Layout } from "../../../settings/enums/Layout";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { SettingLevel } from "../../../settings/SettingLevel";
import { _t } from "../../../languageHandler";
import { clamp } from "../../../utils/numbers";
import SettingsSubsection from "./shared/SettingsSubsection";
import Field from "../elements/Field";
import { FontWatcher } from "../../../settings/watchers/FontWatcher";
interface IProps {}
interface IState {
browserFontSize: number;
// String displaying the current selected fontSize.
// Needs to be string for things like '17.' without
// Needs to be string for things like '1.' without
// trailing 0s.
fontSize: string;
fontSizeDelta: number;
useCustomFontSize: boolean;
layout: Layout;
// User profile data for the message preview
@@ -47,6 +44,10 @@ interface IState {
export default class FontScalingPanel extends React.Component<IProps, IState> {
private readonly MESSAGE_PREVIEW_TEXT = _t("common|preview_message");
/**
* Font sizes available (in px)
*/
private readonly sizes = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36];
private layoutWatcherRef?: string;
private unmounted = false;
@@ -54,7 +55,8 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
super(props);
this.state = {
fontSize: SettingsStore.getValue("baseFontSizeV2", null).toString(),
fontSizeDelta: SettingsStore.getValue<number>("fontSizeDelta", null),
browserFontSize: FontWatcher.getBrowserDefaultFontSize(),
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
layout: SettingsStore.getValue("layout"),
};
@@ -90,30 +92,22 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
}
}
private onFontSizeChanged = (size: number): void => {
this.setState({ fontSize: size.toString() });
SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, size);
/**
* Save the new font size
* @param delta
*/
private onFontSizeChanged = async (delta: string): Promise<void> => {
const parsedDelta = parseInt(delta, 10) || 0;
this.setState({ fontSizeDelta: parsedDelta });
await SettingsStore.setValue("fontSizeDelta", null, SettingLevel.DEVICE, parsedDelta);
};
private onValidateFontSize = async ({ value }: Pick<IFieldState, "value">): Promise<IValidationResult> => {
const parsedSize = parseFloat(value!);
const min = FontWatcher.MIN_SIZE;
const max = FontWatcher.MAX_SIZE;
if (isNaN(parsedSize)) {
return { valid: false, feedback: _t("settings|appearance|font_size_nan") };
}
if (!(min <= parsedSize && parsedSize <= max)) {
return {
valid: false,
feedback: _t("settings|appearance|font_size_limit", { min, max }),
};
}
SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, parseInt(value!, 10));
return { valid: true, feedback: _t("settings|appearance|font_size_valid", { min, max }) };
/**
* Compute the difference between the selected font size and the browser font size
* @param fontSize
*/
private computeDeltaFontSize = (fontSize: number): number => {
return fontSize - this.state.browserFontSize;
};
public render(): React.ReactNode {
@@ -123,6 +117,21 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
stretchContent
data-testid="mx_FontScalingPanel"
>
<Field
element="select"
className="mx_FontScalingPanel_Dropdown"
label={_t("settings|appearance|font_size")}
value={this.state.fontSizeDelta.toString()}
onChange={(e) => this.onFontSizeChanged(e.target.value)}
>
{this.sizes.map((size) => (
<option key={size} value={this.computeDeltaFontSize(size)}>
{size === this.state.browserFontSize
? _t("settings|appearance|font_size_default", { fontSize: size })
: size}
</option>
))}
</Field>
<EventTilePreview
className="mx_FontScalingPanel_preview"
message={this.MESSAGE_PREVIEW_TEXT}
@@ -131,49 +140,6 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<div className="mx_FontScalingPanel_fontSlider">
<div className="mx_FontScalingPanel_fontSlider_smallText">Aa</div>
<Slider
min={FontWatcher.MIN_SIZE}
max={FontWatcher.MAX_SIZE}
step={1}
value={parseInt(this.state.fontSize, 10)}
onChange={this.onFontSizeChanged}
displayFunc={(_) => ""}
disabled={this.state.useCustomFontSize}
label={_t("settings|appearance|font_size")}
/>
<div className="mx_FontScalingPanel_fontSlider_largeText">Aa</div>
</div>
<SettingsFlag
name="useCustomFontSize"
level={SettingLevel.ACCOUNT}
onChange={(checked) => {
this.setState({ useCustomFontSize: checked });
if (!checked) {
const size = parseInt(this.state.fontSize, 10);
const clamped = clamp(size, FontWatcher.MIN_SIZE, FontWatcher.MAX_SIZE);
if (clamped !== size) {
this.onFontSizeChanged(clamped);
}
}
}}
useCheckbox={true}
/>
<Field
type="number"
label={_t("settings|appearance|font_size")}
autoComplete="off"
placeholder={this.state.fontSize.toString()}
value={this.state.fontSize.toString()}
id="font_size_field"
onValidate={this.onValidateFontSize}
onChange={(value: ChangeEvent<HTMLInputElement>) => this.setState({ fontSize: value.target.value })}
disabled={!this.state.useCustomFontSize}
className="mx_AppearanceUserSettingsTab_checkboxControlledField"
/>
</SettingsSubsection>
);
}

View File

@@ -107,9 +107,11 @@ export enum Action {
MigrateBaseFontSize = "migrate_base_font_size",
/**
* Sets the apps root font size. Should be used with UpdateFontSizePayload
* Sets the apps root font size delta. Should be used with UpdateFontSizeDeltaPayload
* It will add the delta to the current font size.
* The delta should be between {@link FontWatcher.MIN_DELTA} and {@link FontWatcher.MAX_DELTA}.
*/
UpdateFontSize = "update_font_size",
UpdateFontSizeDelta = "update_font_size_delta",
/**
* Sets a system font. Should be used with UpdateSystemFontPayload

View File

@@ -17,11 +17,12 @@ limitations under the License.
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface UpdateFontSizePayload extends ActionPayload {
action: Action.UpdateFontSize;
export interface UpdateFontSizeDeltaPayload extends ActionPayload {
action: Action.UpdateFontSizeDelta;
/**
* The font size to set the root to
* The delta is added to the current font size.
* The delta should be between {@link FontWatcher.MIN_DELTA} and {@link FontWatcher.MAX_DELTA}.
*/
size: number;
delta: number;
}

View File

@@ -2422,9 +2422,7 @@
"custom_theme_success": "Theme added!",
"custom_theme_url": "Custom theme URL",
"font_size": "Font size",
"font_size_limit": "Custom font size can only be between %(min)s pt and %(max)s pt",
"font_size_nan": "Size must be a number",
"font_size_valid": "Use between %(min)s pt and %(max)s pt",
"font_size_default": "%(fontSize)s (default)",
"heading": "Customise your appearance",
"image_size_default": "Default",
"image_size_large": "Large",

View File

@@ -52,11 +52,11 @@ import WidgetStore from "../stores/WidgetStore";
import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widgets/WidgetMessagingStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore";
import { getCurrentLanguage } from "../languageHandler";
import { FontWatcher } from "../settings/watchers/FontWatcher";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { UPDATE_EVENT } from "../stores/AsyncStore";
import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers";
import { isVideoRoom } from "../utils/video-rooms";
import { FontWatcher } from "../settings/watchers/FontWatcher";
const TIMEOUT_MS = 16000;
@@ -687,7 +687,8 @@ export class ElementCall extends Call {
roomId: roomId,
baseUrl: client.baseUrl,
lang: getCurrentLanguage().replace("_", "-"),
fontScale: `${(SettingsStore.getValue("baseFontSizeV2") ?? 16) / FontWatcher.DEFAULT_SIZE}`,
fontScale: (FontWatcher.getRootFontSize() / FontWatcher.getBrowserDefaultFontSize()).toString(),
analyticsID,
});

View File

@@ -511,6 +511,9 @@ export const SETTINGS: { [setting: string]: ISetting } = {
supportedLevels: [SettingLevel.CONFIG],
default: 0,
},
/**
* @deprecated in favor of {@link fontSizeDelta}
*/
"baseFontSize": {
displayName: _td("settings|appearance|font_size"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
@@ -530,12 +533,22 @@ export const SETTINGS: { [setting: string]: ISetting } = {
* With the transition to Compound we are moving to a base font size
* of 16px. We're taking the opportunity to move away from the `baseFontSize`
* setting that had a 5px offset.
*
* @deprecated in favor {@link fontSizeDelta}
*/
"baseFontSizeV2": {
displayName: _td("settings|appearance|font_size"),
supportedLevels: [SettingLevel.DEVICE],
default: FontWatcher.DEFAULT_SIZE,
default: "",
controller: new FontSizeController(),
},
/**
* This delta is added to the browser default font size
* Moving from `baseFontSizeV2` to `fontSizeDelta` to replace the default 16px to --cpd-font-size-root (browser default font size) + fontSizeDelta
*/
"fontSizeDelta": {
displayName: _td("settings|appearance|font_size"),
supportedLevels: [SettingLevel.DEVICE],
default: FontWatcher.DEFAULT_DELTA,
controller: new FontSizeController(),
},
"useCustomFontSize": {

View File

@@ -16,7 +16,7 @@ limitations under the License.
import SettingController from "./SettingController";
import dis from "../../dispatcher/dispatcher";
import { UpdateFontSizePayload } from "../../dispatcher/payloads/UpdateFontSizePayload";
import { UpdateFontSizeDeltaPayload } from "../../dispatcher/payloads/UpdateFontSizeDeltaPayload";
import { Action } from "../../dispatcher/actions";
import { SettingLevel } from "../SettingLevel";
@@ -34,9 +34,9 @@ export default class FontSizeController extends SettingController {
dis.fire(Action.MigrateBaseFontSize);
} else if (newValue !== "") {
// Dispatch font size change so that everything open responds to the change.
dis.dispatch<UpdateFontSizePayload>({
action: Action.UpdateFontSize,
size: newValue,
dis.dispatch<UpdateFontSizeDeltaPayload>({
action: Action.UpdateFontSizeDelta,
delta: newValue,
});
}
}

View File

@@ -22,20 +22,19 @@ import { Action } from "../../dispatcher/actions";
import { SettingLevel } from "../SettingLevel";
import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload";
import { ActionPayload } from "../../dispatcher/payloads";
import { clamp } from "../../utils/numbers";
export class FontWatcher implements IWatcher {
/**
* Value indirectly defined by Compound.
* All `rem` calculations are made from a `16px` values in the
* @vector-im/compound-design-tokens package
*
* We might want to move to using `100%` instead so we can inherit the user
* preference set in the browser regarding font sizes.
* This Compound value is using `100%` of the default browser font size.
* It allows EW to use the browser's default font size instead of a fixed value.
* All the Compound font size are using `rem`, they are relative to the root font size
* and therefore of the browser font size.
*/
public static readonly DEFAULT_SIZE = 16;
public static readonly MIN_SIZE = FontWatcher.DEFAULT_SIZE - 5;
public static readonly MAX_SIZE = FontWatcher.DEFAULT_SIZE + 5;
private static readonly DEFAULT_SIZE = "var(--cpd-font-size-root)";
/**
* Default delta added to the ${@link DEFAULT_SIZE}
*/
public static readonly DEFAULT_DELTA = 0;
private dispatcherRef: string | null;
@@ -54,28 +53,106 @@ export class FontWatcher implements IWatcher {
}
/**
* Migrating the old `baseFontSize` for Compound.
* Everything will becomes slightly larger, and getting rid of the `SIZE_DIFF`
* weirdness for locally persisted values
* Migrate the base font size from the V1 and V2 version to the V3 version
* @private
*/
private async migrateBaseFontSize(): Promise<void> {
const legacyBaseFontSize = SettingsStore.getValue("baseFontSize");
if (legacyBaseFontSize) {
console.log("Migrating base font size for Compound, current value", legacyBaseFontSize);
await this.migrateBaseFontV1toFontSizeDelta();
await this.migrateBaseFontV2toFontSizeDelta();
}
// For some odd reason, the persisted value in user storage has an offset
// of 5 pixels for all values stored under `baseFontSize`
const LEGACY_SIZE_DIFF = 5;
// Compound uses a base font size of `16px`, whereas the old Element
// styles based their calculations off a `15px` root font size.
const ROOT_FONT_SIZE_INCREASE = 1;
/**
* Migrating from the V1 version of the base font size to the new delta system.
* The delta system is using the default browser font size as a base
* Everything will become slightly larger, and getting rid of the `SIZE_DIFF`
* weirdness for locally persisted values
* @private
*/
private async migrateBaseFontV1toFontSizeDelta(): Promise<void> {
const legacyBaseFontSize = SettingsStore.getValue<number>("baseFontSize");
// No baseFontV1 found, nothing to migrate
if (!legacyBaseFontSize) return;
const baseFontSize = legacyBaseFontSize + ROOT_FONT_SIZE_INCREASE + LEGACY_SIZE_DIFF;
console.log(
"Migrating base font size -> base font size V2 -> font size delta for Compound, current value",
legacyBaseFontSize,
);
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, baseFontSize);
await SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, "");
console.log("Migration complete, deleting legacy `baseFontSize`");
}
// Compute the V1 to V2 version before migrating to fontSizeDelta
const baseFontSizeV2 = this.computeBaseFontSizeV1toV2(legacyBaseFontSize);
// Compute the difference between the V2 and the fontSizeDelta
const delta = this.computeFontSizeDeltaFromV2BaseFontSize(baseFontSizeV2);
await SettingsStore.setValue("fontSizeDelta", null, SettingLevel.DEVICE, delta);
await SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, 0);
console.log("Migration complete, deleting legacy `baseFontSize`");
}
/**
* Migrating from the V2 version of the base font size to the new delta system
* @private
*/
private async migrateBaseFontV2toFontSizeDelta(): Promise<void> {
const legacyBaseFontV2Size = SettingsStore.getValue<number>("baseFontSizeV2");
// No baseFontV2 found, nothing to migrate
if (!legacyBaseFontV2Size) return;
console.log("Migrating base font size V2 for Compound, current value", legacyBaseFontV2Size);
// Compute the difference between the V2 and the fontSizeDelta
const delta = this.computeFontSizeDeltaFromV2BaseFontSize(legacyBaseFontV2Size);
await SettingsStore.setValue("fontSizeDelta", null, SettingLevel.DEVICE, delta);
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 0);
console.log("Migration complete, deleting legacy `baseFontSizeV2`");
}
/**
* Compute the V2 font size from the V1 font size
* @param legacyBaseFontSize
* @private
*/
private computeBaseFontSizeV1toV2(legacyBaseFontSize: number): number {
// For some odd reason, the persisted value in user storage has an offset
// of 5 pixels for all values stored under `baseFontSize`
const LEGACY_SIZE_DIFF = 5;
// Compound uses a base font size of `16px`, whereas the old Element
// styles based their calculations off a `15px` root font size.
const ROOT_FONT_SIZE_INCREASE = 1;
// Compute the font size of the V2 version before migrating to V3
return legacyBaseFontSize + ROOT_FONT_SIZE_INCREASE + LEGACY_SIZE_DIFF;
}
/**
* Compute the difference between the V2 font size and the default browser font size
* @param legacyBaseFontV2Size
* @private
*/
private computeFontSizeDeltaFromV2BaseFontSize(legacyBaseFontV2Size: number): number {
const browserDefaultFontSize = FontWatcher.getRootFontSize();
// Compute the difference between the V2 font size and the default browser font size
return legacyBaseFontV2Size - browserDefaultFontSize;
}
/**
* Get the root font size of the document
* Fallback to 16px if the value is not found
* @returns {number}
*/
public static getRootFontSize(): number {
return parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("font-size"), 10) || 16;
}
/**
* Get the browser default font size
* @returns {number} the default font size of the browser
*/
public static getBrowserDefaultFontSize(): number {
return this.getRootFontSize() - SettingsStore.getValue<number>("fontSizeDelta");
}
public stop(): void {
@@ -84,7 +161,7 @@ export class FontWatcher implements IWatcher {
}
private updateFont(): void {
this.setRootFontSize(SettingsStore.getValue("baseFontSizeV2"));
this.setRootFontSize(SettingsStore.getValue<number>("fontSizeDelta"));
this.setSystemFont({
useBundledEmojiFont: SettingsStore.getValue("useBundledEmojiFont"),
useSystemFont: SettingsStore.getValue("useSystemFont"),
@@ -95,13 +172,13 @@ export class FontWatcher implements IWatcher {
private onAction = (payload: ActionPayload): void => {
if (payload.action === Action.MigrateBaseFontSize) {
this.migrateBaseFontSize();
} else if (payload.action === Action.UpdateFontSize) {
this.setRootFontSize(payload.size);
} else if (payload.action === Action.UpdateFontSizeDelta) {
this.setRootFontSize(payload.delta);
} else if (payload.action === Action.UpdateSystemFont) {
this.setSystemFont(payload as UpdateSystemFontPayload);
} else if (payload.action === Action.OnLoggedOut) {
// Clear font overrides when logging out
this.setRootFontSize(FontWatcher.DEFAULT_SIZE);
this.setRootFontSize(FontWatcher.DEFAULT_DELTA);
this.setSystemFont({
useBundledEmojiFont: false,
useSystemFont: false,
@@ -113,13 +190,14 @@ export class FontWatcher implements IWatcher {
}
};
private setRootFontSize = async (size: number): Promise<void> => {
const fontSize = clamp(size, FontWatcher.MIN_SIZE, FontWatcher.MAX_SIZE);
if (fontSize !== size) {
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, fontSize);
}
document.querySelector<HTMLElement>(":root")!.style.fontSize = toPx(fontSize);
/**
* Set the root font size of the document
* @param delta {number} the delta to add to the default font size
*/
private setRootFontSize = async (delta: number): Promise<void> => {
// Add the delta to the browser default font size
document.querySelector<HTMLElement>(":root")!.style.fontSize =
`calc(${FontWatcher.DEFAULT_SIZE} + ${toPx(delta)})`;
};
public static readonly FONT_FAMILY_CUSTOM_PROPERTY = "--cpd-font-family-sans";