diff --git a/.ruby-version b/.ruby-version index f9892605c75..4f5e69734c9 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.4 +3.4.5 diff --git a/.storybook/main.ts b/.storybook/main.ts index 72321cbf3f1..bb69f0c6649 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,3 +1,5 @@ +import { resolve } from 'node:path'; + import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { @@ -26,6 +28,12 @@ const config: StorybookConfig = { 'oops.png', ].map((path) => ({ from: `../public/${path}`, to: `/${path}` })), ], + viteFinal(config) { + // For an unknown reason, Storybook does not use the root + // from the Vite config so we need to set it manually. + config.root = resolve(__dirname, '../app/javascript'); + return config; + }, }; export default config; diff --git a/Dockerfile b/Dockerfile index b7d1159dfcb..23214c2483c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io" # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.4.4" +ARG RUBY_VERSION="3.4.5" # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="22" diff --git a/Gemfile.lock b/Gemfile.lock index 6837c110c6b..a5855883e39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -224,7 +224,7 @@ GEM mail (~> 2.7) email_validator (2.2.4) activemodel - erb (5.0.1) + erb (5.0.2) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -315,7 +315,7 @@ GEM http_accept_language (2.1.1) httpclient (2.9.0) mutex_m - httplog (1.7.0) + httplog (1.7.1) rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.7) @@ -335,7 +335,7 @@ GEM inline_svg (1.10.0) activesupport (>= 3.0) nokogiri (>= 1.6) - io-console (0.8.0) + io-console (0.8.1) irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -627,11 +627,10 @@ GEM prism (1.4.0) prometheus_exporter (2.2.0) webrick - propshaft (1.1.0) + propshaft (1.2.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack - railties (>= 7.0.0) psych (5.2.6) date stringio @@ -708,7 +707,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.14.1) + rdoc (6.14.2) erb psych (>= 4.0.0) redcarpet (3.6.1) diff --git a/app/javascript/mastodon/features/alt_text_modal/index.tsx b/app/javascript/mastodon/features/alt_text_modal/index.tsx index 8a91e14e312..f285c35929c 100644 --- a/app/javascript/mastodon/features/alt_text_modal/index.tsx +++ b/app/javascript/mastodon/features/alt_text_modal/index.tsx @@ -261,7 +261,9 @@ export const AltTextModal = forwardRef>( ); const lang = useAppSelector( (state) => - (state.compose as ImmutableMap).get('lang') as string, + (state.compose as ImmutableMap).get( + 'language', + ) as string, ); const focusX = (media?.getIn(['meta', 'focus', 'x'], 0) as number | undefined) ?? 0; diff --git a/app/javascript/mastodon/features/navigation_panel/index.tsx b/app/javascript/mastodon/features/navigation_panel/index.tsx index b62e8f9fa62..66f8a657edf 100644 --- a/app/javascript/mastodon/features/navigation_panel/index.tsx +++ b/app/javascript/mastodon/features/navigation_panel/index.tsx @@ -431,6 +431,7 @@ export const CollapsibleNavigationPanel: React.FC = () => { filterTaps: true, bounds: isLtrDir ? { left: 0 } : { right: 0 }, rubberband: true, + enabled: openable, }, ); diff --git a/app/javascript/mastodon/features/notifications/components/policy_controls.tsx b/app/javascript/mastodon/features/notifications/components/policy_controls.tsx index 032c0ea4835..a4743b0c221 100644 --- a/app/javascript/mastodon/features/notifications/components/policy_controls.tsx +++ b/app/javascript/mastodon/features/notifications/components/policy_controls.tsx @@ -122,98 +122,93 @@ export const PolicyControls: React.FC = () => { value={notificationPolicy.for_not_following} onChange={handleFilterNotFollowing} options={options} - > - + label={ - - + } + hint={ - - + } + /> - + label={ - - + } + hint={ - - + } + /> - + label={ - - + } + hint={ - - + } + /> - + label={ - - + } + hint={ - - + } + /> - + label={ - - + } + hint={ - - + } + /> ); diff --git a/app/javascript/mastodon/features/notifications/components/select_with_label.tsx b/app/javascript/mastodon/features/notifications/components/select_with_label.tsx index 413267c0f8c..b25f8e66be2 100644 --- a/app/javascript/mastodon/features/notifications/components/select_with_label.tsx +++ b/app/javascript/mastodon/features/notifications/components/select_with_label.tsx @@ -1,5 +1,5 @@ import type { PropsWithChildren } from 'react'; -import { useCallback, useState, useRef } from 'react'; +import { useCallback, useState, useRef, useId } from 'react'; import classNames from 'classnames'; @@ -16,6 +16,8 @@ interface DropdownProps { options: SelectItem[]; disabled?: boolean; onChange: (value: string) => void; + 'aria-labelledby': string; + 'aria-describedby'?: string; placement?: Placement; } @@ -24,51 +26,33 @@ const Dropdown: React.FC = ({ options, disabled, onChange, + 'aria-labelledby': ariaLabelledBy, + 'aria-describedby': ariaDescribedBy, placement: initialPlacement = 'bottom-end', }) => { - const activeElementRef = useRef(null); - const containerRef = useRef(null); + const containerRef = useRef(null); + const buttonRef = useRef(null); const [isOpen, setOpen] = useState(false); const [placement, setPlacement] = useState(initialPlacement); - - const handleToggle = useCallback(() => { - if ( - isOpen && - activeElementRef.current && - activeElementRef.current instanceof HTMLElement - ) { - activeElementRef.current.focus({ preventScroll: true }); - } - - setOpen(!isOpen); - }, [isOpen, setOpen]); - - const handleMouseDown = useCallback(() => { - if (!isOpen) activeElementRef.current = document.activeElement; - }, [isOpen]); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - switch (e.key) { - case ' ': - case 'Enter': - if (!isOpen) activeElementRef.current = document.activeElement; - break; - } - }, - [isOpen], - ); + const uniqueId = useId(); + const menuId = `${uniqueId}-menu`; + const buttonLabelId = `${uniqueId}-button`; const handleClose = useCallback(() => { - if ( - isOpen && - activeElementRef.current && - activeElementRef.current instanceof HTMLElement - ) - activeElementRef.current.focus({ preventScroll: true }); + if (isOpen && buttonRef.current) { + buttonRef.current.focus({ preventScroll: true }); + } setOpen(false); }, [isOpen]); + const handleToggle = useCallback(() => { + if (isOpen) { + handleClose(); + } else { + setOpen(true); + } + }, [isOpen, handleClose]); + const handleOverlayEnter = useCallback( (state: Partial) => { if (state.placement) setPlacement(state.placement); @@ -82,13 +66,18 @@ const Dropdown: React.FC = ({
@@ -101,7 +90,7 @@ const Dropdown: React.FC = ({ popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }} > {({ props, placement }) => ( -
+
@@ -123,6 +112,8 @@ const Dropdown: React.FC = ({ interface Props { value: string; options: SelectItem[]; + label: string | React.ReactElement; + hint: string | React.ReactElement; disabled?: boolean; onChange: (value: string) => void; } @@ -130,13 +121,26 @@ interface Props { export const SelectWithLabel: React.FC> = ({ value, options, + label, + hint, disabled, - children, onChange, }) => { + const uniqueId = useId(); + const labelId = `${uniqueId}-label`; + const descId = `${uniqueId}-desc`; + return ( + // This label is only used for its click-forwarding behaviour, + // accessible names are assigned manually + // eslint-disable-next-line jsx-a11y/label-has-associated-control