diff --git a/components/settings/__tests__/accessibility-settings.test.tsx b/components/settings/__tests__/accessibility-settings.test.tsx
index d8a73d641..cc876edea 100644
--- a/components/settings/__tests__/accessibility-settings.test.tsx
+++ b/components/settings/__tests__/accessibility-settings.test.tsx
@@ -99,6 +99,29 @@ describe('reset', () => {
// A shared everyday preference is not an accessibility setting to reset.
expect(settings().disableThreading).toBe(true);
});
+
+ it("gives back the organisation's defaults instead of writing Bulwark's as the user's choice", () => {
+ usePolicyStore.setState({ policy: { ...DEFAULT_POLICY, defaults: { appFont: 'atkinson', fontSize: 'large' } }, loaded: true });
+ settings().applyAdminPolicy();
+ settings().updateSetting('appFont', 'lexend');
+ settings().updateSetting('fontSize', 'huge');
+ render();
+ fireEvent.click(screen.getByRole('button', { name: 'settings.accessibility.reset.button' }));
+ expect(settings().appFont).toBe('atkinson');
+ expect(settings().fontSize).toBe('large');
+ // Nothing is left marked as chosen, so a later change of the default reaches the user.
+ expect(settings().explicitSettings).not.toContain('appFont');
+ expect(settings().explicitSettings).not.toContain('fontSize');
+ });
+
+ it('also resets the plain text font and the send confirmation', () => {
+ settings().updateSetting('plainTextFont', 'mono');
+ settings().updateSetting('sendConfirmation', true);
+ render();
+ fireEvent.click(screen.getByRole('button', { name: 'settings.accessibility.reset.button' }));
+ expect(settings().plainTextFont).toBe('sans');
+ expect(settings().sendConfirmation).toBe(false);
+ });
});
describe('toast duration', () => {
diff --git a/components/settings/accessibility-settings.tsx b/components/settings/accessibility-settings.tsx
index 371f96ac8..3068655e0 100644
--- a/components/settings/accessibility-settings.tsx
+++ b/components/settings/accessibility-settings.tsx
@@ -14,7 +14,6 @@ import { voicesForLocale } from '@/lib/read-aloud';
import { useSpeechVoices } from '@/hooks/use-read-aloud';
import { NOTIFICATION_SOUNDS, playNotificationSound, type NotificationSoundChoice } from '@/lib/notification-sound';
import {
- ACCESSIBILITY_DEFAULTS,
ACCESSIBILITY_KEYS,
FONT_SIZES,
MAIL_MIN_FONT_SIZES,
@@ -37,6 +36,9 @@ import type { Density, SendDelaySeconds } from '@/stores/settings-store';
type SettingKey = Parameters['updateSetting']>[0];
+/** What Reset gives back: the settings only this page has. */
+const RESET_KEYS: SettingKey[] = [...ACCESSIBILITY_KEYS, 'fontSize', 'plainTextFont', 'sendConfirmation'];
+
/** Opens another settings page (and section) the way deep links do. */
function goToSettings(tab: string, section?: string) {
window.dispatchEvent(new CustomEvent('settings-tab-change', { detail: section ? { tab, section } : tab }));
@@ -64,7 +66,7 @@ export function AccessibilitySettings() {
const tNotifications = useTranslations('settings.notifications');
const tCalendar = useTranslations('calendar.settings');
const settings = useSettingsStore();
- const { updateSetting } = settings;
+ const { updateSetting, resetSettings } = settings;
const { isSettingLocked, isSettingHidden, getRestriction } = usePolicyStore();
const faviconsAllowed = !useNoThirdPartyTraffic();
const [applied, setApplied] = useState<{ id: AccessibilityPresetId; changes: PresetChange[] } | null>(null);
@@ -91,12 +93,10 @@ export function AccessibilitySettings() {
setApplied(null);
};
+ // Forgets the user's own values, so the organisation's defaults (or
+ // Bulwark's) apply again. Shared everyday settings shown here too stay.
const resetAll = () => {
- for (const key of ACCESSIBILITY_KEYS) {
- if (isSettingLocked(key)) continue;
- set(key, ACCESSIBILITY_DEFAULTS[key]);
- }
- set('fontSize', 'medium');
+ resetSettings(RESET_KEYS.filter((key) => !isSettingLocked(key)));
setApplied(null);
setResetDone(true);
};
diff --git a/stores/settings-store.ts b/stores/settings-store.ts
index 082ce5896..584864ea1 100644
--- a/stores/settings-store.ts
+++ b/stores/settings-store.ts
@@ -804,6 +804,12 @@ interface SettingsState extends AccessibilitySettings {
opts?: { internal?: boolean }
) => void;
resetToDefaults: () => void;
+ /**
+ * Forgets the user's own value for each of `keys`: the setting loses its
+ * "chosen by the user" mark and takes the admin default (policy.defaults)
+ * again, or the built-in one when the admin set none.
+ */
+ resetSettings: (keys: readonly string[]) => void;
/**
* Applies the admin policy (#1248): `policy.defaults` to every setting the
* user has not chosen, and a locked restriction's value to its setting
@@ -1495,6 +1501,26 @@ export const useSettingsStore = create()(
void settingsHooks.onSettingsReset.emit({});
},
+ resetSettings: (keys) => {
+ const own = keys.filter((key) => hasOwn(DEFAULT_SETTINGS, key) && key !== 'explicitSettings' && !BOOKKEEPING_SETTING_KEYS.has(key));
+ if (own.length === 0) return;
+ const before = get() as unknown as Record;
+ const previous = Object.fromEntries(own.map((key) => [key, before[key]]));
+ const dropped = new Set(own);
+ set({
+ ...Object.fromEntries(own.map((key) => [key, builtInDefault(key)])),
+ explicitSettings: get().explicitSettings.filter((key) => !dropped.has(key)),
+ } as Partial);
+ applyAdminPolicyPatch();
+ applyVisualSettings();
+ const after = get() as unknown as Record;
+ for (const key of own) {
+ if (after[key] !== previous[key]) {
+ void settingsHooks.onSettingChange.emit({ key, value: after[key], previous: previous[key] });
+ }
+ }
+ },
+
exportSettings: (opts) => {
const json = buildExport(opts);
if (!opts?.forSync) {