[#646] improve dark color management (#648)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-03-17 16:26:37 +01:00
committed by GitHub
parent a771b311b5
commit e665ea4a79
12 changed files with 49 additions and 101 deletions
@@ -10,6 +10,13 @@ jest.mock("@/features/Calendars/services/helpers");
jest.mock("@/features/Calendars/CalendarApi");
jest.mock("@/utils/errorUtils");
jest.mock("@/features/Calendars/utils/normalizeCalendar");
jest.mock("@/utils/getAccessiblePair", () => ({
getAccessiblePair: jest.fn().mockReturnValue("#FFF"),
}));
jest.mock("@mui/material/styles", () => ({
createTheme: jest.fn().mockReturnValue({}),
}));
const mockedGetOpenPaasUser = getOpenPaasUser as jest.Mock;
const mockedFetchOwnerData = fetchOwnerData as jest.Mock;
+1 -1
View File
@@ -1,4 +1,4 @@
import { getAccessiblePair } from "@/components/Calendar/utils/calendarColorsUtils";
import { getAccessiblePair } from "@/utils/getAccessiblePair";
import { stringAvatar } from "@/components/Event/utils/eventUtils";
import { useUserSearch } from "./useUserSearch";
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
+1 -1
View File
@@ -1,4 +1,4 @@
import { getAccessiblePair } from "@/components/Calendar/utils/calendarColorsUtils";
import { getAccessiblePair } from "@/utils/getAccessiblePair";
import { useUserSearch } from "./useUserSearch";
import { ResourceIcon } from "./ResourceIcon";
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
+1 -26
View File
@@ -20,7 +20,7 @@ import interactionPlugin from "@fullcalendar/interaction";
import momentTimezonePlugin from "@fullcalendar/moment-timezone";
import FullCalendar from "@fullcalendar/react";
import timeGridPlugin from "@fullcalendar/timegrid";
import { Box, Button, radius, useTheme } from "@linagora/twake-mui";
import { Box, Button, radius } from "@linagora/twake-mui";
import AddIcon from "@mui/icons-material/Add";
import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
import { useI18n } from "twake-i18n";
@@ -38,7 +38,6 @@ import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
import { MiniCalendar } from "./MiniCalendar";
import { TempCalendarsInput } from "./TempCalendarsInput";
import { TimezoneSelector } from "./TimezoneSelector";
import { updateDarkColor } from "./utils/calendarColorsUtils";
import {
eventToFullCalendarFormat,
extractEvents,
@@ -75,7 +74,6 @@ export default function CalendarApp({
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const dispatch = useAppDispatch();
const theme = useTheme();
const view = useAppSelector((state) => state.settings.view);
const userData = useAppSelector((state) => state.user.userData);
const workingDays = useAppSelector(
@@ -103,13 +101,6 @@ export default function CalendarApp({
const [selectedCalendars, setSelectedCalendars] =
useState<string[]>(storedCalendars);
const calendarLightSignature = useMemo(() => {
return Object.values(calendars || {})
.map((cal) => `${cal.id}:${cal.color?.light ?? ""}`)
.sort()
.join("|");
}, [calendars]);
const calendarIdsString = useMemo(
() =>
Object.keys(calendars || {})
@@ -167,22 +158,6 @@ export default function CalendarApp({
}
}, [selectedCalendars, calendarIds.length]);
const prevCalendarLightSignature = useRef<string | null>(null);
useEffect(() => {
if (!calendarLightSignature) {
prevCalendarLightSignature.current = calendarLightSignature;
return;
}
if (prevCalendarLightSignature.current === calendarLightSignature) {
return;
}
prevCalendarLightSignature.current = calendarLightSignature;
updateDarkColor(calendars || {}, theme, dispatch);
}, [calendarLightSignature, calendars, theme, dispatch]);
useEffect(() => {
if (calendarIds.length === 0) return;
const validCalendarIds = new Set(calendarIds);
@@ -11,7 +11,7 @@ import {
import { useState, useEffect } from "react";
import { HexColorPicker } from "react-colorful";
import { useI18n } from "twake-i18n";
import { getAccessiblePair } from "./utils/calendarColorsUtils";
import { getAccessiblePair } from "@/utils/getAccessiblePair";
import { defaultColors } from "@/utils/defaultColors";
export function ColorPicker({
@@ -16,7 +16,7 @@ import { useRef, useState } from "react";
import { useI18n } from "twake-i18n";
import { ResponsiveDialog } from "../Dialog";
import { ColorPicker } from "./CalendarColorPicker";
import { getAccessiblePair } from "./utils/calendarColorsUtils";
import { getAccessiblePair } from "@/utils/getAccessiblePair";
import { defaultColors } from "@/utils/defaultColors";
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
import { Resource, ResourceSearch } from "../Attendees/ResourceSearch";
+1 -1
View File
@@ -20,7 +20,7 @@ import { PeopleSearch, User } from "../Attendees/PeopleSearch";
import { ResponsiveDialog } from "../Dialog";
import { stringAvatar } from "../Event/utils/eventUtils";
import { ColorPicker } from "./CalendarColorPicker";
import { getAccessiblePair } from "./utils/calendarColorsUtils";
import { getAccessiblePair } from "@/utils/getAccessiblePair";
import { defaultColors } from "@/utils/defaultColors";
interface CalendarWithOwner {
@@ -7,7 +7,7 @@ import { TextField, useTheme } from "@linagora/twake-mui";
import { useRef } from "react";
import { useI18n } from "twake-i18n";
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
import { getAccessiblePair } from "./utils/calendarColorsUtils";
import { getAccessiblePair } from "@/utils/getAccessiblePair";
const requestControllers = new Map<string, AbortController>();
@@ -1,56 +0,0 @@
import { AppDispatch } from "@/app/store";
import { updateCalColor } from "@/features/Calendars/CalendarSlice";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { darken, getContrastRatio, lighten, Theme } from "@linagora/twake-mui";
import { defaultColors } from "@/utils/defaultColors";
export function updateDarkColor(
calendars: Record<string, Calendar>,
theme: Theme,
dispatch: AppDispatch
) {
Object.values(calendars).forEach((cal) => {
if (!cal?.color?.light || typeof cal.color.light !== "string") return;
const baseColor = cal.color.light;
const isDefault = Object.values(defaultColors).find(
(c) => c.light === baseColor
);
const darkColor = isDefault
? isDefault.dark
: getAccessiblePair(baseColor, theme);
if (cal.color?.dark === darkColor) {
return;
}
dispatch(
updateCalColor({
id: cal.id,
color: { light: baseColor, dark: darkColor },
})
);
});
}
export function getAccessiblePair(baseColor: string, theme: Theme): string {
if (typeof baseColor !== "string") {
return theme.palette.getContrastText("#000");
}
const contrastToBlack = getContrastRatio(baseColor, "#000");
const contrastToWhite = getContrastRatio(baseColor, "#fff");
const isLight = contrastToBlack > contrastToWhite;
const adjusted = isLight ? darken(baseColor, 0.6) : lighten(baseColor, 0.7);
// Check if contrast meets 4.5
const contrast = getContrastRatio(baseColor, adjusted);
if (contrast >= 4.5) return adjusted;
if (isLight) {
return "#ffffffff";
}
return theme.palette.getContrastText(baseColor);
}
@@ -8,7 +8,6 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../types/RejectedError";
import { extractCalendarEvents } from "../utils/extractCalendarEvents";
import { defaultColors } from "@/utils/defaultColors";
import { type RootState } from "@/app/store";
export const getCalendarDetailAsync = createAsyncThunk<
@@ -42,16 +41,15 @@ export const getCalendarDetailAsync = createAsyncThunk<
match,
signal
)) as CalendarData;
const color = calendar["apple:color"]
? { light: calendar["apple:color"], dark: calendar["apple:color"] }
: defaultColors[0];
const syncToken = calendar._embedded?.["sync-token"];
const items = calendar._embedded?.["dav:item"];
const events: CalendarEvent[] = Array.isArray(items)
? items.flatMap((item: CalendarItem) =>
extractCalendarEvents(item, { cal: calendarStored, color })
extractCalendarEvents(item, {
cal: calendarStored,
color: calendarStored.color,
})
)
: [];
@@ -10,6 +10,10 @@ import { CalendarData } from "../types/CalendarData";
import { RejectedError } from "../types/RejectedError";
import { normalizeCalendar } from "../utils/normalizeCalendar";
import { fetchOwnerData } from "./helpers";
import { createTheme } from "@mui/material/styles";
import { getAccessiblePair } from "@/utils/getAccessiblePair";
const theme = createTheme();
export const getCalendarsListAsync = createAsyncThunk<
{ importedCalendars: Record<string, Calendar>; errors: string },
@@ -77,10 +81,11 @@ export const getCalendarsListAsync = createAsyncThunk<
emails: [],
};
const color = cal["apple:color"]
const rawColor = cal["apple:color"];
const color = rawColor
? {
light: cal["apple:color"],
dark: "#fff",
light: rawColor,
dark: getAccessiblePair(rawColor, theme),
}
: defaultColors[0];
@@ -126,10 +131,6 @@ export const getCalendarsListAsync = createAsyncThunk<
if (fetchedCal) {
importedCalendars[id] = {
...fetchedCal,
color: {
...existingCal.color,
light: fetchedCal.color?.light,
},
events: existingCal.events || {},
};
}
+23
View File
@@ -0,0 +1,23 @@
import { darken, getContrastRatio, lighten, Theme } from "@linagora/twake-mui";
export function getAccessiblePair(baseColor: string, theme: Theme): string {
if (typeof baseColor !== "string") {
return theme.palette.getContrastText("#000");
}
const contrastToBlack = getContrastRatio(baseColor, "#000");
const contrastToWhite = getContrastRatio(baseColor, "#fff");
const isLight = contrastToBlack > contrastToWhite;
const adjusted = isLight ? darken(baseColor, 0.6) : lighten(baseColor, 0.7);
// Check if contrast meets 4.5
const contrast = getContrastRatio(baseColor, adjusted);
if (contrast >= 4.5) return adjusted;
if (isLight) {
return "#ffffffff";
}
return theme.palette.getContrastText(baseColor);
}