From fcf3a958c9fa1da5c5d7bac27afd52f27c9bff5f Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Fri, 3 Jul 2026 15:45:38 +0200 Subject: [PATCH 01/11] Replace title tooltips with descriptive ARIA labels for work package, wiki, user, group, and other inline resource links and macros --- config/locales/js-en.yml | 3 +++ .../macros/attribute-label-macro.component.ts | 5 +++-- .../macros/attribute-value-macro.component.ts | 5 +++-- .../text_formatting/matchers/attribute_macros.rb | 9 ++++++++- .../matchers/link_handlers/base.rb | 15 +++++++++++++++ modules/wikis/config/locales/en.yml | 2 ++ .../markdown/attribute_macros_spec.rb | 14 +++++++------- 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 8a5c2a49fc1f..37cb3e01c628 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -175,6 +175,9 @@ en: macro: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." + aria_label_resource_link: "A dynamic link to a %{resource} placed using a macro" + aria_label_work_package_attribute: "A dynamic work package attribute placed using a macro" + aria_label_work_package_link: "A dynamic link to a work package placed using a macro" not_found: "Requested resource could not be found" nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." diff --git a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts index a683a8074bcf..f788a8f2945d 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts @@ -64,12 +64,12 @@ export class AttributeLabelMacroComponent implements OnInit { error:string|null = null; text = { - help: this.I18n.t('js.editor.macro.attribute_reference.macro_help_tooltip'), + aria_label: this.I18n.t('js.editor.macro.attribute_reference.aria_label_work_package_attribute'), not_found: this.I18n.t('js.editor.macro.attribute_reference.not_found'), invalid_attribute: (attr:string) => this.I18n.t('js.editor.macro.attribute_reference.invalid_attribute', { name: attr }), }; - @HostBinding('title') hostTitle = this.text.help; + @HostBinding('attr.aria-label') hostAriaLabel:string|null = null; // The loaded resource, required for help text resource:HalResource|null = null; @@ -88,6 +88,7 @@ export class AttributeLabelMacroComponent implements OnInit { const model = element.dataset.model as SupportedAttributeModels; const id = element.dataset.id!; const attributeName = element.dataset.attribute!; + this.hostAriaLabel = model === 'workPackage' ? this.text.aria_label : null; this.attributeScope = capitalize(model); void this.loadResourceAttribute(model, id, attributeName); diff --git a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts index 1467ad635f95..650d59e19c72 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts @@ -68,13 +68,13 @@ export class AttributeValueMacroComponent implements OnInit { error:string|null = null; text = { - help: this.I18n.t('js.editor.macro.attribute_reference.macro_help_tooltip'), + aria_label: this.I18n.t('js.editor.macro.attribute_reference.aria_label_work_package_attribute'), placeholder: this.I18n.t('js.placeholders.default'), not_found: this.I18n.t('js.editor.macro.attribute_reference.not_found'), invalid_attribute: (attr:string) => this.I18n.t('js.editor.macro.attribute_reference.invalid_attribute', { name: attr }), }; - @HostBinding('title') hostTitle = this.text.help; + @HostBinding('attr.aria-label') hostAriaLabel:string|null = null; resource:HalResource; @@ -86,6 +86,7 @@ export class AttributeValueMacroComponent implements OnInit { const id = element.dataset.id!; const attributeName = element.dataset.attribute!; element.classList.add(ATTRIBUTE_MACRO_CLASS); + this.hostAriaLabel = model === 'workPackage' ? this.text.aria_label : null; if (this.isNestedMacro(model, id, attributeName)) { const error = this.I18n.t('js.editor.macro.attribute_reference.nested_macro', { model, id }); diff --git a/lib/open_project/text_formatting/matchers/attribute_macros.rb b/lib/open_project/text_formatting/matchers/attribute_macros.rb index c78055ded2b8..2a6100698e09 100644 --- a/lib/open_project/text_formatting/matchers/attribute_macros.rb +++ b/lib/open_project/text_formatting/matchers/attribute_macros.rb @@ -92,9 +92,16 @@ def self.process_match(match, _matched_string, context) macro_attributes[:id] = relative_id(macro_attributes, context) if relative_embed?(macro_attributes) + tag_options = { data: macro_attributes } + if work_package_embed?(macro_attributes) + tag_options[:aria] = { + label: I18n.t("js.editor.macro.attribute_reference.aria_label_work_package_attribute") + } + end + ApplicationController.helpers.content_tag "opce-macro-attribute-#{type}", "", - data: macro_attributes + **tag_options end end end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/base.rb b/lib/open_project/text_formatting/matchers/link_handlers/base.rb index 6dc2d2484e5f..77ef9aba62b0 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/base.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/base.rb @@ -97,6 +97,21 @@ def named_route(name, **args) end def controller; end + + private + + def link_to(name = nil, options = nil, html_options = nil, &) + html_options = (html_options || {}) + .except(:title, "title") + .merge(aria: { label: resource_link_aria_label }) + + super(name, options, html_options, &) # rubocop:disable Style/SuperArguments + end + + def resource_link_aria_label + resource = matcher.prefix.presence || (matcher.sep == "r" ? "revision" : "resource") + I18n.t("js.editor.macro.attribute_reference.aria_label_resource_link", resource:) + end end end end diff --git a/modules/wikis/config/locales/en.yml b/modules/wikis/config/locales/en.yml index 198d590d1e6f..f751d9802e39 100644 --- a/modules/wikis/config/locales/en.yml +++ b/modules/wikis/config/locales/en.yml @@ -132,6 +132,7 @@ en: create_new_wiki_page_dialog: title: Create new wiki page deferred_inline_page_link_macro_component: + aria_label: A dynamic link to a wiki page placed using a macro loading: Loading… delete_relation_page_link_confirmation_dialog: heading: Delete related wiki page link? @@ -180,6 +181,7 @@ en: no_results: No wiki pages found placeholder: Search for a wiki page (or enter its URL) page_links: + aria_label: A dynamic link to a wiki page placed using a macro errors: page_access_forbidden: You do not have permission to access this wiki page page_not_found: Linked wiki page no longer available diff --git a/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb b/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb index c3fb8a5c8286..6a80d18112bd 100644 --- a/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb @@ -61,16 +61,16 @@

- Inline reference to WP: + Inline reference to WP:

- Inline reference to WP by ID: + Inline reference to WP by ID:

- Inline reference to WP by ID with CF with a dot: + Inline reference to WP by ID with CF with a dot:

- Inline reference to WP by subject: + Inline reference to WP by subject:

Inline reference to project: @@ -113,13 +113,13 @@

- Inline reference to WP: + Inline reference to WP:

- Inline reference to WP by ID: + Inline reference to WP by ID:

- Inline reference to WP by subject: + Inline reference to WP by subject:

Inline reference to project: From 2c8ee498dd851c4650a26803ac8e7adaf63bacea Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Fri, 3 Jul 2026 15:48:23 +0200 Subject: [PATCH 02/11] Replace title tooltips with descriptive ARIA labels for resource and wiki page links, and add component and formatter coverage --- .../matchers/link_handlers/colon_separator.rb | 3 +- ..._inline_page_link_macro_component.html.erb | 2 +- .../inline_page_link_macro_component.html.erb | 2 +- ...d_inline_page_link_macro_component_spec.rb | 48 +++++++++++ .../inline_page_link_macro_component_spec.rb | 58 +++++++++++++ .../markdown/in_tool_links_spec.rb | 84 ++++++++++++------- 6 files changed, 163 insertions(+), 34 deletions(-) create mode 100644 modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb create mode 100644 modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb diff --git a/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb b/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb index 71bd7526928f..8f28caa235ad 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb @@ -95,8 +95,7 @@ def render_commit # rubocop:disable Metrics/AbcSize link_to h("#{matcher.project_prefix}#{matcher.identifier}"), { only_path: context[:only_path], controller: "/repositories", action: "revision", project_id: project, rev: changeset.identifier }, - class: "changeset", - title: truncate_single_line(changeset.comments, length: 100) + class: "changeset" end end diff --git a/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb b/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb index c79be6ac8944..f2fc07e5ea90 100644 --- a/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb +++ b/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb @@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= content_tag("turbo-frame", id: frame_id, src: frame_src, data: { provider_id:, page_identifier: identifier, type: "wiki-page-link" }) do %> - <%= render(OpPrimer::InlineMacroComponent.new) do |component| %> + <%= render(OpPrimer::InlineMacroComponent.new("aria-label": t(".aria_label"))) do |component| %> <% component.with_leading_visual_icon(icon: :"op-file-doc") %> <%= render Primer::Beta::Spinner.new(size: :small, display: :flex, mr: 2) %> diff --git a/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb b/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb index cd02da27c17d..2f9f7a034835 100644 --- a/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb +++ b/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb @@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= - render(OpPrimer::InlineMacroComponent.new) do |inline_macro| + render(OpPrimer::InlineMacroComponent.new("aria-label": t("wikis.page_links.aria_label"))) do |inline_macro| page_info_result.either( ->(page_info) do inline_macro.with_leading_visual_icon(icon: :"op-file-doc") diff --git a/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb new file mode 100644 index 000000000000..e7bfe9116622 --- /dev/null +++ b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Wikis::DeferredInlinePageLinkMacroComponent, type: :component do + subject(:render_component) do + render_inline(described_class.new(identifier: "1234", provider_id: "42")) + end + + before { render_component } + + it "renders the loading wiki page macro with an ARIA label and no title" do + expect(page).to have_css("turbo-frame[data-type='wiki-page-link']") + expect(page).to have_css( + ".op-inline-macro[aria-label='#{I18n.t('wikis.deferred_inline_page_link_macro_component.aria_label')}']" + ) + expect(page).to have_no_css(".op-inline-macro[title]") + end +end diff --git a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb new file mode 100644 index 000000000000..031b22857cd6 --- /dev/null +++ b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Wikis::InlinePageLinkMacroComponent, type: :component do + let(:provider) { build_stubbed(:internal_wiki_provider) } + let(:page_info) do + Wikis::Adapters::Results::PageInfo.new( + identifier: "1234", + title: "Stormtrooper training", + provider:, + href: "https://wiki.death.star/Home/stormtrooper_training" + ) + end + let(:page_info_result) { Success(page_info) } + + subject(:render_component) { render_inline(described_class.new(page_info_result)) } + + before { render_component } + + it "renders a wiki page macro link with an ARIA label and no title" do + expect(page).to have_link(text: page_info.title, href: page_info.href) + expect(page).to have_css( + ".op-inline-macro[aria-label='#{I18n.t('wikis.page_links.aria_label')}']" + ) + expect(page).to have_no_css(".op-inline-macro[title]") + end +end + diff --git a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb index 30ab973f82ed..a1c83e718e28 100644 --- a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb @@ -83,12 +83,16 @@ let(:changeset_link) do link_to("r#{changeset1.revision}", { controller: "repositories", action: "revision", project_id: identifier, rev: changeset1.revision }, - class: "changeset op-uc-link", title: "My very first commit", target: "_top") + class: "changeset op-uc-link", + aria: { label: "A dynamic link to a revision placed using a macro" }, + target: "_top") end let(:changeset_link2) do link_to("r#{changeset2.revision}", { controller: "repositories", action: "revision", project_id: identifier, rev: changeset2.revision }, - class: "changeset op-uc-link", title: "This commit fixes #1, #2 and references #1 & #3", target: "_top") + class: "changeset op-uc-link", + aria: { label: "A dynamic link to a revision placed using a macro" }, + target: "_top") end before do @@ -138,7 +142,9 @@ let(:version_link) do link_to("1.0", { controller: "versions", action: "show", id: version.id }, - class: "version op-uc-link", target: "_top") + class: "version op-uc-link", + target: "_top", + aria: { label: "A dynamic link to a version placed using a macro" }) end context "Link with version id" do @@ -195,6 +201,7 @@ "project plan with milestones", project_work_packages_path([query.project.id], query_id: query.id), class: "query op-uc-link", + aria: { label: "A dynamic link to a view placed using a macro" }, target: "_top" ) end @@ -218,6 +225,7 @@ "Work packages", project_work_packages_path([project.id]), class: "query op-uc-link", + aria: { label: "A dynamic link to a view placed using a macro" }, target: "_top" ) end @@ -255,6 +263,7 @@ expect(subject).to be_html_eql("

#{link_to(message1.subject, project_forum_topic_path(project, forum, message1), class: 'message op-uc-link', + aria: { label: "A dynamic link to a message placed using a macro" }, target: '_top')}

") } end @@ -266,6 +275,7 @@ link = link_to(message2.subject, project_forum_topic_path(project, forum, message1, anchor: "message-#{message2.id}", r: message2.id), class: "message op-uc-link", + aria: { label: "A dynamic link to a message placed using a macro" }, target: "_top") expect(subject).to be_html_eql("

#{link}

") } @@ -281,6 +291,7 @@ hover_card_url: hover_card_work_package_path(work_package.id) }, class: "issue work_package op-uc-link", + aria: { label: "A dynamic link to a work package placed using a macro" }, target: "_top") end @@ -310,7 +321,8 @@ let(:work_package_link) do content_tag "opce-macro-wp-quickinfo", "", - data: { id: "1234", display_id: "1234", detailed: "false" } + data: { id: "1234", display_id: "1234", detailed: "false" }, + aria: { label: "A dynamic link to a work package placed using a macro" } end subject { format_text("foo (bar ##1234)") } @@ -322,7 +334,8 @@ let(:work_package_link) do content_tag "opce-macro-wp-quickinfo", "", - data: { id: "1234", display_id: "1234", detailed: "true" } + data: { id: "1234", display_id: "1234", detailed: "true" }, + aria: { label: "A dynamic link to a work package placed using a macro" } end subject { format_text("foo (bar ###1234)") } @@ -355,6 +368,7 @@ hover_card_url: hover_card_work_package_path(work_package.id) }, class: "issue work_package op-uc-link", + aria: { label: "A dynamic link to a work package placed using a macro" }, target: "_top") end @@ -382,6 +396,7 @@ it { expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', + aria: { label: "A dynamic link to a project placed using a macro" }, class: 'project op-uc-link')}

") } end @@ -392,6 +407,7 @@ it { expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', + aria: { label: "A dynamic link to a project placed using a macro" }, class: 'project op-uc-link')}

") } end @@ -402,6 +418,7 @@ it { expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', + aria: { label: "A dynamic link to a project placed using a macro" }, class: 'project op-uc-link')}

") } end @@ -598,6 +615,13 @@ def source_url_with_ext(**) entry_revision_project_repository_path(project_id: identifier, repo_path: "some/file.ext", **) end + def resource_link_to(resource, name, url, **options) + link_to(name, + url, + **options, + aria: { label: "A dynamic link to a #{resource} placed using a macro" }) + end + before do allow(project).to receive(:repository).and_return(repository) allow(User).to receive(:current).and_return(project_member) @@ -608,30 +632,30 @@ def source_url_with_ext(**) @to_test = { # source - "source:/some/file" => link_to("source:/some/file", source_url, class: "source op-uc-link", target: "_top"), - "source:/some/file." => link_to("source:/some/file", source_url, class: "source op-uc-link", target: "_top") + ".", - 'source:"/some/file.ext".' => link_to("source:/some/file.ext", source_url_with_ext, class: "source op-uc-link", - target: "_top") + ".", - "source:/some/file. " => link_to("source:/some/file", source_url, class: "source op-uc-link", target: "_top") + ".", - 'source:"/some/file.ext". ' => link_to("source:/some/file.ext", source_url_with_ext, class: "source op-uc-link", - target: "_top") + ".", - "source:/some/file, " => link_to("source:/some/file", source_url, class: "source op-uc-link", target: "_top") + ",", - "source:/some/file@52" => link_to("source:/some/file@52", source_url(rev: 52), class: "source op-uc-link", - target: "_top"), - 'source:"/some/file.ext@52"' => link_to("source:/some/file.ext@52", source_url_with_ext(rev: 52), - class: "source op-uc-link", - target: "_top"), - 'source:"/some/file#L110"' => link_to("source:/some/file#L110", source_url(anchor: "L110"), class: "source op-uc-link", - target: "_top"), - 'source:"/some/file.ext#L110"' => link_to("source:/some/file.ext#L110", source_url_with_ext(anchor: "L110"), - class: "source op-uc-link", - target: "_top"), - 'source:"/some/file@52#L110"' => link_to("source:/some/file@52#L110", source_url(rev: 52, anchor: "L110"), - class: "source op-uc-link", - target: "_top"), - "export:/some/file" => link_to("export:/some/file", source_url(format: "raw"), - class: "source download op-uc-link", - target: "_top"), + "source:/some/file" => resource_link_to("source", "source:/some/file", source_url, class: "source op-uc-link", target: "_top"), + "source:/some/file." => resource_link_to("source", "source:/some/file", source_url, class: "source op-uc-link", target: "_top") + ".", + 'source:"/some/file.ext".' => resource_link_to("source", "source:/some/file.ext", source_url_with_ext, + class: "source op-uc-link", target: "_top") + ".", + "source:/some/file. " => resource_link_to("source", "source:/some/file", source_url, + class: "source op-uc-link", target: "_top") + ".", + 'source:"/some/file.ext". ' => resource_link_to("source", "source:/some/file.ext", source_url_with_ext, + class: "source op-uc-link", target: "_top") + ".", + "source:/some/file, " => resource_link_to("source", "source:/some/file", source_url, + class: "source op-uc-link", target: "_top") + ",", + "source:/some/file@52" => resource_link_to("source", "source:/some/file@52", source_url(rev: 52), + class: "source op-uc-link", target: "_top"), + 'source:"/some/file.ext@52"' => resource_link_to("source", "source:/some/file.ext@52", source_url_with_ext(rev: 52), + class: "source op-uc-link", target: "_top"), + 'source:"/some/file#L110"' => resource_link_to("source", "source:/some/file#L110", source_url(anchor: "L110"), + class: "source op-uc-link", target: "_top"), + 'source:"/some/file.ext#L110"' => resource_link_to("source", "source:/some/file.ext#L110", + source_url_with_ext(anchor: "L110"), + class: "source op-uc-link", target: "_top"), + 'source:"/some/file@52#L110"' => resource_link_to("source", "source:/some/file@52#L110", + source_url(rev: 52, anchor: "L110"), + class: "source op-uc-link", target: "_top"), + "export:/some/file" => resource_link_to("export", "export:/some/file", source_url(format: "raw"), + class: "source download op-uc-link", target: "_top"), # escaping "!source:/some/file" => "source:/some/file", # invalid expressions @@ -675,7 +699,7 @@ def source_url_with_ext(**) let(:expected) do <<~EXPECTED

CookBook documentation

-

##{work_package.id}

+

##{work_package.id}

           [[CookBook documentation]]
 

From 0e0ef73ce1b4a25b2d8df462828be643c2d24f06 Mon Sep 17 00:00:00 2001
From: Behrokh Satarnejad 
Date: Fri, 3 Jul 2026 15:49:51 +0200
Subject: [PATCH 03/11] Replace generated title tooltips with descriptive ARIA
 labels for user, group, revision, and work package links and macrosand update
 related tests

---
 .../work-package-quickinfo-macro.component.ts |  4 +--
 .../text_formatting/filters/mention_filter.rb | 29 ++++++++++++++++---
 .../matchers/link_handlers/revisions.rb       |  3 +-
 .../matchers/link_handlers/work_packages.rb   | 11 ++++++-
 .../filters/mention_filter_spec.rb            | 12 ++++++++
 .../text_formatting/markdown/mentions_spec.rb | 20 ++++++-------
 .../link_handlers/work_packages_spec.rb       |  4 +--
 7 files changed, 62 insertions(+), 21 deletions(-)

diff --git a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts
index 9e19c302548e..7b2bda703546 100644
--- a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts
+++ b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts
@@ -66,10 +66,10 @@ export class WorkPackageQuickinfoMacroComponent implements OnInit {
 
   text = {
     not_found: this.I18n.t('js.editor.macro.attribute_reference.not_found'),
-    help: this.I18n.t('js.editor.macro.attribute_reference.macro_help_tooltip'),
+    aria_label: this.I18n.t('js.editor.macro.attribute_reference.aria_label_work_package_link'),
   };
 
-  @HostBinding('title') hostTitle = this.text.help;
+  @HostBinding('attr.aria-label') hostAriaLabel = this.text.aria_label;
 
   /** Work package to be shown */
   workPackage$:Observable;
diff --git a/lib/open_project/text_formatting/filters/mention_filter.rb b/lib/open_project/text_formatting/filters/mention_filter.rb
index f1c8defddda2..357945f02cc3 100644
--- a/lib/open_project/text_formatting/filters/mention_filter.rb
+++ b/lib/open_project/text_formatting/filters/mention_filter.rb
@@ -102,13 +102,15 @@ def mention_anchor(mention)
       def user_mention(user)
         link_to_user(user,
                      only_path: context[:only_path],
-                     class: "user-mention")
+                     class: "user-mention",
+                     aria: { label: resource_link_aria_label("user") })
       end
 
       def group_mention(group)
         link_to_group(group,
                       only_path: context[:only_path],
-                      class: "user-mention")
+                      class: "user-mention",
+                      aria: { label: resource_link_aria_label("group") })
       end
 
       def work_package_mention(work_package, mention)
@@ -134,7 +136,10 @@ def work_package_quickinfo(work_package, detailed:)
                                                   "",
                                                   data: { id: work_package.id,
                                                           display_id: work_package.display_id,
-                                                          detailed: }
+                                                          detailed: },
+                                                  aria: {
+                                                    label: work_package_link_aria_label
+                                                  }
       end
 
       # Uses the WP's current `formatted_id` rather than the envelope text,
@@ -145,7 +150,16 @@ def work_package_static_macro(work_package, detailed:)
 
         link_to(label,
                 work_package_path_or_url(id: work_package.display_id, only_path: context[:only_path]),
-                class: "issue work_package")
+                class: "issue work_package",
+                aria: { label: work_package_link_aria_label })
+      end
+
+      def work_package_link_aria_label
+        I18n.t("js.editor.macro.attribute_reference.aria_label_work_package_link")
+      end
+
+      def resource_link_aria_label(resource)
+        I18n.t("js.editor.macro.attribute_reference.aria_label_resource_link", resource:)
       end
 
       def work_package_link(work_package)
@@ -153,6 +167,7 @@ def work_package_link(work_package)
         link_to(work_package.formatted_id,
                 work_package_path_or_url(id: display_id, only_path: context[:only_path]),
                 class: "issue work_package",
+                aria: { label: work_package_link_aria_label },
                 data: {
                   hover_card_trigger_target: "trigger",
                   hover_card_url: hover_card_work_package_path(display_id)
@@ -179,6 +194,12 @@ def fallback_text(mention)
       # For link_to
       def controller; end
 
+      def link_to(name = nil, options = nil, html_options = nil, &)
+        html_options = (html_options || {}).except(:title, "title")
+
+        super(name, options, html_options, &) # rubocop:disable Style/SuperArguments
+      end
+
       def mention_id(mention)
         value = mention.attributes["data-id"]&.value
         # Reject semantic-shaped data-ids: `PROJ-42` must not silently
diff --git a/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb b/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb
index d031c94f1b67..276e76d3eda3 100644
--- a/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb
+++ b/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb
@@ -53,8 +53,7 @@ def call # rubocop:disable Metrics/AbcSize
           link_to(h("#{matcher.project_prefix}r#{matcher.identifier}"),
                   { only_path: context[:only_path], controller: "/repositories", action: "revision", project_id: project,
                     rev: changeset.revision },
-                  class: "changeset",
-                  title: truncate_single_line(changeset.comments, length: 100))
+                  class: "changeset")
         end
       end
     end
diff --git a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb
index 1f44e340e5f0..07e41cd428cf 100644
--- a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb
+++ b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb
@@ -108,7 +108,10 @@ def render_work_package_macro(work_package:, fallback_id:, detailed: false)
 
         ApplicationController.helpers.content_tag "opce-macro-wp-quickinfo",
                                                   "",
-                                                  data: { id:, display_id:, detailed: }
+                                                  data: { id:, display_id:, detailed: },
+                                                  aria: {
+                                                    label: work_package_link_aria_label
+                                                  }
       end
 
       # The label keeps what the author wrote (possibly a historical
@@ -139,6 +142,12 @@ def render_work_package_link(work_package, fallback_id:)
                 })
       end
 
+      def work_package_link_aria_label
+        I18n.t("js.editor.macro.attribute_reference.aria_label_work_package_link")
+      end
+
+      alias_method :resource_link_aria_label, :work_package_link_aria_label
+
       # A nil WP means classic mode skipped the preload, or the reference
       # didn't resolve — neither case needs visibility gating.
       def text_only?(work_package)
diff --git a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb
index 0bf9b22a13ff..78a43738cbb6 100644
--- a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb
+++ b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb
@@ -64,6 +64,8 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil)
         expect(rendered).to include(">##{work_package.id}<")
         expect(rendered).to include(%(href="/work_packages/#{work_package.id}"))
         expect(rendered).to include(%(data-hover-card-url="/work_packages/#{work_package.id}/hover_card"))
+        expect(rendered).to include(%(aria-label="A dynamic link to a work package placed using a macro"))
+        expect(rendered).not_to include(" title=")
       end
     end
 
@@ -101,6 +103,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil)
         expect(rendered).to include(%(data-id="#{wp.id}"))
         expect(rendered).to include(%(data-display-id="#{wp.display_id}"))
         expect(rendered).to include(%(data-detailed="false"))
+        expect(rendered).to include(%(aria-label="A dynamic link to a work package placed using a macro"))
       end
     end
 
@@ -119,6 +122,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil)
         expect(rendered).to include(%(data-id="#{wp.id}"))
         expect(rendered).to include(%(data-display-id="#{wp.display_id}"))
         expect(rendered).to include(%(data-detailed="true"))
+        expect(rendered).to include(%(aria-label="A dynamic link to a work package placed using a macro"))
       end
     end
 
@@ -270,6 +274,14 @@ def user_mention_tag(user)
         %(@#{user.name})
       end
 
+      it "labels a user mention without rendering a title tooltip" do
+        user = create(:user, member_with_roles: { project => role })
+        rendered = format_text(user_mention_tag(user))
+
+        expect(rendered).to include(%(aria-label="A dynamic link to a user placed using a macro"))
+        expect(rendered).not_to include(" title=")
+      end
+
       it "loads many mentioned users with a single users SELECT keyed by id" do
         users = create_list(:user, 5, member_with_roles: { project => role })
         tags = users.map { |u| user_mention_tag(u) }.join
diff --git a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
index c8fb850644ef..51b6ef67d07d 100644
--- a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
+++ b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
@@ -85,7 +85,7 @@
                 

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - title: "User #{linked_project_member.name}", + aria: { label: "A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -184,7 +184,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - title: "User #{linked_project_member.name}", + aria: { label: "A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -212,7 +212,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - title: "User #{linked_project_member.name}", + aria: { label: "A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -241,7 +241,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - title: "User #{linked_project_member.name}", + aria: { label: "A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -273,7 +273,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - title: "User #{linked_project_member.name}", + aria: { label: "A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -302,7 +302,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - title: "User #{linked_project_member.name}", + aria: { label: "A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -373,7 +373,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="/users/#{user.id}/hover_card" href="/users/#{user.id}" - title="User Foo Barrit">Foo Barrit + aria-label="A dynamic link to a user placed using a macro">Foo Barrit

EXPECTED end @@ -400,7 +400,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="http://openproject.org/users/#{user.id}/hover_card" href="http://openproject.org/users/#{user.id}" - title="User Foo Barrit">Foo Barrit + aria-label="A dynamic link to a user placed using a macro">Foo Barrit

EXPECTED end @@ -436,7 +436,7 @@ + aria-label="A dynamic link to a group placed using a macro"> #{linked_project_member_group.name}

@@ -482,7 +482,7 @@ + aria-label="A dynamic link to a group placed using a macro"> #{linked_project_member_group.name}

diff --git a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb index a819e110a82a..d76df8dd925a 100644 --- a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb +++ b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb @@ -174,7 +174,7 @@ rendered = format_text("see ###{wp.display_id} here") expect(rendered).to include( - %() + %() ) end @@ -183,7 +183,7 @@ rendered = format_text("see ####{wp.display_id} here") expect(rendered).to include( - %() + %() ) end From 8cef7f460c2293f60620a8c416026d601dbe8689 Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Fri, 3 Jul 2026 16:02:10 +0200 Subject: [PATCH 04/11] Fix rubocop errors --- config/locales/js-en.yml | 6 +-- .../inline_page_link_macro_component_spec.rb | 1 - .../markdown/in_tool_links_spec.rb | 46 +++++++++++-------- .../text_formatting/markdown/mentions_spec.rb | 12 ++--- .../link_handlers/work_packages_spec.rb | 16 ++++--- 5 files changed, 47 insertions(+), 34 deletions(-) diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 37cb3e01c628..065472f9e949 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -174,13 +174,13 @@ en: wysiwyg: "Switch to WYSIWYG editor" macro: attribute_reference: - macro_help_tooltip: "This text segment is being dynamically rendered by a macro." aria_label_resource_link: "A dynamic link to a %{resource} placed using a macro" aria_label_work_package_attribute: "A dynamic work package attribute placed using a macro" aria_label_work_package_link: "A dynamic link to a work package placed using a macro" - not_found: "Requested resource could not be found" - nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." + macro_help_tooltip: "This text segment is being dynamically rendered by a macro." + nested_macro: "This macro is recursively referencing %{model} %{id}." + not_found: "Requested resource could not be found" child_pages: button: "List of sub-pages" include_parent: "Include parent" diff --git a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb index 031b22857cd6..f5ddf17001a6 100644 --- a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb +++ b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb @@ -55,4 +55,3 @@ expect(page).to have_no_css(".op-inline-macro[title]") end end - diff --git a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb index a1c83e718e28..03526ec77344 100644 --- a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb @@ -263,7 +263,9 @@ expect(subject).to be_html_eql("

#{link_to(message1.subject, project_forum_topic_path(project, forum, message1), class: 'message op-uc-link', - aria: { label: "A dynamic link to a message placed using a macro" }, + aria: { + label: 'A dynamic link to a message placed using a macro' + }, target: '_top')}

") } end @@ -396,7 +398,9 @@ it { expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', - aria: { label: "A dynamic link to a project placed using a macro" }, + aria: { + label: 'A dynamic link to a project placed using a macro' + }, class: 'project op-uc-link')}

") } end @@ -407,7 +411,9 @@ it { expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', - aria: { label: "A dynamic link to a project placed using a macro" }, + aria: { + label: 'A dynamic link to a project placed using a macro' + }, class: 'project op-uc-link')}

") } end @@ -418,7 +424,9 @@ it { expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', - aria: { label: "A dynamic link to a project placed using a macro" }, + aria: { + label: 'A dynamic link to a project placed using a macro' + }, class: 'project op-uc-link')}

") } end @@ -615,10 +623,10 @@ def source_url_with_ext(**) entry_revision_project_repository_path(project_id: identifier, repo_path: "some/file.ext", **) end - def resource_link_to(resource, name, url, **options) + def resource_link_to(resource, name, url, **) link_to(name, url, - **options, + **, aria: { label: "A dynamic link to a #{resource} placed using a macro" }) end @@ -632,18 +640,20 @@ def resource_link_to(resource, name, url, **options) @to_test = { # source - "source:/some/file" => resource_link_to("source", "source:/some/file", source_url, class: "source op-uc-link", target: "_top"), - "source:/some/file." => resource_link_to("source", "source:/some/file", source_url, class: "source op-uc-link", target: "_top") + ".", - 'source:"/some/file.ext".' => resource_link_to("source", "source:/some/file.ext", source_url_with_ext, - class: "source op-uc-link", target: "_top") + ".", - "source:/some/file. " => resource_link_to("source", "source:/some/file", source_url, - class: "source op-uc-link", target: "_top") + ".", - 'source:"/some/file.ext". ' => resource_link_to("source", "source:/some/file.ext", source_url_with_ext, - class: "source op-uc-link", target: "_top") + ".", - "source:/some/file, " => resource_link_to("source", "source:/some/file", source_url, - class: "source op-uc-link", target: "_top") + ",", + "source:/some/file" => resource_link_to("source", "source:/some/file", source_url, + class: "source op-uc-link", target: "_top"), + "source:/some/file." => "#{resource_link_to('source', 'source:/some/file', source_url, + class: 'source op-uc-link', target: '_top')}.", + 'source:"/some/file.ext".' => "#{resource_link_to('source', 'source:/some/file.ext', source_url_with_ext, + class: 'source op-uc-link', target: '_top')}.", + "source:/some/file. " => "#{resource_link_to('source', 'source:/some/file', source_url, + class: 'source op-uc-link', target: '_top')}.", + 'source:"/some/file.ext". ' => "#{resource_link_to('source', 'source:/some/file.ext', source_url_with_ext, + class: 'source op-uc-link', target: '_top')}.", + "source:/some/file, " => "#{resource_link_to('source', 'source:/some/file', source_url, + class: 'source op-uc-link', target: '_top')},", "source:/some/file@52" => resource_link_to("source", "source:/some/file@52", source_url(rev: 52), - class: "source op-uc-link", target: "_top"), + class: "source op-uc-link", target: "_top"), 'source:"/some/file.ext@52"' => resource_link_to("source", "source:/some/file.ext@52", source_url_with_ext(rev: 52), class: "source op-uc-link", target: "_top"), 'source:"/some/file#L110"' => resource_link_to("source", "source:/some/file#L110", source_url(anchor: "L110"), @@ -655,7 +665,7 @@ def resource_link_to(resource, name, url, **options) source_url(rev: 52, anchor: "L110"), class: "source op-uc-link", target: "_top"), "export:/some/file" => resource_link_to("export", "export:/some/file", source_url(format: "raw"), - class: "source download op-uc-link", target: "_top"), + class: "source download op-uc-link", target: "_top"), # escaping "!source:/some/file" => "source:/some/file", # invalid expressions diff --git a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb index 51b6ef67d07d..bd866d6cd149 100644 --- a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb @@ -85,7 +85,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "A dynamic link to a user placed using a macro" }, + aria: { label: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -184,7 +184,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "A dynamic link to a user placed using a macro" }, + aria: { label: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -212,7 +212,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "A dynamic link to a user placed using a macro" }, + aria: { label: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -241,7 +241,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "A dynamic link to a user placed using a macro" }, + aria: { label: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -273,7 +273,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "A dynamic link to a user placed using a macro" }, + aria: { label: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -302,7 +302,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "A dynamic link to a user placed using a macro" }, + aria: { label: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { diff --git a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb index d76df8dd925a..94e4cc3a8bd6 100644 --- a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb +++ b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb @@ -172,19 +172,23 @@ wp = work_package.reload # Prepend "see " so Markly doesn't parse `##...` as an H2 ATX heading. rendered = format_text("see ###{wp.display_id} here") + macro = Nokogiri::HTML.fragment(rendered).at_css("opce-macro-wp-quickinfo") - expect(rendered).to include( - %() - ) + expect(macro["data-id"]).to eq(wp.id.to_s) + expect(macro["data-display-id"]).to eq(wp.display_id) + expect(macro["data-detailed"]).to eq("false") + expect(macro["aria-label"]).to eq("A dynamic link to a work package placed using a macro") end it "renders `###PROJ-N` as a detailed quickinfo macro element" do wp = work_package.reload rendered = format_text("see ####{wp.display_id} here") + macro = Nokogiri::HTML.fragment(rendered).at_css("opce-macro-wp-quickinfo") - expect(rendered).to include( - %() - ) + expect(macro["data-id"]).to eq(wp.id.to_s) + expect(macro["data-display-id"]).to eq(wp.display_id) + expect(macro["data-detailed"]).to eq("true") + expect(macro["aria-label"]).to eq("A dynamic link to a work package placed using a macro") end context "when the referenced work package does not exist" do From 78a7684a85e1eda38bc07533cb4eb66bb47850cd Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Fri, 3 Jul 2026 16:31:46 +0200 Subject: [PATCH 05/11] Fix failing tests --- modules/documents/spec/application_helper_spec.rb | 13 +++++++++++-- .../lib/open_project/markdown_formatting_spec.rb | 1 + .../lib/open_project/markdown_formatting_spec.rb | 1 + .../v3/repositories/revision_representer_spec.rb | 1 + spec/requests/api/v3/render_resource_spec.rb | 2 ++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/documents/spec/application_helper_spec.rb b/modules/documents/spec/application_helper_spec.rb index 20ff1a4b13f8..8004afc6e560 100644 --- a/modules/documents/spec/application_helper_spec.rb +++ b/modules/documents/spec/application_helper_spec.rb @@ -67,6 +67,7 @@ link_to("Test document", { controller: "documents", action: "show", id: document.id }, class: "document op-uc-link", + aria: { label: "A dynamic link to a document placed using a macro" }, target: "_top") end @@ -108,7 +109,11 @@ subject { format_text("#{identifier}:document##{document.id}", project: the_other_project) } it { - expect(subject).to be_html_eql("

Test document

") + expect(subject).to be_html_eql( + "

Test document

" + ) } end @@ -116,7 +121,11 @@ subject { format_text("#{identifier}:document:\"#{document.title}\"", project: the_other_project) } it { - expect(subject).to be_html_eql("

Test document

") + expect(subject).to be_html_eql( + "

Test document

" + ) } end diff --git a/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb b/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb index 39b92c04476f..4c52c6aae537 100644 --- a/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb +++ b/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb @@ -81,6 +81,7 @@ def controller "My document", { controller: "/documents", action: "show", id: document.id, only_path: true }, class: "document op-uc-link", + aria: { label: "A dynamic link to a document placed using a macro" }, target: "_top" ) end diff --git a/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb b/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb index 90f9fb337439..8f771ab9e4b7 100644 --- a/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb +++ b/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb @@ -85,6 +85,7 @@ def controller "Monthly coordination", { controller: "/meetings", action: "show", project_id: project.id, id: meeting.id, only_path: true }, class: "meeting op-uc-link", + aria: { label: "A dynamic link to a meeting placed using a macro" }, target: "_top" ) end diff --git a/spec/lib/api/v3/repositories/revision_representer_spec.rb b/spec/lib/api/v3/repositories/revision_representer_spec.rb index 3639313df79c..94af017bf5b3 100644 --- a/spec/lib/api/v3/repositories/revision_representer_spec.rb +++ b/spec/lib/api/v3/repositories/revision_representer_spec.rb @@ -100,6 +100,7 @@ "class=\"issue work_package\" " \ "data-hover-card-trigger-target=\"trigger\" " \ "data-hover-card-url=\"/work_packages/#{id}/hover_card\" " \ + "aria-label=\"A dynamic link to a work package placed using a macro\" " \ "href=\"/work_packages/#{id}\">" \ "##{id}" end diff --git a/spec/requests/api/v3/render_resource_spec.rb b/spec/requests/api/v3/render_resource_spec.rb index 0437988b8691..2a310dec51d8 100644 --- a/spec/requests/api/v3/render_resource_spec.rb +++ b/spec/requests/api/v3/render_resource_spec.rb @@ -95,6 +95,7 @@ ##{id}

@@ -190,6 +191,7 @@ #1

\n\n

with two lines.

HTML From 273a3fb7ed870662169446256af8952bba7c4263 Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Mon, 6 Jul 2026 11:26:44 +0200 Subject: [PATCH 06/11] Use aria-description instead of aria-label --- .../macros/attribute-label-macro.component.ts | 4 +-- .../macros/attribute-value-macro.component.ts | 4 +-- .../work-package-quickinfo-macro.component.ts | 4 +-- .../macros/work-package-quickinfo-macro.html | 1 + .../text_formatting/filters/mention_filter.rb | 10 +++--- .../matchers/attribute_macros.rb | 2 +- .../matchers/link_handlers/base.rb | 2 +- .../matchers/link_handlers/work_packages.rb | 2 +- .../documents/spec/application_helper_spec.rb | 6 ++-- .../open_project/markdown_formatting_spec.rb | 2 +- .../open_project/markdown_formatting_spec.rb | 2 +- ..._inline_page_link_macro_component.html.erb | 2 +- .../inline_page_link_macro_component.html.erb | 6 ++-- ...d_inline_page_link_macro_component_spec.rb | 4 +-- .../inline_page_link_macro_component_spec.rb | 5 +-- .../repositories/revision_representer_spec.rb | 2 +- .../filters/mention_filter_spec.rb | 8 ++--- .../markdown/attribute_macros_spec.rb | 14 ++++---- .../markdown/in_tool_links_spec.rb | 32 +++++++++---------- .../text_formatting/markdown/mentions_spec.rb | 20 ++++++------ .../link_handlers/work_packages_spec.rb | 4 +-- spec/requests/api/v3/render_resource_spec.rb | 4 +-- 22 files changed, 71 insertions(+), 69 deletions(-) diff --git a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts index f788a8f2945d..2987a3b88c4c 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts @@ -69,7 +69,7 @@ export class AttributeLabelMacroComponent implements OnInit { invalid_attribute: (attr:string) => this.I18n.t('js.editor.macro.attribute_reference.invalid_attribute', { name: attr }), }; - @HostBinding('attr.aria-label') hostAriaLabel:string|null = null; + @HostBinding('attr.aria-description') hostAriaDescription:string|null = null; // The loaded resource, required for help text resource:HalResource|null = null; @@ -88,7 +88,7 @@ export class AttributeLabelMacroComponent implements OnInit { const model = element.dataset.model as SupportedAttributeModels; const id = element.dataset.id!; const attributeName = element.dataset.attribute!; - this.hostAriaLabel = model === 'workPackage' ? this.text.aria_label : null; + this.hostAriaDescription = model === 'workPackage' ? this.text.aria_label : null; this.attributeScope = capitalize(model); void this.loadResourceAttribute(model, id, attributeName); diff --git a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts index 650d59e19c72..707548f072ad 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts @@ -74,7 +74,7 @@ export class AttributeValueMacroComponent implements OnInit { invalid_attribute: (attr:string) => this.I18n.t('js.editor.macro.attribute_reference.invalid_attribute', { name: attr }), }; - @HostBinding('attr.aria-label') hostAriaLabel:string|null = null; + @HostBinding('attr.aria-description') hostAriaDescription:string|null = null; resource:HalResource; @@ -86,7 +86,7 @@ export class AttributeValueMacroComponent implements OnInit { const id = element.dataset.id!; const attributeName = element.dataset.attribute!; element.classList.add(ATTRIBUTE_MACRO_CLASS); - this.hostAriaLabel = model === 'workPackage' ? this.text.aria_label : null; + this.hostAriaDescription = model === 'workPackage' ? this.text.aria_label : null; if (this.isNestedMacro(model, id, attributeName)) { const error = this.I18n.t('js.editor.macro.attribute_reference.nested_macro', { model, id }); diff --git a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts index 7b2bda703546..a03ec51c944c 100644 --- a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts @@ -26,7 +26,7 @@ // See COPYRIGHT and LICENSE files for more details. //++ Ng1FieldControlsWrapper, -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, Injector, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Injector, OnInit, inject } from '@angular/core'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @@ -69,8 +69,6 @@ export class WorkPackageQuickinfoMacroComponent implements OnInit { aria_label: this.I18n.t('js.editor.macro.attribute_reference.aria_label_work_package_link'), }; - @HostBinding('attr.aria-label') hostAriaLabel = this.text.aria_label; - /** Work package to be shown */ workPackage$:Observable; diff --git a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html index 46be0a05f2fd..3634b0a57ab7 100644 --- a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html +++ b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html @@ -10,6 +10,7 @@ fieldName="type" /> diff --git a/lib/open_project/text_formatting/filters/mention_filter.rb b/lib/open_project/text_formatting/filters/mention_filter.rb index 357945f02cc3..48b4ed3485c0 100644 --- a/lib/open_project/text_formatting/filters/mention_filter.rb +++ b/lib/open_project/text_formatting/filters/mention_filter.rb @@ -103,14 +103,14 @@ def user_mention(user) link_to_user(user, only_path: context[:only_path], class: "user-mention", - aria: { label: resource_link_aria_label("user") }) + aria: { description: resource_link_aria_label("user") }) end def group_mention(group) link_to_group(group, only_path: context[:only_path], class: "user-mention", - aria: { label: resource_link_aria_label("group") }) + aria: { description: resource_link_aria_label("group") }) end def work_package_mention(work_package, mention) @@ -138,7 +138,7 @@ def work_package_quickinfo(work_package, detailed:) display_id: work_package.display_id, detailed: }, aria: { - label: work_package_link_aria_label + description: work_package_link_aria_label } end @@ -151,7 +151,7 @@ def work_package_static_macro(work_package, detailed:) link_to(label, work_package_path_or_url(id: work_package.display_id, only_path: context[:only_path]), class: "issue work_package", - aria: { label: work_package_link_aria_label }) + aria: { description: work_package_link_aria_label }) end def work_package_link_aria_label @@ -167,7 +167,7 @@ def work_package_link(work_package) link_to(work_package.formatted_id, work_package_path_or_url(id: display_id, only_path: context[:only_path]), class: "issue work_package", - aria: { label: work_package_link_aria_label }, + aria: { description: work_package_link_aria_label }, data: { hover_card_trigger_target: "trigger", hover_card_url: hover_card_work_package_path(display_id) diff --git a/lib/open_project/text_formatting/matchers/attribute_macros.rb b/lib/open_project/text_formatting/matchers/attribute_macros.rb index 2a6100698e09..92212dd20251 100644 --- a/lib/open_project/text_formatting/matchers/attribute_macros.rb +++ b/lib/open_project/text_formatting/matchers/attribute_macros.rb @@ -95,7 +95,7 @@ def self.process_match(match, _matched_string, context) tag_options = { data: macro_attributes } if work_package_embed?(macro_attributes) tag_options[:aria] = { - label: I18n.t("js.editor.macro.attribute_reference.aria_label_work_package_attribute") + description: I18n.t("js.editor.macro.attribute_reference.aria_label_work_package_attribute") } end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/base.rb b/lib/open_project/text_formatting/matchers/link_handlers/base.rb index 77ef9aba62b0..bc0f8f8228e6 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/base.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/base.rb @@ -103,7 +103,7 @@ def controller; end def link_to(name = nil, options = nil, html_options = nil, &) html_options = (html_options || {}) .except(:title, "title") - .merge(aria: { label: resource_link_aria_label }) + .merge(aria: { description: resource_link_aria_label }) super(name, options, html_options, &) # rubocop:disable Style/SuperArguments end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb index 07e41cd428cf..119f8e94ee37 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb @@ -110,7 +110,7 @@ def render_work_package_macro(work_package:, fallback_id:, detailed: false) "", data: { id:, display_id:, detailed: }, aria: { - label: work_package_link_aria_label + description: work_package_link_aria_label } end diff --git a/modules/documents/spec/application_helper_spec.rb b/modules/documents/spec/application_helper_spec.rb index 8004afc6e560..e9badf9af10c 100644 --- a/modules/documents/spec/application_helper_spec.rb +++ b/modules/documents/spec/application_helper_spec.rb @@ -67,7 +67,7 @@ link_to("Test document", { controller: "documents", action: "show", id: document.id }, class: "document op-uc-link", - aria: { label: "A dynamic link to a document placed using a macro" }, + aria: { description: "A dynamic link to a document placed using a macro" }, target: "_top") end @@ -111,7 +111,7 @@ it { expect(subject).to be_html_eql( "

Test document

" ) } @@ -123,7 +123,7 @@ it { expect(subject).to be_html_eql( "

Test document

" ) } diff --git a/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb b/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb index 4c52c6aae537..d552898db6cb 100644 --- a/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb +++ b/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb @@ -81,7 +81,7 @@ def controller "My document", { controller: "/documents", action: "show", id: document.id, only_path: true }, class: "document op-uc-link", - aria: { label: "A dynamic link to a document placed using a macro" }, + aria: { description: "A dynamic link to a document placed using a macro" }, target: "_top" ) end diff --git a/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb b/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb index 8f771ab9e4b7..ce2fce25c83f 100644 --- a/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb +++ b/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb @@ -85,7 +85,7 @@ def controller "Monthly coordination", { controller: "/meetings", action: "show", project_id: project.id, id: meeting.id, only_path: true }, class: "meeting op-uc-link", - aria: { label: "A dynamic link to a meeting placed using a macro" }, + aria: { description: "A dynamic link to a meeting placed using a macro" }, target: "_top" ) end diff --git a/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb b/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb index f2fc07e5ea90..80bf6b5e1cc3 100644 --- a/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb +++ b/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb @@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= content_tag("turbo-frame", id: frame_id, src: frame_src, data: { provider_id:, page_identifier: identifier, type: "wiki-page-link" }) do %> - <%= render(OpPrimer::InlineMacroComponent.new("aria-label": t(".aria_label"))) do |component| %> + <%= render(OpPrimer::InlineMacroComponent.new("aria-description": t(".aria_label"))) do |component| %> <% component.with_leading_visual_icon(icon: :"op-file-doc") %> <%= render Primer::Beta::Spinner.new(size: :small, display: :flex, mr: 2) %> diff --git a/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb b/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb index 2f9f7a034835..aaae278eb072 100644 --- a/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb +++ b/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb @@ -28,11 +28,13 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= - render(OpPrimer::InlineMacroComponent.new("aria-label": t("wikis.page_links.aria_label"))) do |inline_macro| + render(OpPrimer::InlineMacroComponent.new) do |inline_macro| page_info_result.either( ->(page_info) do inline_macro.with_leading_visual_icon(icon: :"op-file-doc") - render(Primer::Beta::Link.new(href: page_info.href)) { page_info.title } + render(Primer::Beta::Link.new(href: page_info.href, "aria-description": t("wikis.page_links.aria_label"))) do + page_info.title + end end, ->(error) do inline_macro.with_leading_visual_icon(icon: :alert) diff --git a/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb index e7bfe9116622..e43bc129c8ba 100644 --- a/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb +++ b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb @@ -38,10 +38,10 @@ before { render_component } - it "renders the loading wiki page macro with an ARIA label and no title" do + it "renders the loading wiki page macro with an ARIA description and no title" do expect(page).to have_css("turbo-frame[data-type='wiki-page-link']") expect(page).to have_css( - ".op-inline-macro[aria-label='#{I18n.t('wikis.deferred_inline_page_link_macro_component.aria_label')}']" + ".op-inline-macro[aria-description='#{I18n.t('wikis.deferred_inline_page_link_macro_component.aria_label')}']" ) expect(page).to have_no_css(".op-inline-macro[title]") end diff --git a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb index f5ddf17001a6..1cf97ce96861 100644 --- a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb +++ b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb @@ -47,10 +47,11 @@ before { render_component } - it "renders a wiki page macro link with an ARIA label and no title" do + it "renders a wiki page macro link with an ARIA description and no title" do expect(page).to have_link(text: page_info.title, href: page_info.href) expect(page).to have_css( - ".op-inline-macro[aria-label='#{I18n.t('wikis.page_links.aria_label')}']" + ".op-inline-macro a[aria-description='#{I18n.t('wikis.page_links.aria_label')}']", + text: page_info.title ) expect(page).to have_no_css(".op-inline-macro[title]") end diff --git a/spec/lib/api/v3/repositories/revision_representer_spec.rb b/spec/lib/api/v3/repositories/revision_representer_spec.rb index 94af017bf5b3..403bfde3a8f8 100644 --- a/spec/lib/api/v3/repositories/revision_representer_spec.rb +++ b/spec/lib/api/v3/repositories/revision_representer_spec.rb @@ -100,7 +100,7 @@ "class=\"issue work_package\" " \ "data-hover-card-trigger-target=\"trigger\" " \ "data-hover-card-url=\"/work_packages/#{id}/hover_card\" " \ - "aria-label=\"A dynamic link to a work package placed using a macro\" " \ + "aria-description=\"A dynamic link to a work package placed using a macro\" " \ "href=\"/work_packages/#{id}\">" \ "##{id}" end diff --git a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb index 78a43738cbb6..c7fc5e48490e 100644 --- a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb +++ b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb @@ -64,7 +64,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(">##{work_package.id}<") expect(rendered).to include(%(href="/work_packages/#{work_package.id}")) expect(rendered).to include(%(data-hover-card-url="/work_packages/#{work_package.id}/hover_card")) - expect(rendered).to include(%(aria-label="A dynamic link to a work package placed using a macro")) + expect(rendered).to include(%(aria-description="A dynamic link to a work package placed using a macro")) expect(rendered).not_to include(" title=") end end @@ -103,7 +103,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(%(data-id="#{wp.id}")) expect(rendered).to include(%(data-display-id="#{wp.display_id}")) expect(rendered).to include(%(data-detailed="false")) - expect(rendered).to include(%(aria-label="A dynamic link to a work package placed using a macro")) + expect(rendered).to include(%(aria-description="A dynamic link to a work package placed using a macro")) end end @@ -122,7 +122,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(%(data-id="#{wp.id}")) expect(rendered).to include(%(data-display-id="#{wp.display_id}")) expect(rendered).to include(%(data-detailed="true")) - expect(rendered).to include(%(aria-label="A dynamic link to a work package placed using a macro")) + expect(rendered).to include(%(aria-description="A dynamic link to a work package placed using a macro")) end end @@ -278,7 +278,7 @@ def user_mention_tag(user) user = create(:user, member_with_roles: { project => role }) rendered = format_text(user_mention_tag(user)) - expect(rendered).to include(%(aria-label="A dynamic link to a user placed using a macro")) + expect(rendered).to include(%(aria-description="A dynamic link to a user placed using a macro")) expect(rendered).not_to include(" title=") end diff --git a/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb b/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb index 6a80d18112bd..5b558a02c41b 100644 --- a/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb @@ -61,16 +61,16 @@

- Inline reference to WP: + Inline reference to WP:

- Inline reference to WP by ID: + Inline reference to WP by ID:

- Inline reference to WP by ID with CF with a dot: + Inline reference to WP by ID with CF with a dot:

- Inline reference to WP by subject: + Inline reference to WP by subject:

Inline reference to project: @@ -113,13 +113,13 @@

- Inline reference to WP: + Inline reference to WP:

- Inline reference to WP by ID: + Inline reference to WP by ID:

- Inline reference to WP by subject: + Inline reference to WP by subject:

Inline reference to project: diff --git a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb index 03526ec77344..ddba26667b5d 100644 --- a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb @@ -84,14 +84,14 @@ link_to("r#{changeset1.revision}", { controller: "repositories", action: "revision", project_id: identifier, rev: changeset1.revision }, class: "changeset op-uc-link", - aria: { label: "A dynamic link to a revision placed using a macro" }, + aria: { description: "A dynamic link to a revision placed using a macro" }, target: "_top") end let(:changeset_link2) do link_to("r#{changeset2.revision}", { controller: "repositories", action: "revision", project_id: identifier, rev: changeset2.revision }, class: "changeset op-uc-link", - aria: { label: "A dynamic link to a revision placed using a macro" }, + aria: { description: "A dynamic link to a revision placed using a macro" }, target: "_top") end @@ -144,7 +144,7 @@ { controller: "versions", action: "show", id: version.id }, class: "version op-uc-link", target: "_top", - aria: { label: "A dynamic link to a version placed using a macro" }) + aria: { description: "A dynamic link to a version placed using a macro" }) end context "Link with version id" do @@ -201,7 +201,7 @@ "project plan with milestones", project_work_packages_path([query.project.id], query_id: query.id), class: "query op-uc-link", - aria: { label: "A dynamic link to a view placed using a macro" }, + aria: { description: "A dynamic link to a view placed using a macro" }, target: "_top" ) end @@ -225,7 +225,7 @@ "Work packages", project_work_packages_path([project.id]), class: "query op-uc-link", - aria: { label: "A dynamic link to a view placed using a macro" }, + aria: { description: "A dynamic link to a view placed using a macro" }, target: "_top" ) end @@ -264,7 +264,7 @@ project_forum_topic_path(project, forum, message1), class: 'message op-uc-link', aria: { - label: 'A dynamic link to a message placed using a macro' + description: 'A dynamic link to a message placed using a macro' }, target: '_top')}

") } @@ -277,7 +277,7 @@ link = link_to(message2.subject, project_forum_topic_path(project, forum, message1, anchor: "message-#{message2.id}", r: message2.id), class: "message op-uc-link", - aria: { label: "A dynamic link to a message placed using a macro" }, + aria: { description: "A dynamic link to a message placed using a macro" }, target: "_top") expect(subject).to be_html_eql("

#{link}

") } @@ -293,7 +293,7 @@ hover_card_url: hover_card_work_package_path(work_package.id) }, class: "issue work_package op-uc-link", - aria: { label: "A dynamic link to a work package placed using a macro" }, + aria: { description: "A dynamic link to a work package placed using a macro" }, target: "_top") end @@ -324,7 +324,7 @@ content_tag "opce-macro-wp-quickinfo", "", data: { id: "1234", display_id: "1234", detailed: "false" }, - aria: { label: "A dynamic link to a work package placed using a macro" } + aria: { description: "A dynamic link to a work package placed using a macro" } end subject { format_text("foo (bar ##1234)") } @@ -337,7 +337,7 @@ content_tag "opce-macro-wp-quickinfo", "", data: { id: "1234", display_id: "1234", detailed: "true" }, - aria: { label: "A dynamic link to a work package placed using a macro" } + aria: { description: "A dynamic link to a work package placed using a macro" } end subject { format_text("foo (bar ###1234)") } @@ -370,7 +370,7 @@ hover_card_url: hover_card_work_package_path(work_package.id) }, class: "issue work_package op-uc-link", - aria: { label: "A dynamic link to a work package placed using a macro" }, + aria: { description: "A dynamic link to a work package placed using a macro" }, target: "_top") end @@ -399,7 +399,7 @@ expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', aria: { - label: 'A dynamic link to a project placed using a macro' + description: 'A dynamic link to a project placed using a macro' }, class: 'project op-uc-link')}

") } @@ -412,7 +412,7 @@ expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', aria: { - label: 'A dynamic link to a project placed using a macro' + description: 'A dynamic link to a project placed using a macro' }, class: 'project op-uc-link')}

") } @@ -425,7 +425,7 @@ expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', aria: { - label: 'A dynamic link to a project placed using a macro' + description: 'A dynamic link to a project placed using a macro' }, class: 'project op-uc-link')}

") } @@ -627,7 +627,7 @@ def resource_link_to(resource, name, url, **) link_to(name, url, **, - aria: { label: "A dynamic link to a #{resource} placed using a macro" }) + aria: { description: "A dynamic link to a #{resource} placed using a macro" }) end before do @@ -709,7 +709,7 @@ def resource_link_to(resource, name, url, **) let(:expected) do <<~EXPECTED

CookBook documentation

-

##{work_package.id}

+

##{work_package.id}

           [[CookBook documentation]]
 
diff --git a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
index bd866d6cd149..58ab042e960d 100644
--- a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
+++ b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
@@ -85,7 +85,7 @@
                 

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: 'A dynamic link to a user placed using a macro' }, + aria: { description: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -184,7 +184,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: 'A dynamic link to a user placed using a macro' }, + aria: { description: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -212,7 +212,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: 'A dynamic link to a user placed using a macro' }, + aria: { description: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -241,7 +241,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: 'A dynamic link to a user placed using a macro' }, + aria: { description: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -273,7 +273,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: 'A dynamic link to a user placed using a macro' }, + aria: { description: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -302,7 +302,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: 'A dynamic link to a user placed using a macro' }, + aria: { description: 'A dynamic link to a user placed using a macro' }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -373,7 +373,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="/users/#{user.id}/hover_card" href="/users/#{user.id}" - aria-label="A dynamic link to a user placed using a macro">Foo Barrit + aria-description="A dynamic link to a user placed using a macro">Foo Barrit

EXPECTED end @@ -400,7 +400,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="http://openproject.org/users/#{user.id}/hover_card" href="http://openproject.org/users/#{user.id}" - aria-label="A dynamic link to a user placed using a macro">Foo Barrit + aria-description="A dynamic link to a user placed using a macro">Foo Barrit

EXPECTED end @@ -436,7 +436,7 @@ + aria-description="A dynamic link to a group placed using a macro"> #{linked_project_member_group.name}

@@ -482,7 +482,7 @@ + aria-description="A dynamic link to a group placed using a macro"> #{linked_project_member_group.name}

diff --git a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb index 94e4cc3a8bd6..ccab6c5a20e9 100644 --- a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb +++ b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb @@ -177,7 +177,7 @@ expect(macro["data-id"]).to eq(wp.id.to_s) expect(macro["data-display-id"]).to eq(wp.display_id) expect(macro["data-detailed"]).to eq("false") - expect(macro["aria-label"]).to eq("A dynamic link to a work package placed using a macro") + expect(macro["aria-description"]).to eq("A dynamic link to a work package placed using a macro") end it "renders `###PROJ-N` as a detailed quickinfo macro element" do @@ -188,7 +188,7 @@ expect(macro["data-id"]).to eq(wp.id.to_s) expect(macro["data-display-id"]).to eq(wp.display_id) expect(macro["data-detailed"]).to eq("true") - expect(macro["aria-label"]).to eq("A dynamic link to a work package placed using a macro") + expect(macro["aria-description"]).to eq("A dynamic link to a work package placed using a macro") end context "when the referenced work package does not exist" do diff --git a/spec/requests/api/v3/render_resource_spec.rb b/spec/requests/api/v3/render_resource_spec.rb index 2a310dec51d8..19487f47bc80 100644 --- a/spec/requests/api/v3/render_resource_spec.rb +++ b/spec/requests/api/v3/render_resource_spec.rb @@ -95,7 +95,7 @@ ##{id}

@@ -191,7 +191,7 @@ #1

\n\n

with two lines.

HTML From e8df6528c15cd0d6816beab0534af6f8ec48e10b Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Mon, 6 Jul 2026 12:14:10 +0200 Subject: [PATCH 07/11] Preserve visible link text in accessible names, retain revision context, move wiki labels onto links, and add hidden context for non-interactive macros --- config/locales/js-en.yml | 1 + .../macros/attribute-label-macro.component.ts | 8 ++-- .../fields/macros/attribute-label-macro.html | 3 ++ .../macros/attribute-value-macro.component.ts | 8 ++-- .../fields/macros/attribute-value-macro.html | 3 ++ .../work-package-quickinfo-macro.component.ts | 5 ++- .../macros/work-package-quickinfo-macro.html | 2 +- .../text_formatting/filters/mention_filter.rb | 21 +++++---- .../matchers/attribute_macros.rb | 9 +--- .../matchers/link_handlers/base.rb | 17 +++++-- .../matchers/link_handlers/colon_separator.rb | 3 +- .../matchers/link_handlers/revisions.rb | 3 +- .../matchers/link_handlers/work_packages.rb | 5 +-- .../documents/spec/application_helper_spec.rb | 6 +-- .../open_project/markdown_formatting_spec.rb | 2 +- .../open_project/markdown_formatting_spec.rb | 2 +- ..._inline_page_link_macro_component.html.erb | 2 +- .../inline_page_link_macro_component.html.erb | 7 ++- ...d_inline_page_link_macro_component_spec.rb | 5 +-- .../inline_page_link_macro_component_spec.rb | 4 +- .../repositories/revision_representer_spec.rb | 2 +- .../filters/mention_filter_spec.rb | 8 ++-- .../markdown/attribute_macros_spec.rb | 14 +++--- .../markdown/in_tool_links_spec.rb | 44 +++++++++++-------- .../text_formatting/markdown/mentions_spec.rb | 20 ++++----- .../link_handlers/work_packages_spec.rb | 4 +- spec/requests/api/v3/render_resource_spec.rb | 4 +- 27 files changed, 119 insertions(+), 93 deletions(-) diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 065472f9e949..c1ed4214a3a1 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -174,6 +174,7 @@ en: wysiwyg: "Switch to WYSIWYG editor" macro: attribute_reference: + aria_label_with_name: "%{name}. %{description}" aria_label_resource_link: "A dynamic link to a %{resource} placed using a macro" aria_label_work_package_attribute: "A dynamic work package attribute placed using a macro" aria_label_work_package_link: "A dynamic link to a work package placed using a macro" diff --git a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts index 2987a3b88c4c..964a5ba750c5 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts @@ -26,7 +26,7 @@ // See COPYRIGHT and LICENSE files for more details. //++ Ng1FieldControlsWrapper, -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, Injector, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Injector, OnInit, inject } from '@angular/core'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { @@ -69,7 +69,7 @@ export class AttributeLabelMacroComponent implements OnInit { invalid_attribute: (attr:string) => this.I18n.t('js.editor.macro.attribute_reference.invalid_attribute', { name: attr }), }; - @HostBinding('attr.aria-description') hostAriaDescription:string|null = null; + showAriaContext = false; // The loaded resource, required for help text resource:HalResource|null = null; @@ -88,7 +88,7 @@ export class AttributeLabelMacroComponent implements OnInit { const model = element.dataset.model as SupportedAttributeModels; const id = element.dataset.id!; const attributeName = element.dataset.attribute!; - this.hostAriaDescription = model === 'workPackage' ? this.text.aria_label : null; + this.showAriaContext = model === 'workPackage'; this.attributeScope = capitalize(model); void this.loadResourceAttribute(model, id, attributeName); @@ -109,7 +109,7 @@ export class AttributeLabelMacroComponent implements OnInit { } const schema = await this.schemaCache.ensureLoaded(this.resource); - this.attribute = schema.attributeFromLocalizedName(attributeName) || attributeName; + this.attribute = schema.attributeFromLocalizedName(attributeName) ?? attributeName; this.label = (schema[this.attribute] as IOPFieldSchema|undefined)?.name; if (!this.label) { diff --git a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.html b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.html index 78687e3c25a7..0a8c1d1b9ee0 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.html +++ b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.html @@ -1,5 +1,8 @@ @if (resource) { + @if (showAriaContext) { + + } } diff --git a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts index 707548f072ad..cd0b51df76d9 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts @@ -26,7 +26,7 @@ // See COPYRIGHT and LICENSE files for more details. //++ Ng1FieldControlsWrapper, -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, Injector, OnInit, ViewChild, inject } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Injector, OnInit, ViewChild, inject } from '@angular/core'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { @@ -74,7 +74,7 @@ export class AttributeValueMacroComponent implements OnInit { invalid_attribute: (attr:string) => this.I18n.t('js.editor.macro.attribute_reference.invalid_attribute', { name: attr }), }; - @HostBinding('attr.aria-description') hostAriaDescription:string|null = null; + showAriaContext = false; resource:HalResource; @@ -86,7 +86,7 @@ export class AttributeValueMacroComponent implements OnInit { const id = element.dataset.id!; const attributeName = element.dataset.attribute!; element.classList.add(ATTRIBUTE_MACRO_CLASS); - this.hostAriaDescription = model === 'workPackage' ? this.text.aria_label : null; + this.showAriaContext = model === 'workPackage'; if (this.isNestedMacro(model, id, attributeName)) { const error = this.I18n.t('js.editor.macro.attribute_reference.nested_macro', { model, id }); @@ -121,7 +121,7 @@ export class AttributeValueMacroComponent implements OnInit { const schema = await this.schemaCache.ensureLoaded(resource); const proxied = this.schemaCache.proxied(resource, schema); - const attribute = schema.attributeFromLocalizedName(attributeName) || this.dateAttribute(resource, proxied, attributeName); + const attribute = schema.attributeFromLocalizedName(attributeName) ?? this.dateAttribute(resource, proxied, attributeName); const fieldSchema = proxied.ofProperty(attribute) as IFieldSchema|undefined; if (fieldSchema) { diff --git a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.html b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.html index f7584cdd6a5e..3cced91e2db5 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.html +++ b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.html @@ -9,4 +9,7 @@ containerType="single-view" [displayFieldOptions]="{ writable: false }" [fieldName]="fieldName" /> + @if (showAriaContext) { + + } } diff --git a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts index a03ec51c944c..3b5087ecf40a 100644 --- a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.component.ts @@ -66,7 +66,10 @@ export class WorkPackageQuickinfoMacroComponent implements OnInit { text = { not_found: this.I18n.t('js.editor.macro.attribute_reference.not_found'), - aria_label: this.I18n.t('js.editor.macro.attribute_reference.aria_label_work_package_link'), + aria_label: (name:string) => this.I18n.t('js.editor.macro.attribute_reference.aria_label_with_name', { + name, + description: String(this.I18n.t('js.editor.macro.attribute_reference.aria_label_work_package_link')), + }), }; /** Work package to be shown */ diff --git a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html index 3634b0a57ab7..fbd6134af08c 100644 --- a/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html +++ b/frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html @@ -10,7 +10,7 @@ fieldName="type" /> diff --git a/lib/open_project/text_formatting/filters/mention_filter.rb b/lib/open_project/text_formatting/filters/mention_filter.rb index 48b4ed3485c0..dca2cb9162ab 100644 --- a/lib/open_project/text_formatting/filters/mention_filter.rb +++ b/lib/open_project/text_formatting/filters/mention_filter.rb @@ -103,14 +103,14 @@ def user_mention(user) link_to_user(user, only_path: context[:only_path], class: "user-mention", - aria: { description: resource_link_aria_label("user") }) + aria: { label: accessible_link_label(user.name, resource_link_aria_label("user")) }) end def group_mention(group) link_to_group(group, only_path: context[:only_path], class: "user-mention", - aria: { description: resource_link_aria_label("group") }) + aria: { label: accessible_link_label(group.name, resource_link_aria_label("group")) }) end def work_package_mention(work_package, mention) @@ -136,10 +136,7 @@ def work_package_quickinfo(work_package, detailed:) "", data: { id: work_package.id, display_id: work_package.display_id, - detailed: }, - aria: { - description: work_package_link_aria_label - } + detailed: } end # Uses the WP's current `formatted_id` rather than the envelope text, @@ -151,7 +148,7 @@ def work_package_static_macro(work_package, detailed:) link_to(label, work_package_path_or_url(id: work_package.display_id, only_path: context[:only_path]), class: "issue work_package", - aria: { description: work_package_link_aria_label }) + aria: { label: accessible_link_label(label, work_package_link_aria_label) }) end def work_package_link_aria_label @@ -162,12 +159,20 @@ def resource_link_aria_label(resource) I18n.t("js.editor.macro.attribute_reference.aria_label_resource_link", resource:) end + def accessible_link_label(name, description) + visible_name = Nokogiri::HTML.fragment(name.to_s).text.squish + plain_description = Nokogiri::HTML.fragment(description.to_s).text.squish + I18n.t("js.editor.macro.attribute_reference.aria_label_with_name", + name: visible_name, + description: plain_description) + end + def work_package_link(work_package) display_id = work_package.display_id link_to(work_package.formatted_id, work_package_path_or_url(id: display_id, only_path: context[:only_path]), class: "issue work_package", - aria: { description: work_package_link_aria_label }, + aria: { label: accessible_link_label(work_package.formatted_id, work_package_link_aria_label) }, data: { hover_card_trigger_target: "trigger", hover_card_url: hover_card_work_package_path(display_id) diff --git a/lib/open_project/text_formatting/matchers/attribute_macros.rb b/lib/open_project/text_formatting/matchers/attribute_macros.rb index 92212dd20251..c78055ded2b8 100644 --- a/lib/open_project/text_formatting/matchers/attribute_macros.rb +++ b/lib/open_project/text_formatting/matchers/attribute_macros.rb @@ -92,16 +92,9 @@ def self.process_match(match, _matched_string, context) macro_attributes[:id] = relative_id(macro_attributes, context) if relative_embed?(macro_attributes) - tag_options = { data: macro_attributes } - if work_package_embed?(macro_attributes) - tag_options[:aria] = { - description: I18n.t("js.editor.macro.attribute_reference.aria_label_work_package_attribute") - } - end - ApplicationController.helpers.content_tag "opce-macro-attribute-#{type}", "", - **tag_options + data: macro_attributes end end end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/base.rb b/lib/open_project/text_formatting/matchers/link_handlers/base.rb index bc0f8f8228e6..4b465b586f83 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/base.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/base.rb @@ -101,17 +101,26 @@ def controller; end private def link_to(name = nil, options = nil, html_options = nil, &) - html_options = (html_options || {}) - .except(:title, "title") - .merge(aria: { description: resource_link_aria_label }) + html_options ||= {} + title = html_options.delete(:title) || html_options.delete("title") + description = [resource_link_aria_label, title].compact.join(". ") + aria = (html_options[:aria] || {}).merge(label: accessible_link_label(name, description)) - super(name, options, html_options, &) # rubocop:disable Style/SuperArguments + super(name, options, html_options.merge(aria:), &) end def resource_link_aria_label resource = matcher.prefix.presence || (matcher.sep == "r" ? "revision" : "resource") I18n.t("js.editor.macro.attribute_reference.aria_label_resource_link", resource:) end + + def accessible_link_label(name, description) + visible_name = Nokogiri::HTML.fragment(name.to_s).text.squish + plain_description = Nokogiri::HTML.fragment(description.to_s).text.squish + I18n.t("js.editor.macro.attribute_reference.aria_label_with_name", + name: visible_name, + description: plain_description) + end end end end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb b/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb index 8f28caa235ad..71bd7526928f 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb @@ -95,7 +95,8 @@ def render_commit # rubocop:disable Metrics/AbcSize link_to h("#{matcher.project_prefix}#{matcher.identifier}"), { only_path: context[:only_path], controller: "/repositories", action: "revision", project_id: project, rev: changeset.identifier }, - class: "changeset" + class: "changeset", + title: truncate_single_line(changeset.comments, length: 100) end end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb b/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb index 276e76d3eda3..d031c94f1b67 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/revisions.rb @@ -53,7 +53,8 @@ def call # rubocop:disable Metrics/AbcSize link_to(h("#{matcher.project_prefix}r#{matcher.identifier}"), { only_path: context[:only_path], controller: "/repositories", action: "revision", project_id: project, rev: changeset.revision }, - class: "changeset") + class: "changeset", + title: truncate_single_line(changeset.comments, length: 100)) end end end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb index 119f8e94ee37..ce8a5b29ca40 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb @@ -108,10 +108,7 @@ def render_work_package_macro(work_package:, fallback_id:, detailed: false) ApplicationController.helpers.content_tag "opce-macro-wp-quickinfo", "", - data: { id:, display_id:, detailed: }, - aria: { - description: work_package_link_aria_label - } + data: { id:, display_id:, detailed: } end # The label keeps what the author wrote (possibly a historical diff --git a/modules/documents/spec/application_helper_spec.rb b/modules/documents/spec/application_helper_spec.rb index e9badf9af10c..0fa704d99ee5 100644 --- a/modules/documents/spec/application_helper_spec.rb +++ b/modules/documents/spec/application_helper_spec.rb @@ -67,7 +67,7 @@ link_to("Test document", { controller: "documents", action: "show", id: document.id }, class: "document op-uc-link", - aria: { description: "A dynamic link to a document placed using a macro" }, + aria: { label: "Test document. A dynamic link to a document placed using a macro" }, target: "_top") end @@ -111,7 +111,7 @@ it { expect(subject).to be_html_eql( "

Test document

" ) } @@ -123,7 +123,7 @@ it { expect(subject).to be_html_eql( "

Test document

" ) } diff --git a/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb b/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb index d552898db6cb..600a19617edc 100644 --- a/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb +++ b/modules/documents/spec/lib/open_project/markdown_formatting_spec.rb @@ -81,7 +81,7 @@ def controller "My document", { controller: "/documents", action: "show", id: document.id, only_path: true }, class: "document op-uc-link", - aria: { description: "A dynamic link to a document placed using a macro" }, + aria: { label: "My document. A dynamic link to a document placed using a macro" }, target: "_top" ) end diff --git a/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb b/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb index ce2fce25c83f..1964e7eb19dd 100644 --- a/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb +++ b/modules/meeting/spec/lib/open_project/markdown_formatting_spec.rb @@ -85,7 +85,7 @@ def controller "Monthly coordination", { controller: "/meetings", action: "show", project_id: project.id, id: meeting.id, only_path: true }, class: "meeting op-uc-link", - aria: { description: "A dynamic link to a meeting placed using a macro" }, + aria: { label: "Monthly coordination. A dynamic link to a meeting placed using a macro" }, target: "_top" ) end diff --git a/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb b/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb index 80bf6b5e1cc3..c79be6ac8944 100644 --- a/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb +++ b/modules/wikis/app/components/wikis/deferred_inline_page_link_macro_component.html.erb @@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= content_tag("turbo-frame", id: frame_id, src: frame_src, data: { provider_id:, page_identifier: identifier, type: "wiki-page-link" }) do %> - <%= render(OpPrimer::InlineMacroComponent.new("aria-description": t(".aria_label"))) do |component| %> + <%= render(OpPrimer::InlineMacroComponent.new) do |component| %> <% component.with_leading_visual_icon(icon: :"op-file-doc") %> <%= render Primer::Beta::Spinner.new(size: :small, display: :flex, mr: 2) %> diff --git a/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb b/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb index aaae278eb072..55e44d9b0602 100644 --- a/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb +++ b/modules/wikis/app/components/wikis/inline_page_link_macro_component.html.erb @@ -32,7 +32,12 @@ See COPYRIGHT and LICENSE files for more details. page_info_result.either( ->(page_info) do inline_macro.with_leading_visual_icon(icon: :"op-file-doc") - render(Primer::Beta::Link.new(href: page_info.href, "aria-description": t("wikis.page_links.aria_label"))) do + aria_label = t( + "js.editor.macro.attribute_reference.aria_label_with_name", + name: page_info.title, + description: t("wikis.page_links.aria_label") + ) + render(Primer::Beta::Link.new(href: page_info.href, "aria-label": aria_label)) do page_info.title end end, diff --git a/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb index e43bc129c8ba..b531b9591f6e 100644 --- a/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb +++ b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb @@ -38,11 +38,8 @@ before { render_component } - it "renders the loading wiki page macro with an ARIA description and no title" do + it "renders the loading wiki page macro without a title" do expect(page).to have_css("turbo-frame[data-type='wiki-page-link']") - expect(page).to have_css( - ".op-inline-macro[aria-description='#{I18n.t('wikis.deferred_inline_page_link_macro_component.aria_label')}']" - ) expect(page).to have_no_css(".op-inline-macro[title]") end end diff --git a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb index 1cf97ce96861..42d3786fff34 100644 --- a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb +++ b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb @@ -47,10 +47,10 @@ before { render_component } - it "renders a wiki page macro link with an ARIA description and no title" do + it "renders a wiki page macro link with an ARIA label and no title" do expect(page).to have_link(text: page_info.title, href: page_info.href) expect(page).to have_css( - ".op-inline-macro a[aria-description='#{I18n.t('wikis.page_links.aria_label')}']", + ".op-inline-macro a[aria-label='#{page_info.title}. #{I18n.t('wikis.page_links.aria_label')}']", text: page_info.title ) expect(page).to have_no_css(".op-inline-macro[title]") diff --git a/spec/lib/api/v3/repositories/revision_representer_spec.rb b/spec/lib/api/v3/repositories/revision_representer_spec.rb index 403bfde3a8f8..3900d5613b1b 100644 --- a/spec/lib/api/v3/repositories/revision_representer_spec.rb +++ b/spec/lib/api/v3/repositories/revision_representer_spec.rb @@ -100,7 +100,7 @@ "class=\"issue work_package\" " \ "data-hover-card-trigger-target=\"trigger\" " \ "data-hover-card-url=\"/work_packages/#{id}/hover_card\" " \ - "aria-description=\"A dynamic link to a work package placed using a macro\" " \ + "aria-label=\"##{id}. A dynamic link to a work package placed using a macro\" " \ "href=\"/work_packages/#{id}\">" \ "##{id}" end diff --git a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb index c7fc5e48490e..974e441dd209 100644 --- a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb +++ b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb @@ -64,7 +64,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(">##{work_package.id}<") expect(rendered).to include(%(href="/work_packages/#{work_package.id}")) expect(rendered).to include(%(data-hover-card-url="/work_packages/#{work_package.id}/hover_card")) - expect(rendered).to include(%(aria-description="A dynamic link to a work package placed using a macro")) + expect(rendered).to include(%(aria-label="##{work_package.id}. A dynamic link to a work package placed using a macro")) expect(rendered).not_to include(" title=") end end @@ -103,7 +103,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(%(data-id="#{wp.id}")) expect(rendered).to include(%(data-display-id="#{wp.display_id}")) expect(rendered).to include(%(data-detailed="false")) - expect(rendered).to include(%(aria-description="A dynamic link to a work package placed using a macro")) + expect(rendered).not_to include("aria-label") end end @@ -122,7 +122,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(%(data-id="#{wp.id}")) expect(rendered).to include(%(data-display-id="#{wp.display_id}")) expect(rendered).to include(%(data-detailed="true")) - expect(rendered).to include(%(aria-description="A dynamic link to a work package placed using a macro")) + expect(rendered).not_to include("aria-label") end end @@ -278,7 +278,7 @@ def user_mention_tag(user) user = create(:user, member_with_roles: { project => role }) rendered = format_text(user_mention_tag(user)) - expect(rendered).to include(%(aria-description="A dynamic link to a user placed using a macro")) + expect(rendered).to include(%(aria-label="#{user.name}. A dynamic link to a user placed using a macro")) expect(rendered).not_to include(" title=") end diff --git a/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb b/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb index 5b558a02c41b..c3fb8a5c8286 100644 --- a/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/attribute_macros_spec.rb @@ -61,16 +61,16 @@

- Inline reference to WP: + Inline reference to WP:

- Inline reference to WP by ID: + Inline reference to WP by ID:

- Inline reference to WP by ID with CF with a dot: + Inline reference to WP by ID with CF with a dot:

- Inline reference to WP by subject: + Inline reference to WP by subject:

Inline reference to project: @@ -113,13 +113,13 @@

- Inline reference to WP: + Inline reference to WP:

- Inline reference to WP by ID: + Inline reference to WP by ID:

- Inline reference to WP by subject: + Inline reference to WP by subject:

Inline reference to project: diff --git a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb index ddba26667b5d..a1b302143a54 100644 --- a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb @@ -84,14 +84,20 @@ link_to("r#{changeset1.revision}", { controller: "repositories", action: "revision", project_id: identifier, rev: changeset1.revision }, class: "changeset op-uc-link", - aria: { description: "A dynamic link to a revision placed using a macro" }, + aria: { + label: "r#{changeset1.revision}. A dynamic link to a revision placed using a macro. " \ + "#{changeset1.comments}" + }, target: "_top") end let(:changeset_link2) do link_to("r#{changeset2.revision}", { controller: "repositories", action: "revision", project_id: identifier, rev: changeset2.revision }, class: "changeset op-uc-link", - aria: { description: "A dynamic link to a revision placed using a macro" }, + aria: { + label: "r#{changeset2.revision}. A dynamic link to a revision placed using a macro. " \ + "#{changeset2.comments}" + }, target: "_top") end @@ -144,7 +150,7 @@ { controller: "versions", action: "show", id: version.id }, class: "version op-uc-link", target: "_top", - aria: { description: "A dynamic link to a version placed using a macro" }) + aria: { label: "1.0. A dynamic link to a version placed using a macro" }) end context "Link with version id" do @@ -201,7 +207,7 @@ "project plan with milestones", project_work_packages_path([query.project.id], query_id: query.id), class: "query op-uc-link", - aria: { description: "A dynamic link to a view placed using a macro" }, + aria: { label: "project plan with milestones. A dynamic link to a view placed using a macro" }, target: "_top" ) end @@ -225,7 +231,7 @@ "Work packages", project_work_packages_path([project.id]), class: "query op-uc-link", - aria: { description: "A dynamic link to a view placed using a macro" }, + aria: { label: "Work packages. A dynamic link to a view placed using a macro" }, target: "_top" ) end @@ -264,7 +270,8 @@ project_forum_topic_path(project, forum, message1), class: 'message op-uc-link', aria: { - description: 'A dynamic link to a message placed using a macro' + label: "#{message1.subject}. " \ + 'A dynamic link to a message placed using a macro' }, target: '_top')}

") } @@ -277,7 +284,7 @@ link = link_to(message2.subject, project_forum_topic_path(project, forum, message1, anchor: "message-#{message2.id}", r: message2.id), class: "message op-uc-link", - aria: { description: "A dynamic link to a message placed using a macro" }, + aria: { label: "#{message2.subject}. A dynamic link to a message placed using a macro" }, target: "_top") expect(subject).to be_html_eql("

#{link}

") } @@ -293,7 +300,7 @@ hover_card_url: hover_card_work_package_path(work_package.id) }, class: "issue work_package op-uc-link", - aria: { description: "A dynamic link to a work package placed using a macro" }, + aria: { label: "##{work_package.id}. A dynamic link to a work package placed using a macro" }, target: "_top") end @@ -323,8 +330,7 @@ let(:work_package_link) do content_tag "opce-macro-wp-quickinfo", "", - data: { id: "1234", display_id: "1234", detailed: "false" }, - aria: { description: "A dynamic link to a work package placed using a macro" } + data: { id: "1234", display_id: "1234", detailed: "false" } end subject { format_text("foo (bar ##1234)") } @@ -336,8 +342,7 @@ let(:work_package_link) do content_tag "opce-macro-wp-quickinfo", "", - data: { id: "1234", display_id: "1234", detailed: "true" }, - aria: { description: "A dynamic link to a work package placed using a macro" } + data: { id: "1234", display_id: "1234", detailed: "true" } end subject { format_text("foo (bar ###1234)") } @@ -370,7 +375,7 @@ hover_card_url: hover_card_work_package_path(work_package.id) }, class: "issue work_package op-uc-link", - aria: { description: "A dynamic link to a work package placed using a macro" }, + aria: { label: "##{work_package.id}. A dynamic link to a work package placed using a macro" }, target: "_top") end @@ -399,7 +404,8 @@ expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', aria: { - description: 'A dynamic link to a project placed using a macro' + label: "#{subproject.name}. " \ + 'A dynamic link to a project placed using a macro' }, class: 'project op-uc-link')}

") } @@ -412,7 +418,8 @@ expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', aria: { - description: 'A dynamic link to a project placed using a macro' + label: "#{subproject.name}. " \ + 'A dynamic link to a project placed using a macro' }, class: 'project op-uc-link')}

") } @@ -425,7 +432,8 @@ expect(subject).to be_html_eql("

#{link_to(subproject.name, project_url, target: '_top', aria: { - description: 'A dynamic link to a project placed using a macro' + label: "#{subproject.name}. " \ + 'A dynamic link to a project placed using a macro' }, class: 'project op-uc-link')}

") } @@ -627,7 +635,7 @@ def resource_link_to(resource, name, url, **) link_to(name, url, **, - aria: { description: "A dynamic link to a #{resource} placed using a macro" }) + aria: { label: "#{name}. A dynamic link to a #{resource} placed using a macro" }) end before do @@ -709,7 +717,7 @@ def resource_link_to(resource, name, url, **) let(:expected) do <<~EXPECTED

CookBook documentation

-

##{work_package.id}

+

##{work_package.id}

           [[CookBook documentation]]
 
diff --git a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
index 58ab042e960d..5a5fa25d2bb7 100644
--- a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
+++ b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb
@@ -85,7 +85,7 @@
                 

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { description: 'A dynamic link to a user placed using a macro' }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -184,7 +184,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { description: 'A dynamic link to a user placed using a macro' }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -212,7 +212,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { description: 'A dynamic link to a user placed using a macro' }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -241,7 +241,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { description: 'A dynamic link to a user placed using a macro' }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -273,7 +273,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { description: 'A dynamic link to a user placed using a macro' }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -302,7 +302,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { description: 'A dynamic link to a user placed using a macro' }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -373,7 +373,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="/users/#{user.id}/hover_card" href="/users/#{user.id}" - aria-description="A dynamic link to a user placed using a macro">Foo Barrit + aria-label="Foo Barrit. A dynamic link to a user placed using a macro. User Foo Barrit">Foo Barrit

EXPECTED end @@ -400,7 +400,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="http://openproject.org/users/#{user.id}/hover_card" href="http://openproject.org/users/#{user.id}" - aria-description="A dynamic link to a user placed using a macro">Foo Barrit + aria-label="Foo Barrit. A dynamic link to a user placed using a macro. User Foo Barrit">Foo Barrit

EXPECTED end @@ -436,7 +436,7 @@ + aria-label="#{linked_project_member_group.name}. A dynamic link to a group placed using a macro. Group #{linked_project_member_group.name}"> #{linked_project_member_group.name}

@@ -482,7 +482,7 @@ + aria-label="#{linked_project_member_group.name}. A dynamic link to a group placed using a macro"> #{linked_project_member_group.name}

diff --git a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb index ccab6c5a20e9..0c09f8014c23 100644 --- a/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb +++ b/spec/lib/open_project/text_formatting/matchers/link_handlers/work_packages_spec.rb @@ -177,7 +177,7 @@ expect(macro["data-id"]).to eq(wp.id.to_s) expect(macro["data-display-id"]).to eq(wp.display_id) expect(macro["data-detailed"]).to eq("false") - expect(macro["aria-description"]).to eq("A dynamic link to a work package placed using a macro") + expect(macro["aria-label"]).to be_nil end it "renders `###PROJ-N` as a detailed quickinfo macro element" do @@ -188,7 +188,7 @@ expect(macro["data-id"]).to eq(wp.id.to_s) expect(macro["data-display-id"]).to eq(wp.display_id) expect(macro["data-detailed"]).to eq("true") - expect(macro["aria-description"]).to eq("A dynamic link to a work package placed using a macro") + expect(macro["aria-label"]).to be_nil end context "when the referenced work package does not exist" do diff --git a/spec/requests/api/v3/render_resource_spec.rb b/spec/requests/api/v3/render_resource_spec.rb index 19487f47bc80..eac91ccb6928 100644 --- a/spec/requests/api/v3/render_resource_spec.rb +++ b/spec/requests/api/v3/render_resource_spec.rb @@ -95,7 +95,7 @@ ##{id}

@@ -191,7 +191,7 @@ #1

\n\n

with two lines.

HTML From 88bd22d32b44111e1c3e4046882797858e86ec14 Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Mon, 6 Jul 2026 13:01:54 +0200 Subject: [PATCH 08/11] Fix redundant ARIA labels for user and group links --- lib/open_project/object_linking.rb | 4 ++-- .../matchers/link_handlers/colon_separator.rb | 2 +- .../matchers/link_handlers/hash_separator.rb | 6 ++++-- modules/wikis/config/locales/en.yml | 1 - .../text_formatting/markdown/mentions_spec.rb | 16 ++++++++-------- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/open_project/object_linking.rb b/lib/open_project/object_linking.rb index a130d27cd3ac..3959abdf1125 100644 --- a/lib/open_project/object_linking.rb +++ b/lib/open_project/object_linking.rb @@ -49,7 +49,7 @@ def link_to_user(user, options = {}) # rubocop:disable Metrics/AbcSize only_path = options.delete(:only_path) { true } name = options.delete(:name) { user.name } - options[:title] ||= I18n.t(:label_user_named, name:) + options[:title] = I18n.t(:label_user_named, name:) unless options.key?(:title) add_hover_card_options(user, options, only_path:) @@ -63,7 +63,7 @@ def link_to_group(group, options = {}) name = group.name href = show_group_url(group, only_path: options.delete(:only_path) { true }) - options[:title] ||= I18n.t(:label_group_named, name:) + options[:title] = I18n.t(:label_group_named, name:) unless options.key?(:title) link_to(name, href, options) end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb b/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb index 71bd7526928f..6615b0fbc781 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb @@ -139,7 +139,7 @@ def render_project def render_user if (user = User.visible.find_by(login: oid)) - link_to_user(user, only_path: context[:only_path], class: "user-mention") + link_to_user(user, only_path: context[:only_path], class: "user-mention", title: nil) end end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/hash_separator.rb b/lib/open_project/text_formatting/matchers/link_handlers/hash_separator.rb index 1d3555bc005f..79a460f53eca 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/hash_separator.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/hash_separator.rb @@ -109,7 +109,8 @@ def render_user if user link_to_user(user, only_path: context[:only_path], - class: "user-mention") + class: "user-mention", + title: nil) end end @@ -119,7 +120,8 @@ def render_group if group link_to_group(group, only_path: context[:only_path], - class: "user-mention") + class: "user-mention", + title: nil) end end diff --git a/modules/wikis/config/locales/en.yml b/modules/wikis/config/locales/en.yml index f751d9802e39..ab23fef8e439 100644 --- a/modules/wikis/config/locales/en.yml +++ b/modules/wikis/config/locales/en.yml @@ -132,7 +132,6 @@ en: create_new_wiki_page_dialog: title: Create new wiki page deferred_inline_page_link_macro_component: - aria_label: A dynamic link to a wiki page placed using a macro loading: Loading… delete_relation_page_link_confirmation_dialog: heading: Delete related wiki page link? diff --git a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb index 5a5fa25d2bb7..d778242dd48c 100644 --- a/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/mentions_spec.rb @@ -184,7 +184,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -212,7 +212,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -241,7 +241,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -273,7 +273,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -302,7 +302,7 @@

#{link_to(linked_project_member.name, { controller: :users, action: :show, id: linked_project_member.id }, - aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro. User #{linked_project_member.name}" }, + aria: { label: "#{linked_project_member.name}. A dynamic link to a user placed using a macro" }, class: 'user-mention op-uc-link', target: '_top', data: { @@ -373,7 +373,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="/users/#{user.id}/hover_card" href="/users/#{user.id}" - aria-label="Foo Barrit. A dynamic link to a user placed using a macro. User Foo Barrit">Foo Barrit + aria-label="Foo Barrit. A dynamic link to a user placed using a macro">Foo Barrit

EXPECTED end @@ -400,7 +400,7 @@ data-hover-card-trigger-target="trigger" data-hover-card-url="http://openproject.org/users/#{user.id}/hover_card" href="http://openproject.org/users/#{user.id}" - aria-label="Foo Barrit. A dynamic link to a user placed using a macro. User Foo Barrit">Foo Barrit + aria-label="Foo Barrit. A dynamic link to a user placed using a macro">Foo Barrit

EXPECTED end @@ -436,7 +436,7 @@ + aria-label="#{linked_project_member_group.name}. A dynamic link to a group placed using a macro"> #{linked_project_member_group.name}

From 2aa57e1d9d5c691605f77e0ea0773378f90c91c7 Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Mon, 6 Jul 2026 14:02:26 +0200 Subject: [PATCH 09/11] Update feature specs --- config/locales/js-en.yml | 2 +- ...d_inline_page_link_macro_component_spec.rb | 27 +++++++-- .../inline_page_link_macro_component_spec.rb | 58 ++++++++++++++++--- .../filters/mention_filter_spec.rb | 4 -- 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index c1ed4214a3a1..3f2c13ebe566 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -174,8 +174,8 @@ en: wysiwyg: "Switch to WYSIWYG editor" macro: attribute_reference: - aria_label_with_name: "%{name}. %{description}" aria_label_resource_link: "A dynamic link to a %{resource} placed using a macro" + aria_label_with_name: "%{name}. %{description}" aria_label_work_package_attribute: "A dynamic work package attribute placed using a macro" aria_label_work_package_link: "A dynamic link to a work package placed using a macro" invalid_attribute: "The selected attribute '%{name}' does not exist." diff --git a/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb index b531b9591f6e..059971e30da2 100644 --- a/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb +++ b/modules/wikis/spec/components/wikis/deferred_inline_page_link_macro_component_spec.rb @@ -32,14 +32,29 @@ require_module_spec_helper RSpec.describe Wikis::DeferredInlinePageLinkMacroComponent, type: :component do - subject(:render_component) do - render_inline(described_class.new(identifier: "1234", provider_id: "42")) - end + subject(:render_component) { render_inline(component) } + + let(:identifier) { "1234" } + let(:provider_id) { "42" } + let(:component) { described_class.new(identifier:, provider_id:) } before { render_component } - it "renders the loading wiki page macro without a title" do - expect(page).to have_css("turbo-frame[data-type='wiki-page-link']") - expect(page).to have_no_css(".op-inline-macro[title]") + it "renders a frame that loads the requested wiki page link" do + frame = page.find("turbo-frame[data-type='wiki-page-link']") + + expect(frame[:id]).to eq(component.frame_id) + expect(frame[:src]).to eq(component.frame_src) + expect(frame["data-provider-id"]).to eq(provider_id) + expect(frame["data-page-identifier"]).to eq(identifier) + end + + it "renders the loading macro" do + frame = page.find("turbo-frame[data-type='wiki-page-link']") + + expect(frame).to have_css( + ".op-inline-macro", + text: I18n.t("wikis.deferred_inline_page_link_macro_component.loading") + ) end end diff --git a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb index 42d3786fff34..77c9705ae88a 100644 --- a/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb +++ b/modules/wikis/spec/components/wikis/inline_page_link_macro_component_spec.rb @@ -42,17 +42,59 @@ ) end let(:page_info_result) { Success(page_info) } + let(:component) { described_class.new(page_info_result) } - subject(:render_component) { render_inline(described_class.new(page_info_result)) } + subject(:render_component) { render_inline(component) } before { render_component } - it "renders a wiki page macro link with an ARIA label and no title" do - expect(page).to have_link(text: page_info.title, href: page_info.href) - expect(page).to have_css( - ".op-inline-macro a[aria-label='#{page_info.title}. #{I18n.t('wikis.page_links.aria_label')}']", - text: page_info.title - ) - expect(page).to have_no_css(".op-inline-macro[title]") + context "when the page information is available" do + it "renders an accessible link to the wiki page" do + macro = page.find(".op-inline-macro") + link = macro.find("a", text: page_info.title, exact_text: true) + + expect(link[:href]).to eq(page_info.href) + expect(link["aria-label"]).to eq( + "#{page_info.title}. #{I18n.t('wikis.page_links.aria_label')}" + ) + end + end + + context "when retrieving the page information fails" do + let(:page_info_result) do + Failure( + Wikis::Adapters::Results::Error.new( + source: Wikis::Adapters::Providers::Internal::Queries::PageInfo, + code: error_code + ) + ) + end + + shared_examples "an unresolved wiki page link" do |error_text_key| + it "renders the error without a link" do + macro = page.find(".op-inline-macro") + + expect(macro).to have_text(I18n.t(error_text_key)) + expect(macro).to have_no_link + end + end + + context "when the page was not found" do + let(:error_code) { :not_found } + + it_behaves_like "an unresolved wiki page link", "wikis.page_links.errors.page_not_found" + end + + context "when access to the page is forbidden" do + let(:error_code) { :forbidden } + + it_behaves_like "an unresolved wiki page link", "wikis.page_links.errors.page_access_forbidden" + end + + context "when an unexpected error occurs" do + let(:error_code) { :timeout } + + it_behaves_like "an unresolved wiki page link", "wikis.page_links.errors.unexpected" + end end end diff --git a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb index 974e441dd209..85cfa316fb7b 100644 --- a/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb +++ b/spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb @@ -65,7 +65,6 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(%(href="/work_packages/#{work_package.id}")) expect(rendered).to include(%(data-hover-card-url="/work_packages/#{work_package.id}/hover_card")) expect(rendered).to include(%(aria-label="##{work_package.id}. A dynamic link to a work package placed using a macro")) - expect(rendered).not_to include(" title=") end end @@ -103,7 +102,6 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(%(data-id="#{wp.id}")) expect(rendered).to include(%(data-display-id="#{wp.display_id}")) expect(rendered).to include(%(data-detailed="false")) - expect(rendered).not_to include("aria-label") end end @@ -122,7 +120,6 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil) expect(rendered).to include(%(data-id="#{wp.id}")) expect(rendered).to include(%(data-display-id="#{wp.display_id}")) expect(rendered).to include(%(data-detailed="true")) - expect(rendered).not_to include("aria-label") end end @@ -279,7 +276,6 @@ def user_mention_tag(user) rendered = format_text(user_mention_tag(user)) expect(rendered).to include(%(aria-label="#{user.name}. A dynamic link to a user placed using a macro")) - expect(rendered).not_to include(" title=") end it "loads many mentioned users with a single users SELECT keyed by id" do From 465ffa421de6fc1c1f052fa7f7fa8cffc330dc11 Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Mon, 6 Jul 2026 14:45:32 +0200 Subject: [PATCH 10/11] Fix redundant ARIA labels and extract accessible_link_label helper --- .../text_formatting/filters/mention_filter.rb | 9 +--- .../helpers/accessible_link_label.rb | 46 +++++++++++++++++++ .../matchers/link_handlers/base.rb | 8 +--- 3 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 lib/open_project/text_formatting/helpers/accessible_link_label.rb diff --git a/lib/open_project/text_formatting/filters/mention_filter.rb b/lib/open_project/text_formatting/filters/mention_filter.rb index dca2cb9162ab..45f320aa0504 100644 --- a/lib/open_project/text_formatting/filters/mention_filter.rb +++ b/lib/open_project/text_formatting/filters/mention_filter.rb @@ -35,6 +35,7 @@ class MentionFilter < HTML::Pipeline::Filter include ActionView::Helpers::UrlHelper include OpenProject::ObjectLinking include OpenProject::StaticRouting::UrlHelpers + include OpenProject::TextFormatting::Helpers::AccessibleLinkLabel def call preload_mentions @@ -159,14 +160,6 @@ def resource_link_aria_label(resource) I18n.t("js.editor.macro.attribute_reference.aria_label_resource_link", resource:) end - def accessible_link_label(name, description) - visible_name = Nokogiri::HTML.fragment(name.to_s).text.squish - plain_description = Nokogiri::HTML.fragment(description.to_s).text.squish - I18n.t("js.editor.macro.attribute_reference.aria_label_with_name", - name: visible_name, - description: plain_description) - end - def work_package_link(work_package) display_id = work_package.display_id link_to(work_package.formatted_id, diff --git a/lib/open_project/text_formatting/helpers/accessible_link_label.rb b/lib/open_project/text_formatting/helpers/accessible_link_label.rb new file mode 100644 index 000000000000..c2d87aa352cd --- /dev/null +++ b/lib/open_project/text_formatting/helpers/accessible_link_label.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OpenProject::TextFormatting + module Helpers + # Shared helper for building composite ARIA labels on macro-generated links. + # Combines the visible link text with a plain-text context description, + # stripping any HTML from both before interpolating. + module AccessibleLinkLabel + def accessible_link_label(name, description) + visible_name = Nokogiri::HTML.fragment(name.to_s).text.squish + plain_description = Nokogiri::HTML.fragment(description.to_s).text.squish + I18n.t("js.editor.macro.attribute_reference.aria_label_with_name", + name: visible_name, + description: plain_description) + end + end + end +end diff --git a/lib/open_project/text_formatting/matchers/link_handlers/base.rb b/lib/open_project/text_formatting/matchers/link_handlers/base.rb index 4b465b586f83..08b6f97f91a2 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/base.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/base.rb @@ -32,6 +32,7 @@ module OpenProject::TextFormatting::Matchers module LinkHandlers class Base include ::OpenProject::TextFormatting::Truncation + include ::OpenProject::TextFormatting::Helpers::AccessibleLinkLabel # used for the work package quick links include WorkPackagesHelper # Used for escaping helper 'h()' @@ -114,13 +115,6 @@ def resource_link_aria_label I18n.t("js.editor.macro.attribute_reference.aria_label_resource_link", resource:) end - def accessible_link_label(name, description) - visible_name = Nokogiri::HTML.fragment(name.to_s).text.squish - plain_description = Nokogiri::HTML.fragment(description.to_s).text.squish - I18n.t("js.editor.macro.attribute_reference.aria_label_with_name", - name: visible_name, - description: plain_description) - end end end end From caa5f9505d4fb30cf1c4c24acb3bb05b54995e3d Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad Date: Mon, 6 Jul 2026 16:54:52 +0200 Subject: [PATCH 11/11] Remove unrelated macro fallback changes --- .../components/fields/macros/attribute-label-macro.component.ts | 2 +- .../components/fields/macros/attribute-value-macro.component.ts | 2 +- lib/open_project/text_formatting/matchers/link_handlers/base.rb | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts index 964a5ba750c5..9cf37298a6aa 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-label-macro.component.ts @@ -109,7 +109,7 @@ export class AttributeLabelMacroComponent implements OnInit { } const schema = await this.schemaCache.ensureLoaded(this.resource); - this.attribute = schema.attributeFromLocalizedName(attributeName) ?? attributeName; + this.attribute = schema.attributeFromLocalizedName(attributeName) || attributeName; this.label = (schema[this.attribute] as IOPFieldSchema|undefined)?.name; if (!this.label) { diff --git a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts index cd0b51df76d9..9c3ab821736d 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts @@ -121,7 +121,7 @@ export class AttributeValueMacroComponent implements OnInit { const schema = await this.schemaCache.ensureLoaded(resource); const proxied = this.schemaCache.proxied(resource, schema); - const attribute = schema.attributeFromLocalizedName(attributeName) ?? this.dateAttribute(resource, proxied, attributeName); + const attribute = schema.attributeFromLocalizedName(attributeName) || this.dateAttribute(resource, proxied, attributeName); const fieldSchema = proxied.ofProperty(attribute) as IFieldSchema|undefined; if (fieldSchema) { diff --git a/lib/open_project/text_formatting/matchers/link_handlers/base.rb b/lib/open_project/text_formatting/matchers/link_handlers/base.rb index 08b6f97f91a2..1a68eb70313e 100644 --- a/lib/open_project/text_formatting/matchers/link_handlers/base.rb +++ b/lib/open_project/text_formatting/matchers/link_handlers/base.rb @@ -114,7 +114,6 @@ def resource_link_aria_label resource = matcher.prefix.presence || (matcher.sep == "r" ? "revision" : "resource") I18n.t("js.editor.macro.attribute_reference.aria_label_resource_link", resource:) end - end end end