mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-12 13:45:42 -05:00
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:
@@ -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 },
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -24,5 +24,6 @@ export function useMergedRefs<T>(...refs: React.Ref<T>[]) {
|
||||
},
|
||||
[refs],
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
return setRef;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
33
app/javascript/mastodon/hooks/useRevealedMedia.ts
Normal file
33
app/javascript/mastodon/hooks/useRevealedMedia.ts
Normal 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;
|
||||
}
|
||||
@@ -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'],
|
||||
{
|
||||
|
||||
20
package.json
20
package.json
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user