fix: let accessibility reset bring back the organisation's defaults and cover every option

This commit is contained in:
Linus Rath
2026-10-31 17:57:20 +01:00
parent 48c8d37e37
commit 6a1da49f52
3 changed files with 56 additions and 7 deletions
@@ -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(<AccessibilitySettings />);
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(<AccessibilitySettings />);
fireEvent.click(screen.getByRole('button', { name: 'settings.accessibility.reset.button' }));
expect(settings().plainTextFont).toBe('sans');
expect(settings().sendConfirmation).toBe(false);
});
});
describe('toast duration', () => {
@@ -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<ReturnType<typeof useSettingsStore.getState>['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);
};
+26
View File
@@ -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<SettingsState>()(
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<string, unknown>;
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<SettingsState>);
applyAdminPolicyPatch();
applyVisualSettings();
const after = get() as unknown as Record<string, unknown>;
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) {