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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 453 additions and 366 deletions

View file

@ -73,48 +73,18 @@ test.describe("Appearance user settings tab", () => {
await expect(page.locator(".mx_RoomView_body[data-layout='bubble']")).toBeVisible(); await expect(page.locator(".mx_RoomView_body[data-layout='bubble']")).toBeVisible();
}); });
test("should support changing font size by clicking the font slider", async ({ page, app, user }) => { test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => {
await app.settings.openUserSettings("Appearance"); await app.settings.openUserSettings("Appearance");
const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
const fontSliderSection = tab.locator(".mx_FontScalingPanel_fontSlider"); const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown");
await expect(fontDropdown.getByLabel("Font size")).toBeVisible();
await expect(fontSliderSection.getByLabel("Font size")).toBeVisible(); // Default browser font size is 16px and the select value is 0
// -4 value is 12px
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
const slider = fontSliderSection.getByRole("slider"); await expect(page).toMatchScreenshot("window-12px.png");
// Click the left position of the slider
await slider.click({ position: { x: 0, y: 10 } });
const MIN_FONT_SIZE = 11;
// Assert that the smallest font size is selected
await expect(fontSliderSection.locator(`input[value='${MIN_FONT_SIZE}']`)).toBeVisible();
await expect(
fontSliderSection.locator("output .mx_Slider_selection_label", { hasText: String(MIN_FONT_SIZE) }),
).toBeVisible();
await expect(fontSliderSection).toMatchScreenshot(`font-slider-${MIN_FONT_SIZE}.png`);
// Click the right position of the slider
await slider.click({ position: { x: 572, y: 10 } });
const MAX_FONT_SIZE = 21;
// Assert that the largest font size is selected
await expect(fontSliderSection.locator(`input[value='${MAX_FONT_SIZE}']`)).toBeVisible();
await expect(
fontSliderSection.locator("output .mx_Slider_selection_label", { hasText: String(MAX_FONT_SIZE) }),
).toBeVisible();
await expect(fontSliderSection).toMatchScreenshot(`font-slider-${MAX_FONT_SIZE}.png`);
});
test("should disable font size slider when custom font size is used", async ({ page, app, user }) => {
await app.settings.openUserSettings("Appearance");
const panel = page.getByTestId("mx_FontScalingPanel");
await panel.locator("label", { hasText: "Use custom size" }).click();
// Assert that the font slider is disabled
await expect(panel.locator(".mx_FontScalingPanel_fontSlider input[disabled]")).toBeVisible();
}); });
test("should support enabling compact group (modern) layout", async ({ page, app, user }) => { test("should support enabling compact group (modern) layout", async ({ page, app, user }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -36,26 +36,8 @@ limitations under the License.
} }
} }
.mx_FontScalingPanel_fontSlider { .mx_FontScalingPanel_Dropdown {
display: flex; width: 120px;
align-items: center; /* Override default mx_Field margin */
padding: 15px $spacing-20 35px; margin-bottom: var(--cpd-space-2x) !important;
background: $panels;
border-radius: 10px;
font-size: $font-10px;
.mx_FontScalingPanel_fontSlider_smallText,
.mx_FontScalingPanel_fontSlider_largeText {
font-weight: 500;
}
.mx_FontScalingPanel_fontSlider_smallText {
font-size: $font-15px;
padding-inline-end: $spacing-20;
}
.mx_FontScalingPanel_fontSlider_largeText {
font-size: $font-18px;
padding-inline-start: $spacing-20;
}
} }

View file

@ -14,29 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ChangeEvent } from "react"; import React from "react";
import EventTilePreview from "../elements/EventTilePreview"; import EventTilePreview from "../elements/EventTilePreview";
import Field from "../elements/Field";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsStore from "../../../settings/SettingsStore"; 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 { Layout } from "../../../settings/enums/Layout";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { clamp } from "../../../utils/numbers";
import SettingsSubsection from "./shared/SettingsSubsection"; import SettingsSubsection from "./shared/SettingsSubsection";
import Field from "../elements/Field";
import { FontWatcher } from "../../../settings/watchers/FontWatcher";
interface IProps {} interface IProps {}
interface IState { interface IState {
browserFontSize: number;
// String displaying the current selected fontSize. // 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. // trailing 0s.
fontSize: string; fontSizeDelta: number;
useCustomFontSize: boolean; useCustomFontSize: boolean;
layout: Layout; layout: Layout;
// User profile data for the message preview // User profile data for the message preview
@ -47,6 +44,10 @@ interface IState {
export default class FontScalingPanel extends React.Component<IProps, IState> { export default class FontScalingPanel extends React.Component<IProps, IState> {
private readonly MESSAGE_PREVIEW_TEXT = _t("common|preview_message"); 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 layoutWatcherRef?: string;
private unmounted = false; private unmounted = false;
@ -54,7 +55,8 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
super(props); super(props);
this.state = { this.state = {
fontSize: SettingsStore.getValue("baseFontSizeV2", null).toString(), fontSizeDelta: SettingsStore.getValue<number>("fontSizeDelta", null),
browserFontSize: FontWatcher.getBrowserDefaultFontSize(),
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"), useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
layout: SettingsStore.getValue("layout"), 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() }); * Save the new font size
SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 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!); * Compute the difference between the selected font size and the browser font size
const min = FontWatcher.MIN_SIZE; * @param fontSize
const max = FontWatcher.MAX_SIZE; */
private computeDeltaFontSize = (fontSize: number): number => {
if (isNaN(parsedSize)) { return fontSize - this.state.browserFontSize;
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 }) };
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
@ -123,6 +117,21 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
stretchContent stretchContent
data-testid="mx_FontScalingPanel" 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 <EventTilePreview
className="mx_FontScalingPanel_preview" className="mx_FontScalingPanel_preview"
message={this.MESSAGE_PREVIEW_TEXT} message={this.MESSAGE_PREVIEW_TEXT}
@ -131,49 +140,6 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
displayName={this.state.displayName} displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl} 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> </SettingsSubsection>
); );
} }

View file

@ -107,9 +107,11 @@ export enum Action {
MigrateBaseFontSize = "migrate_base_font_size", 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 * Sets a system font. Should be used with UpdateSystemFontPayload

View file

@ -17,11 +17,12 @@ limitations under the License.
import { ActionPayload } from "../payloads"; import { ActionPayload } from "../payloads";
import { Action } from "../actions"; import { Action } from "../actions";
export interface UpdateFontSizePayload extends ActionPayload { export interface UpdateFontSizeDeltaPayload extends ActionPayload {
action: Action.UpdateFontSize; 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_success": "Theme added!",
"custom_theme_url": "Custom theme URL", "custom_theme_url": "Custom theme URL",
"font_size": "Font size", "font_size": "Font size",
"font_size_limit": "Custom font size can only be between %(min)s pt and %(max)s pt", "font_size_default": "%(fontSize)s (default)",
"font_size_nan": "Size must be a number",
"font_size_valid": "Use between %(min)s pt and %(max)s pt",
"heading": "Customise your appearance", "heading": "Customise your appearance",
"image_size_default": "Default", "image_size_default": "Default",
"image_size_large": "Large", "image_size_large": "Large",

View file

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

View file

@ -511,6 +511,9 @@ export const SETTINGS: { [setting: string]: ISetting } = {
supportedLevels: [SettingLevel.CONFIG], supportedLevels: [SettingLevel.CONFIG],
default: 0, default: 0,
}, },
/**
* @deprecated in favor of {@link fontSizeDelta}
*/
"baseFontSize": { "baseFontSize": {
displayName: _td("settings|appearance|font_size"), displayName: _td("settings|appearance|font_size"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS, 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 * 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` * of 16px. We're taking the opportunity to move away from the `baseFontSize`
* setting that had a 5px offset. * setting that had a 5px offset.
* * @deprecated in favor {@link fontSizeDelta}
*/ */
"baseFontSizeV2": { "baseFontSizeV2": {
displayName: _td("settings|appearance|font_size"), displayName: _td("settings|appearance|font_size"),
supportedLevels: [SettingLevel.DEVICE], 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(), controller: new FontSizeController(),
}, },
"useCustomFontSize": { "useCustomFontSize": {

View file

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

View file

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

View file

@ -15,11 +15,10 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { fireEvent, render, waitFor } from "@testing-library/react"; import { render } from "@testing-library/react";
import * as TestUtils from "../../../test-utils"; import * as TestUtils from "../../../test-utils";
import FontScalingPanel from "../../../../src/components/views/settings/FontScalingPanel"; import FontScalingPanel from "../../../../src/components/views/settings/FontScalingPanel";
import SettingsStore from "../../../../src/settings/SettingsStore";
describe("FontScalingPanel", () => { describe("FontScalingPanel", () => {
it("renders the font scaling UI", () => { it("renders the font scaling UI", () => {
@ -27,19 +26,4 @@ describe("FontScalingPanel", () => {
const { asFragment } = render(<FontScalingPanel />); const { asFragment } = render(<FontScalingPanel />);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it("should clamp custom font size when disabling it", async () => {
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
TestUtils.stubClient();
const { container, getByText } = render(<FontScalingPanel />);
fireEvent.click(getByText("Use custom size"));
await waitFor(() => {
expect(container.querySelector("input[checked]")).toBeDefined();
});
fireEvent.change(container.querySelector("#font_size_field")!, { target: { value: "25" } });
fireEvent.click(getByText("Use custom size"));
await waitFor(() => {
expect(container.querySelector("#font_size_field")).toHaveValue(21);
});
});
}); });

View file

@ -18,6 +18,117 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch" class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
> >
<div
class="mx_Field mx_Field_select mx_FontScalingPanel_Dropdown"
>
<select
id="mx_Field_1"
label="Font size"
placeholder="Font size"
type="text"
>
<option
value="-7"
>
9
</option>
<option
value="-6"
>
10
</option>
<option
value="-5"
>
11
</option>
<option
value="-4"
>
12
</option>
<option
value="-3"
>
13
</option>
<option
value="-2"
>
14
</option>
<option
value="-1"
>
15
</option>
<option
value="0"
>
16 (default)
</option>
<option
value="1"
>
17
</option>
<option
value="2"
>
18
</option>
<option
value="4"
>
20
</option>
<option
value="6"
>
22
</option>
<option
value="8"
>
24
</option>
<option
value="10"
>
26
</option>
<option
value="12"
>
28
</option>
<option
value="14"
>
30
</option>
<option
value="16"
>
32
</option>
<option
value="18"
>
34
</option>
<option
value="20"
>
36
</option>
</select>
<label
for="mx_Field_1"
>
Font size
</label>
</div>
<div <div
class="mx_FontScalingPanel_preview mx_EventTilePreview_loader" class="mx_FontScalingPanel_preview mx_EventTilePreview_loader"
> >
@ -33,83 +144,6 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
/> />
</div> </div>
</div> </div>
<div
class="mx_FontScalingPanel_fontSlider"
>
<div
class="mx_FontScalingPanel_fontSlider_smallText"
>
Aa
</div>
<div
class="mx_Slider"
>
<input
aria-label="Font size"
autocomplete="off"
max="21"
min="11"
step="1"
type="range"
value="16"
/>
<output
class="mx_Slider_selection"
style="left: calc(2px + 50% + 1.2em - 1.2em);"
>
<span
class="mx_Slider_selection_label"
>
16
</span>
</output>
</div>
<div
class="mx_FontScalingPanel_fontSlider_largeText"
>
Aa
</div>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_QgU2PomxwK"
type="checkbox"
/>
<label
for="checkbox_QgU2PomxwK"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
Use custom size
</div>
</label>
</span>
<div
class="mx_Field mx_Field_input mx_AppearanceUserSettingsTab_checkboxControlledField"
>
<input
autocomplete="off"
disabled=""
id="font_size_field"
label="Font size"
placeholder="16"
type="number"
value="16"
/>
<label
for="font_size_field"
>
Font size
</label>
</div>
</div> </div>
</div> </div>
</DocumentFragment> </DocumentFragment>

View file

@ -247,6 +247,117 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch" class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
> >
<div
class="mx_Field mx_Field_select mx_FontScalingPanel_Dropdown"
>
<select
id="mx_Field_1"
label="Font size"
placeholder="Font size"
type="text"
>
<option
value="-7"
>
9
</option>
<option
value="-6"
>
10
</option>
<option
value="-5"
>
11
</option>
<option
value="-4"
>
12
</option>
<option
value="-3"
>
13
</option>
<option
value="-2"
>
14
</option>
<option
value="-1"
>
15
</option>
<option
value="0"
>
16 (default)
</option>
<option
value="1"
>
17
</option>
<option
value="2"
>
18
</option>
<option
value="4"
>
20
</option>
<option
value="6"
>
22
</option>
<option
value="8"
>
24
</option>
<option
value="10"
>
26
</option>
<option
value="12"
>
28
</option>
<option
value="14"
>
30
</option>
<option
value="16"
>
32
</option>
<option
value="18"
>
34
</option>
<option
value="20"
>
36
</option>
</select>
<label
for="mx_Field_1"
>
Font size
</label>
</div>
<div <div
class="mx_FontScalingPanel_preview mx_EventTilePreview_loader" class="mx_FontScalingPanel_preview mx_EventTilePreview_loader"
> >
@ -262,83 +373,6 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
/> />
</div> </div>
</div> </div>
<div
class="mx_FontScalingPanel_fontSlider"
>
<div
class="mx_FontScalingPanel_fontSlider_smallText"
>
Aa
</div>
<div
class="mx_Slider"
>
<input
aria-label="Font size"
autocomplete="off"
max="21"
min="11"
step="1"
type="range"
value="16"
/>
<output
class="mx_Slider_selection"
style="left: calc(2px + 50% + 1.2em - 1.2em);"
>
<span
class="mx_Slider_selection_label"
>
16
</span>
</output>
</div>
<div
class="mx_FontScalingPanel_fontSlider_largeText"
>
Aa
</div>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_QgU2PomxwK"
type="checkbox"
/>
<label
for="checkbox_QgU2PomxwK"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
Use custom size
</div>
</label>
</span>
<div
class="mx_Field mx_Field_input mx_AppearanceUserSettingsTab_checkboxControlledField"
>
<input
autocomplete="off"
disabled=""
id="font_size_field"
label="Font size"
placeholder="16"
type="number"
value="16"
/>
<label
for="font_size_field"
>
Font size
</label>
</div>
</div> </div>
</div> </div>
<div <div

View file

@ -710,8 +710,8 @@ describe("ElementCall", () => {
const originalGetValue = SettingsStore.getValue; const originalGetValue = SettingsStore.getValue;
SettingsStore.getValue = <T>(name: string, roomId?: string, excludeDefault?: boolean) => { SettingsStore.getValue = <T>(name: string, roomId?: string, excludeDefault?: boolean) => {
switch (name) { switch (name) {
case "baseFontSizeV2": case "fontSizeDelta":
return 12 as T; return 4 as T;
case "useSystemFont": case "useSystemFont":
return true as T; return true as T;
case "systemFont": case "systemFont":
@ -720,13 +720,14 @@ describe("ElementCall", () => {
return originalGetValue<T>(name, roomId, excludeDefault); return originalGetValue<T>(name, roomId, excludeDefault);
} }
}; };
document.documentElement.style.fontSize = "12px";
await ElementCall.create(room); await ElementCall.create(room);
const call = Call.get(room); const call = Call.get(room);
if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
expect(urlParams.get("fontScale")).toBe("0.75"); expect(urlParams.get("fontScale")).toBe("1.5");
expect(urlParams.getAll("font")).toEqual(["OpenDyslexic", "DejaVu Sans"]); expect(urlParams.getAll("font")).toEqual(["OpenDyslexic", "DejaVu Sans"]);
SettingsStore.getValue = originalGetValue; SettingsStore.getValue = originalGetValue;

View file

@ -123,6 +123,7 @@ describe("FontWatcher", function () {
let watcher: FontWatcher | undefined; let watcher: FontWatcher | undefined;
beforeEach(() => { beforeEach(() => {
document.documentElement.style.fontSize = "14px";
watcher = new FontWatcher(); watcher = new FontWatcher();
}); });
@ -132,13 +133,35 @@ describe("FontWatcher", function () {
it("should not run the migration", async () => { it("should not run the migration", async () => {
await watcher!.start(); await watcher!.start();
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(16); expect(SettingsStore.getValue("fontSizeDelta")).toBe(0);
}); });
it("should migrate to default font size", async () => { it("should migrate from V1 font size to V3", async () => {
await SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, 13); await SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, 13);
await watcher!.start(); await watcher!.start();
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(19); // 13px (V1 font size) + 5px (V1 offset) + 1px (root font size increase) - 14px (default browser font size) = 5px
expect(SettingsStore.getValue("fontSizeDelta")).toBe(5);
// baseFontSize should be cleared
expect(SettingsStore.getValue("baseFontSize")).toBe(0);
});
it("should migrate from V2 font size to V3 using browser font size", async () => {
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 18);
await watcher!.start();
// 18px - 14px (default browser font size) = 2px
expect(SettingsStore.getValue("fontSizeDelta")).toBe(4);
// baseFontSize should be cleared
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(0);
});
it("should migrate from V2 font size to V3 using fallback font size", async () => {
document.documentElement.style.fontSize = "";
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 18);
await watcher!.start();
// 18px - 16px (fallback) = 2px
expect(SettingsStore.getValue("fontSizeDelta")).toBe(2);
// baseFontSize should be cleared
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(0);
}); });
}); });
}); });