Update eslint (non-major) (#39797)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
renovate[bot]
2026-08-12 18:02:18 +02:00
committed by GitHub
parent 2e93afed98
commit 3cc36b98c0
14 changed files with 819 additions and 278 deletions

View File

@@ -1,4 +1,4 @@
import type { FC } from 'react';
import type { CSSProperties, FC } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn, userEvent, expect } from 'storybook/test';
@@ -10,16 +10,19 @@ interface TestSlideProps {
id: number;
text: string;
color: string;
style?: CSSProperties;
}
const TestSlide: FC<TestSlideProps & { active: boolean }> = ({
active,
text,
color,
style,
}) => (
<div
className='test-slide'
style={{
...style,
backgroundColor: active ? color : undefined,
}}
>
@@ -114,7 +117,7 @@ export const DifferentHeights: Story = {
args: {
items: slides.map((props, index) => ({
...props,
styles: { height: 100 + index * 100 },
style: { height: 100 + index * 100 },
})),
},
};

View File

@@ -1,4 +1,4 @@
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type {
ComponentPropsWithoutRef,
ComponentType,
@@ -75,10 +75,13 @@ export const Carousel = <
}: CarouselProps<SlideProps> & ComponentPropsWithoutRef<'div'>) => {
// Handle slide change
const [slideIndex, setSlideIndex] = useState(0);
const wrapperRef = useRef<HTMLDivElement>(null);
const [wrapperElement, setWrapperElement] = useState<HTMLDivElement | null>(
null,
);
// Handle slide heights
const [currentSlideHeight, setCurrentSlideHeight] = useState(
() => wrapperRef.current?.scrollHeight ?? 0,
const [currentSlideHeight, setCurrentSlideHeight] = useState<number | null>(
null,
);
const previousSlideHeight = usePrevious(currentSlideHeight);
const handleSlideChange = useCallback(
@@ -92,7 +95,7 @@ export const Carousel = <
newIndex = 0;
}
const slide = wrapperRef.current?.children[newIndex];
const slide = wrapperElement?.children[newIndex];
if (slide) {
setCurrentSlideHeight(slide.scrollHeight);
if (slide instanceof HTMLElement) {
@@ -103,26 +106,27 @@ export const Carousel = <
return newIndex;
});
},
[items.length, onChangeSlide],
[wrapperElement, items.length, onChangeSlide],
);
const observerRef = useRef<ResizeObserver>(null);
observerRef.current ??= new ResizeObserver(() => {
// Update slide height when the component mounts
if (wrapperElement && currentSlideHeight === null) {
handleSlideChange(0);
});
}
const [observer] = useState<ResizeObserver>(
() =>
new ResizeObserver(() => {
handleSlideChange(0);
}),
);
const wrapperStyles = useSpring({
x: `-${slideIndex * 100}%`,
height: currentSlideHeight,
height: currentSlideHeight ?? 0,
// Don't animate from zero to the height of the initial slide
immediate: !previousSlideHeight,
});
useLayoutEffect(() => {
// Update slide height when the component mounts
if (currentSlideHeight === 0) {
handleSlideChange(0);
}
}, [currentSlideHeight, handleSlideChange]);
// Handle swiping animations
const bind = useDrag(
@@ -135,12 +139,12 @@ export const Carousel = <
handleSlideChange(-1);
// We're focusing on the wrapper as the child slides can potentially be inert.
// Because of that, only the active slide can be focused anyway.
wrapperRef.current?.focus();
}, [handleSlideChange]);
wrapperElement?.focus();
}, [handleSlideChange, wrapperElement]);
const handleNext = useCallback(() => {
handleSlideChange(1);
wrapperRef.current?.focus();
}, [handleSlideChange]);
wrapperElement?.focus();
}, [handleSlideChange, wrapperElement]);
const intl = useIntl();
@@ -173,7 +177,7 @@ export const Carousel = <
<animated.div
className={`${classNamePrefix}__slides`}
ref={wrapperRef}
ref={setWrapperElement}
style={wrapperStyles}
aria-label={intl.formatMessage(messages.slide, {
current: slideIndex + 1,
@@ -185,7 +189,7 @@ export const Carousel = <
<CarouselSlideWrapper<SlideProps>
item={itemsProps}
renderItem={renderItem}
observer={observerRef.current}
observer={observer}
index={index}
key={`slide-${itemsProps.id}`}
className={classNames(`${classNamePrefix}__slide`, slideClassName, {

View File

@@ -488,6 +488,8 @@ export const Dropdown = <Item extends object | null = MenuItem>({
};
if (children) {
// We're just assigning the ref to an element so this is safe
// eslint-disable-next-line react-hooks/refs
button = cloneElement(Children.only(children), buttonProps);
} else if (icon && iconComponent) {
button = (

View File

@@ -317,6 +317,9 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
// Reset scroll & highlight when menu items change
useEffect(() => {
if (flatItems.length) {
// This only runs when the items change so should be safe from
// cascade renders.
// eslint-disable-next-line react-hooks/set-state-in-effect
resetHighlight();
}
}, [flatItems, resetHighlight]);

View File

@@ -74,6 +74,7 @@ export const ScrollContext: React.FC<ScrollContextProps> = ({
* than a ref to simplify the types and ensure it's defined immediately.
*/
const [scrollBehavior] = useState(
// eslint-disable-next-line react-hooks/refs
(): ScrollBehaviorInstance =>
new ScrollBehavior({
addNavigationListener: history.listen.bind(history),

View File

@@ -20,9 +20,11 @@ import { SpoilerButton } from 'mastodon/components/spoiler_button';
import { formatTime, getPointerPosition } from 'mastodon/features/video';
import { useAudioContext } from 'mastodon/hooks/useAudioContext';
import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer';
import { displayMedia, useBlurhash } from 'mastodon/initial_state';
import { useBlurhash } from 'mastodon/initial_state';
import { playerSettings } from 'mastodon/settings';
import { useRevealedMedia } from '../../hooks/useRevealedMedia';
import { AudioVisualizer } from './visualizer';
const messages = defineMessages({
@@ -100,7 +102,7 @@ export const Audio: React.FC<{
const [volume, setVolume] = useState(0.5);
const [hovered, setHovered] = useState(false);
const [dragging, setDragging] = useState(false);
const [revealed, setRevealed] = useState(false);
const [revealed, setRevealed] = useRevealedMedia({ visible, sensitive });
const playerRef = useRef<HTMLDivElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);
@@ -177,17 +179,6 @@ export const Audio: React.FC<{
}
}, [volume, muted, gainNodeRef]);
useEffect(() => {
if (typeof visible !== 'undefined') {
setRevealed(visible);
} else {
setRevealed(
displayMedia === 'show_all' ||
(displayMedia !== 'hide_all' && !sensitive),
);
}
}, [visible, sensitive]);
useEffect(() => {
if (!revealed) {
pauseAudio();

View File

@@ -8,6 +8,7 @@ import { useSpring, animated, config } from '@react-spring/web';
import { throttle } from 'lodash';
import type { DeployPictureInPictureCallback } from '@/mastodon/actions/picture_in_picture';
import { useRevealedMedia } from '@/mastodon/hooks/useRevealedMedia';
import Forward5Icon from '@/material-icons/400-24px/forward_5-fill.svg?react';
import FullscreenIcon from '@/material-icons/400-24px/fullscreen.svg?react';
import FullscreenExitIcon from '@/material-icons/400-24px/fullscreen_exit.svg?react';
@@ -28,7 +29,7 @@ import {
attachFullscreenListener,
detachFullscreenListener,
} from 'mastodon/features/ui/util/fullscreen';
import { displayMedia, useBlurhash } from 'mastodon/initial_state';
import { useBlurhash } from 'mastodon/initial_state';
import { playerSettings } from 'mastodon/settings';
import { HotkeyIndicator } from './components/hotkey_indicator';
@@ -214,7 +215,7 @@ export const Video: React.FC<{
const [fullscreen, setFullscreen] = useState(false);
const [hovered, setHovered] = useState(false);
const [muted, setMuted] = useState(false);
const [revealed, setRevealed] = useState(false);
const [revealed, setRevealed] = useRevealedMedia({ visible, sensitive });
const [hotkeyEvents, setHotkeyEvents] = useState<HotkeyEvent[]>([]);
const playerRef = useRef<HTMLDivElement>(null);
@@ -362,17 +363,6 @@ export const Video: React.FC<{
videoRef.current.muted = muted;
}, [volume, muted]);
useEffect(() => {
if (typeof visible !== 'undefined') {
setRevealed(visible);
} else {
setRevealed(
displayMedia === 'show_all' ||
(displayMedia !== 'hide_all' && !sensitive),
);
}
}, [visible, sensitive]);
useEffect(() => {
if (!revealed && videoRef.current) {
videoRef.current.pause();

View File

@@ -24,5 +24,6 @@ export function useMergedRefs<T>(...refs: React.Ref<T>[]) {
},
[refs],
);
// eslint-disable-next-line react-hooks/immutability
return setRef;
}

View File

@@ -1,29 +1,25 @@
import { useEffect, useRef } from 'react';
import { useEffect, useState } from 'react';
export function useResizeObserver(callback: ResizeObserverCallback) {
const observerRef = useRef<ResizeObserver>(null);
observerRef.current ??= new ResizeObserver(callback);
const [observer] = useState(() => new ResizeObserver(callback));
useEffect(() => {
const observer = observerRef.current;
return () => {
observer?.disconnect();
observer.disconnect();
};
}, []);
}, [observer]);
return observerRef.current;
return observer;
}
export function useMutationObserver(callback: MutationCallback) {
const observerRef = useRef<MutationObserver>(null);
observerRef.current ??= new MutationObserver(callback);
const [observer] = useState(() => new MutationObserver(callback));
useEffect(() => {
const observer = observerRef.current;
return () => {
observer?.disconnect();
observer.disconnect();
};
}, []);
}, [observer]);
return observerRef.current;
return observer;
}

View File

@@ -213,6 +213,7 @@ export function useOverflowObservers({
// Watch the wrapper for size changes, and recalculate when it resizes.
const wrapperRef = useRef<HTMLElement>(null);
const wrapperRefCallback = useCallback(
// eslint-disable-next-line react-hooks/immutability
(node: HTMLElement | null) => {
if (node) {
wrapperRef.current = node; // eslint-disable-line react-hooks/immutability -- https://github.com/facebook/react/issues/34955
@@ -229,6 +230,7 @@ export function useOverflowObservers({
// If there are changes to the children, recalculate which are visible.
const listRefCallback = useCallback(
// eslint-disable-next-line react-hooks/immutability
(node: HTMLElement | null) => {
if (node) {
listRef.current = node;

View File

@@ -0,0 +1,33 @@
import { useState } from 'react';
import { displayMedia } from 'mastodon/initial_state';
interface RevealedMediaProps {
visible?: boolean;
sensitive?: boolean;
}
function getRevealedState({ visible, sensitive }: RevealedMediaProps) {
if (typeof visible !== 'undefined') {
return visible;
} else {
return (
displayMedia === 'show_all' || (displayMedia !== 'hide_all' && !sensitive)
);
}
}
export function useRevealedMedia({ visible, sensitive }: RevealedMediaProps) {
const [revealed, setRevealed] = useState(() =>
getRevealedState({ visible, sensitive }),
);
// Update `revealed` state when `sensitive` or `visible` props change
const [previous, setPrevious] = useState({ visible, sensitive });
if (sensitive !== previous.sensitive || visible !== previous.visible) {
setRevealed(getRevealedState({ visible, sensitive }));
setPrevious({ visible, sensitive });
}
return [revealed, setRevealed] as const;
}

View File

@@ -10,7 +10,6 @@ import jsxA11Y from 'eslint-plugin-jsx-a11y';
import promisePlugin from 'eslint-plugin-promise';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
// @ts-expect-error -- No types available for this package
import storybook from 'eslint-plugin-storybook';
import { globalIgnores } from 'eslint/config';
import globals from 'globals';
@@ -137,7 +136,6 @@ export default tseslint.config([
reactHooks.configs.flat.recommended,
jsxA11Y.flatConfigs.recommended,
importPlugin.flatConfigs.react,
// @ts-expect-error -- For some reason the formatjs package exports an empty object?
formatjs.configs.strict,
storybook.configs['flat/recommended'],
{

View File

@@ -127,7 +127,7 @@
"wicg-inert": "3.1.3"
},
"devDependencies": {
"@eslint/js": "9.39.2",
"@eslint/js": "9.39.5",
"@formatjs/cli": "6.14.4",
"@storybook/addon-a11y": "10.3.3",
"@storybook/addon-docs": "10.3.3",
@@ -160,18 +160,18 @@
"@vitest/coverage-v8": "4.1.9",
"@vitest/ui": "4.1.9",
"chromatic": "18.0.0",
"eslint": "9.39.2",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-formatjs": "6.4.3",
"eslint": "9.39.5",
"eslint-import-resolver-typescript": "4.4.5",
"eslint-plugin-formatjs": "6.4.20",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsdoc": "63.0.0",
"eslint-plugin-jsdoc": "63.3.3",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-promise": "7.3.0",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-storybook": "10.2.8",
"eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-storybook": "10.5.7",
"fake-indexeddb": "6.2.5",
"globals": "17.3.0",
"globals": "17.9.0",
"husky": "9.1.7",
"lint-staged": "17.0.5",
"msw": "2.12.14",
@@ -183,7 +183,7 @@
"stylelint-config-standard-scss": "17.0.0",
"type-fest": "5.7.0",
"typescript": "npm:@typescript/typescript6@6.0.2",
"typescript-eslint": "8.65.0",
"typescript-eslint": "8.66.0",
"typescript-plugin-css-modules": "5.2.0",
"vitest": "4.1.10"
},

923
yarn.lock

File diff suppressed because it is too large Load Diff