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:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -218,8 +218,8 @@ const useFloatingMenu = ({
|
|||||||
const raf = window.requestAnimationFrame;
|
const raf = window.requestAnimationFrame;
|
||||||
const rafId = raf
|
const rafId = raf
|
||||||
? raf(() => {
|
? raf(() => {
|
||||||
ignoreFocusEvents = false;
|
ignoreFocusEvents = false;
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!anchorRef?.current) {
|
if (!anchorRef?.current) {
|
||||||
@@ -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 = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handlePointer);
|
// Delay adding listeners to avoid capturing the event that opened the menu
|
||||||
document.addEventListener('touchstart', handlePointer, { passive: true });
|
const timer = setTimeout(() => {
|
||||||
document.addEventListener('focusin', handlePointer);
|
document.addEventListener('mousedown', handlePointer);
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('touchstart', handlePointer, { passive: true });
|
||||||
|
document.addEventListener('focusin', handlePointer);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
if (rafId != null) {
|
if (rafId != null) {
|
||||||
window.cancelAnimationFrame(rafId);
|
window.cancelAnimationFrame(rafId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user