From f10b522f0cfbe1b8d9d8cbb0e5bffa82b089592f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 10:47:39 +0200 Subject: [PATCH 01/10] chore(deps): update dependency ruby to v3.4.5 (#35395) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 02a4e30594e0d1524f92e5f8d29b9837852bd37f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 10:48:10 +0200 Subject: [PATCH 02/10] chore(deps): update dependency propshaft to v1.2.0 (#35398) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6837c110c6b..27c19c437e5 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 @@ -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) From faffb73cbd48e2ef094e8802fbd008382cf8f26e Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 17 Jul 2025 10:56:00 +0200 Subject: [PATCH 03/10] fix: Improve a11y of custom select menus in notifications settings (#35403) --- .../components/policy_controls.tsx | 55 +++++------ .../components/select_with_label.tsx | 92 ++++++++++--------- 2 files changed, 74 insertions(+), 73 deletions(-) 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