refactor: Remove neutral contrast theme adjustment and improve floating menu event handling by delaying listeners and checking for unmounted elements.

This commit is contained in:
2025-11-25 14:12:53 +01:00
parent 30602e6b66
commit 2c42e63bab
4 changed files with 27 additions and 95 deletions
+1 -35
View File
@@ -308,9 +308,6 @@ const Sidebar: React.FC<SidebarProps> = ({
neutralChroma, neutralChroma,
setNeutralChroma, setNeutralChroma,
resetNeutralChroma, resetNeutralChroma,
neutralContrast,
setNeutralContrast,
resetNeutralContrast,
themeMode, themeMode,
cycleThemeMode, cycleThemeMode,
themeModes, themeModes,
@@ -343,7 +340,6 @@ const Sidebar: React.FC<SidebarProps> = ({
}, [collapseSidebar]); }, [collapseSidebar]);
const neutralHueInputId = useId(); const neutralHueInputId = useId();
const neutralChromaInputId = useId(); const neutralChromaInputId = useId();
const neutralContrastInputId = useId();
const sortedCorrespondents = useMemo<CorrespondentEntry[]>(() => { const sortedCorrespondents = useMemo<CorrespondentEntry[]>(() => {
if (!Array.isArray(correspondents)) { if (!Array.isArray(correspondents)) {
return []; return [];
@@ -446,8 +442,7 @@ const Sidebar: React.FC<SidebarProps> = ({
const handleThemeAdjustmentsReset = useCallback(() => { const handleThemeAdjustmentsReset = useCallback(() => {
resetNeutralHue(); resetNeutralHue();
resetNeutralChroma(); resetNeutralChroma();
resetNeutralContrast(); }, [resetNeutralHue, resetNeutralChroma]);
}, [resetNeutralHue, resetNeutralChroma, resetNeutralContrast]);
const handleNeutralHueChange = useCallback( const handleNeutralHueChange = useCallback(
(value: string) => { (value: string) => {
@@ -479,21 +474,6 @@ const Sidebar: React.FC<SidebarProps> = ({
[resetNeutralChroma, setNeutralChroma], [resetNeutralChroma, setNeutralChroma],
); );
const handleNeutralContrastChange = useCallback(
(value: string) => {
if (value === '') {
resetNeutralContrast();
return;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return;
}
setNeutralContrast(parsed);
},
[resetNeutralContrast, setNeutralContrast],
);
const formatSliderValue = useCallback((value: number | string) => { const formatSliderValue = useCallback((value: number | string) => {
const parsed = Number(value); const parsed = Number(value);
if (Number.isNaN(parsed)) { if (Number.isNaN(parsed)) {
@@ -574,20 +554,6 @@ const Sidebar: React.FC<SidebarProps> = ({
/> />
<span className="menu__slider-value">{formatSliderValue(neutralChroma)}</span> <span className="menu__slider-value">{formatSliderValue(neutralChroma)}</span>
</label> </label>
<label className="menu__slider" htmlFor={neutralContrastInputId}>
<span className="menu__slider-label">Contrast</span>
<input
id={neutralContrastInputId}
type="range"
min="0"
max="1"
step="0.01"
value={neutralContrast}
onChange={(event) => handleNeutralContrastChange(event.target.value)}
onDoubleClick={resetNeutralContrast}
/>
<span className="menu__slider-value">{formatSliderValue(neutralContrast)}</span>
</label>
</div> </div>
) )
: null; : null;
+2 -44
View File
@@ -9,7 +9,6 @@ import React, {
import { import {
DARK_MODE_MEDIA_QUERY, DARK_MODE_MEDIA_QUERY,
DEFAULT_NEUTRAL_CHROMA, DEFAULT_NEUTRAL_CHROMA,
DEFAULT_NEUTRAL_CONTRAST,
DEFAULT_NEUTRAL_HUE, DEFAULT_NEUTRAL_HUE,
DEFAULT_THEME_MODE, DEFAULT_THEME_MODE,
SIDEBAR_COLLAPSE_STORAGE_KEY, SIDEBAR_COLLAPSE_STORAGE_KEY,
@@ -26,9 +25,6 @@ interface SidebarContextValue {
neutralChroma: number; neutralChroma: number;
setNeutralChroma: (value: number | string) => void; setNeutralChroma: (value: number | string) => void;
resetNeutralChroma: () => void; resetNeutralChroma: () => void;
neutralContrast: number;
setNeutralContrast: (value: number | string) => void;
resetNeutralContrast: () => void;
themeMode: ThemeMode; themeMode: ThemeMode;
setThemeMode: (mode: ThemeMode) => void; setThemeMode: (mode: ThemeMode) => void;
cycleThemeMode: () => void; cycleThemeMode: () => void;
@@ -43,7 +39,6 @@ const loadInitialThemeSettings = () => {
const defaults = { const defaults = {
neutralHue: DEFAULT_NEUTRAL_HUE, neutralHue: DEFAULT_NEUTRAL_HUE,
neutralChroma: DEFAULT_NEUTRAL_CHROMA, neutralChroma: DEFAULT_NEUTRAL_CHROMA,
neutralContrast: DEFAULT_NEUTRAL_CONTRAST,
mode: DEFAULT_THEME_MODE, mode: DEFAULT_THEME_MODE,
}; };
@@ -68,14 +63,12 @@ const loadInitialThemeSettings = () => {
return { return {
neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed, neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed,
neutralChroma: readNumberVar('--neutral-chroma', defaults.neutralChroma), neutralChroma: readNumberVar('--neutral-chroma', defaults.neutralChroma),
neutralContrast: readNumberVar('--neutral-contrast', defaults.neutralContrast),
}; };
}; };
const rootValues = loadFromRoot(); const rootValues = loadFromRoot();
let neutralHueValue = rootValues.neutralHue; let neutralHueValue = rootValues.neutralHue;
let neutralChromaValue = rootValues.neutralChroma; let neutralChromaValue = rootValues.neutralChroma;
let neutralContrastValue = rootValues.neutralContrast;
let modeValue = defaults.mode; let modeValue = defaults.mode;
const composite = window.localStorage.getItem(THEME_STORAGE_KEY); const composite = window.localStorage.getItem(THEME_STORAGE_KEY);
@@ -90,10 +83,7 @@ const loadInitialThemeSettings = () => {
if (!Number.isNaN(storedChroma)) { if (!Number.isNaN(storedChroma)) {
neutralChromaValue = Math.min(Math.max(storedChroma, 0), 1); neutralChromaValue = Math.min(Math.max(storedChroma, 0), 1);
} }
const storedContrast = Number.parseFloat(parsed?.neutralContrast);
if (!Number.isNaN(storedContrast)) {
neutralContrastValue = Math.min(Math.max(storedContrast, 0), 1);
}
const storedMode = parsed?.mode; const storedMode = parsed?.mode;
if (THEME_MODES.includes(storedMode)) { if (THEME_MODES.includes(storedMode)) {
modeValue = storedMode; modeValue = storedMode;
@@ -118,7 +108,6 @@ const loadInitialThemeSettings = () => {
return { return {
neutralHue: neutralHueValue, neutralHue: neutralHueValue,
neutralChroma: neutralChromaValue, neutralChroma: neutralChromaValue,
neutralContrast: neutralContrastValue,
mode: modeValue, mode: modeValue,
}; };
}; };
@@ -143,7 +132,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
const initialTheme = useMemo(() => loadInitialThemeSettings(), []); const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue); const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
const [neutralChroma, setNeutralChromaState] = useState(initialTheme.neutralChroma); const [neutralChroma, setNeutralChromaState] = useState(initialTheme.neutralChroma);
const [neutralContrast, setNeutralContrastState] = useState(initialTheme.neutralContrast);
const [themeMode, setThemeModeState] = useState(initialTheme.mode); const [themeMode, setThemeModeState] = useState(initialTheme.mode);
const [systemPrefersDark, setSystemPrefersDark] = useState(() => { const [systemPrefersDark, setSystemPrefersDark] = useState(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
@@ -160,10 +148,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
document.documentElement.style.setProperty('--neutral-chroma', String(neutralChroma)); document.documentElement.style.setProperty('--neutral-chroma', String(neutralChroma));
}, [neutralChroma]); }, [neutralChroma]);
useEffect(() => {
document.documentElement.style.setProperty('--neutral-contrast', String(neutralContrast));
}, [neutralContrast]);
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return () => undefined; return () => undefined;
@@ -198,7 +182,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
const payload = JSON.stringify({ const payload = JSON.stringify({
neutralHue, neutralHue,
neutralChroma, neutralChroma,
neutralContrast,
mode: themeMode, mode: themeMode,
}); });
window.localStorage.setItem(THEME_STORAGE_KEY, payload); window.localStorage.setItem(THEME_STORAGE_KEY, payload);
@@ -207,7 +190,7 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
} catch (error) { } catch (error) {
console.warn('[theme] failed to persist theme settings', error); console.warn('[theme] failed to persist theme settings', error);
} }
}, [neutralHue, neutralChroma, neutralContrast, themeMode]); }, [neutralHue, neutralChroma, themeMode]);
const setNeutralHue = useCallback((value) => { const setNeutralHue = useCallback((value) => {
setNeutralHueState((prev) => { setNeutralHueState((prev) => {
@@ -241,24 +224,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
setNeutralChromaState(DEFAULT_NEUTRAL_CHROMA); setNeutralChromaState(DEFAULT_NEUTRAL_CHROMA);
}, []); }, []);
const setNeutralContrast = useCallback((value) => {
setNeutralContrastState((prev) => {
if (value === '' || value === null || value === undefined) {
return DEFAULT_NEUTRAL_CONTRAST;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return prev;
}
const clamped = Math.min(Math.max(parsed, 0), 1);
return Math.round(clamped * 1000) / 1000;
});
}, []);
const resetNeutralContrast = useCallback(() => {
setNeutralContrastState(DEFAULT_NEUTRAL_CONTRAST);
}, []);
const resetNeutralHue = useCallback(() => { const resetNeutralHue = useCallback(() => {
setNeutralHueState(DEFAULT_NEUTRAL_HUE); setNeutralHueState(DEFAULT_NEUTRAL_HUE);
}, []); }, []);
@@ -296,18 +261,14 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
setNeutralHue, setNeutralHue,
neutralChroma, neutralChroma,
setNeutralChroma, setNeutralChroma,
neutralContrast,
setNeutralContrast,
resetNeutralHue, resetNeutralHue,
resetNeutralChroma, resetNeutralChroma,
resetNeutralContrast,
themeMode, themeMode,
setThemeMode, setThemeMode,
cycleThemeMode, cycleThemeMode,
themeModes: THEME_MODES, themeModes: THEME_MODES,
defaultNeutralHue: DEFAULT_NEUTRAL_HUE, defaultNeutralHue: DEFAULT_NEUTRAL_HUE,
defaultNeutralChroma: DEFAULT_NEUTRAL_CHROMA, defaultNeutralChroma: DEFAULT_NEUTRAL_CHROMA,
defaultNeutralContrast: DEFAULT_NEUTRAL_CONTRAST,
}), }),
[ [
collapsed, collapsed,
@@ -316,11 +277,8 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
setNeutralHue, setNeutralHue,
neutralChroma, neutralChroma,
setNeutralChroma, setNeutralChroma,
neutralContrast,
setNeutralContrast,
resetNeutralHue, resetNeutralHue,
resetNeutralChroma, resetNeutralChroma,
resetNeutralContrast,
themeMode, themeMode,
setThemeMode, setThemeMode,
cycleThemeMode, cycleThemeMode,
+2 -8
View File
@@ -11,14 +11,8 @@
--neutral-chroma-min: 0.01; --neutral-chroma-min: 0.01;
--neutral-chroma-max: 0.20; --neutral-chroma-max: 0.20;
--neutral-chroma-range: calc(var(--neutral-chroma-max) - var(--neutral-chroma-min)); --neutral-chroma-range: calc(var(--neutral-chroma-max) - var(--neutral-chroma-min));
--neutral-chroma-effective: calc( --neutral-chroma-effective: calc(var(--neutral-chroma-min) + clamp(0, var(--neutral-chroma), 1) * var(--neutral-chroma-range));
var(--neutral-chroma-min) --neutral-contrast-delta: 0.033;
+ clamp(0, var(--neutral-chroma), 1) * var(--neutral-chroma-range)
);
--neutral-contrast: 0.56;
--neutral-contrast-amount: calc(0.85 + var(--neutral-contrast) * 0.55);
--neutral-contrast-baseline: 1.125;
--neutral-contrast-delta: calc(var(--neutral-contrast-amount) - var(--neutral-contrast-baseline));
--neutral-chroma-scale: calc(clamp(0.0001, var(--neutral-chroma-effective), 1) / 0.1); --neutral-chroma-scale: calc(clamp(0.0001, var(--neutral-chroma-effective), 1) / 0.1);
--tag-chip-default-l: 0.9; --tag-chip-default-l: 0.9;
--tag-chip-bg-lighten: 0.4; --tag-chip-bg-lighten: 0.4;
+15 -1
View File
@@ -231,9 +231,18 @@ const useFloatingMenu = ({
if (event.type === 'focusin' && ignoreFocusEvents) { if (event.type === 'focusin' && ignoreFocusEvents) {
return; return;
} }
const target = event.target;
// If the target is no longer in the document, it means it was unmounted
// (e.g. due to a re-render caused by the open action).
// In this case, we should ignore the event.
if (target instanceof Node && !document.contains(target)) {
return;
}
const menu = menuRef.current; const menu = menuRef.current;
const anchor = anchorRef?.current; const anchor = anchorRef?.current;
if ((anchor && anchor.contains(event.target)) || (menu && menu.contains(event.target))) {
if ((anchor && anchor.contains(target)) || (menu && menu.contains(target))) {
return; return;
} }
close(); close();
@@ -245,11 +254,16 @@ const useFloatingMenu = ({
} }
}; };
// Delay adding listeners to avoid capturing the event that opened the menu
const timer = setTimeout(() => {
document.addEventListener('mousedown', handlePointer); document.addEventListener('mousedown', handlePointer);
document.addEventListener('touchstart', handlePointer, { passive: true }); document.addEventListener('touchstart', handlePointer, { passive: true });
document.addEventListener('focusin', handlePointer); document.addEventListener('focusin', handlePointer);
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
}, 0);
return () => { return () => {
clearTimeout(timer);
if (rafId != null) { if (rafId != null) {
window.cancelAnimationFrame(rafId); window.cancelAnimationFrame(rafId);
} }