UX: replaces custom more menu by d-menu (#29090)

One of the big advantages is a nicer menu on mobile.

This commit also fixes a bug where the close modal action was called for any destroyed d-menu trigger, even if this specific menu was not expanding, which means it was closing a different modal than its own modal, given we can only have one modal at a time.
This commit is contained in:
Joffrey JAFFEUX
2025-01-20 12:00:11 +01:00
committed by GitHub
parent 2c81e24bca
commit 89ff7d51e6
25 changed files with 206 additions and 205 deletions

View File

@ -158,6 +158,12 @@ export default class DModal extends Component {
this.closeModal(CLOSE_INITIATED_BY_SWIPE_DOWN);
}
@action
handleWrapperPointerDown(e) {
// prevents hamburger menu to close on modal backdrop click
e.stopPropagation();
}
@action
handleWrapperClick(e) {
if (e.button !== 0) {
@ -396,6 +402,8 @@ export default class DModal extends Component {
enabled=this.dismissable
}}
{{on "click" this.handleWrapperClick}}
{{! template-lint-disable no-pointer-down-event-binding }}
{{on "pointerdown" this.handleWrapperPointerDown}}
></div>
{{/unless}}
</ConditionalInElement>

View File

@ -12,7 +12,7 @@ import closeOnClickOutside from "../../modifiers/close-on-click-outside";
import SidebarHamburgerDropdown from "../sidebar/hamburger-dropdown";
const CLOSE_ON_CLICK_SELECTORS =
"a[href], .sidebar-section-header-button, .sidebar-section-link-button, .sidebar-section-link";
"a[href], .sidebar-section-header-button, .sidebar-section-link:not(.--link-button)";
export default class HamburgerDropdownWrapper extends Component {
@service currentUser;
@ -37,6 +37,10 @@ export default class HamburgerDropdownWrapper extends Component {
@action
clickOutside(e) {
if (e.target.closest(".sidebar-more-section-content")) {
return;
}
if (
e.target.classList.contains("header-cloak") &&
!prefersReducedMotion()
@ -101,6 +105,7 @@ export default class HamburgerDropdownWrapper extends Component {
>
<SidebarHamburgerDropdown
@forceMainSidebarPanel={{this.forceMainSidebarPanel}}
@toggleNavigationMenu={{this.toggleNavigation}}
/>
</div>
</template>

View File

@ -9,7 +9,10 @@ export default class SidebarAnonymousSections extends Component {
<template>
<div class="sidebar-sections sidebar-sections-anonymous">
<CustomSections @collapsable={{@collapsableSections}} />
<CustomSections
@collapsable={{@collapsableSections}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
<CategoriesSection @collapsable={{@collapsableSections}} />
{{#if this.siteSettings.tagging_enabled}}

View File

@ -93,6 +93,7 @@ export default class SidebarCustomSection extends Component {
@moreButtonAction={{this.section.moreSectionButtonAction}}
@moreButtonText={{this.section.moreSectionButtonText}}
@moreButtonIcon={{this.section.moreSectionButtonIcon}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
{{/if}}
{{else if this.section.moreSectionButtonAction}}

View File

@ -24,7 +24,11 @@ export default class SidebarCustomSections extends Component {
<template>
<div class="sidebar-custom-sections">
{{#each this.sections as |section|}}
<CustomSection @sectionData={{section}} @collapsable={{@collapsable}} />
<CustomSection
@sectionData={{section}}
@collapsable={{@collapsable}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
{{/each}}
</div>
</template>

View File

@ -66,6 +66,7 @@ export default class SidebarHamburgerDropdown extends Component {
@collapsableSections={{this.collapsableSections}}
@panel={{this.sidebarState.currentPanel}}
@hideApiSections={{@forceMainSidebarPanel}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
{{else}}
<ApiPanels

View File

@ -20,6 +20,8 @@ const SidebarMoreSectionLink = <template>
@suffixType={{@sectionLink.suffixType}}
@suffixValue={{@sectionLink.suffixValue}}
@title={{@sectionLink.title}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
...attributes
/>
</template>;

View File

@ -1,14 +1,14 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import DropdownMenu from "discourse/components/dropdown-menu";
import icon from "discourse/helpers/d-icon";
import { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import DMenu from "float-kit/components/d-menu";
import MoreSectionLink from "./more-section-link";
import SectionLinkButton from "./section-link-button";
@ -16,7 +16,6 @@ export default class SidebarMoreSectionLinks extends Component {
@service router;
@tracked activeSectionLink;
@tracked open = false;
constructor() {
super(...arguments);
@ -26,7 +25,6 @@ export default class SidebarMoreSectionLinks extends Component {
willDestroy() {
super.willDestroy(...arguments);
this.#removeClickEventListener();
this.router.off("routeDidChange", this, this.#setActiveSectionLink);
}
@ -52,53 +50,6 @@ export default class SidebarMoreSectionLinks extends Component {
});
}
@bind
closeDetails(event) {
if (event.target.closest(".sidebar-more-section-links-details-summary")) {
return;
}
if (this.open) {
const isLinkClick =
event.target.className.includes("sidebar-section-link") ||
event.target.className.includes("--link-button");
if (isLinkClick || this.#isOutsideDetailsClick(event)) {
this.open = false;
}
}
}
@action
registerClickListener() {
this.#addClickEventListener();
}
@action
unregisterClickListener() {
this.#removeClickEventListener();
}
@action
toggleSectionLinks(event) {
event.stopPropagation();
this.open = !this.open;
}
#removeClickEventListener() {
document.removeEventListener("click", this.closeDetails);
}
#addClickEventListener() {
document.addEventListener("click", this.closeDetails);
}
#isOutsideDetailsClick(event) {
return !event.composedPath().some((element) => {
return element.className === "sidebar-more-section-links-details";
});
}
#setActiveSectionLink() {
this.activeSectionLink = this.args.sectionLinks.find((sectionLink) => {
const args = [sectionLink.route];
@ -117,54 +68,64 @@ export default class SidebarMoreSectionLinks extends Component {
});
}
@action
closeMenu(menu) {
menu.close();
if (this.args.toggleNavigationMenu) {
this.args.toggleNavigationMenu();
}
}
<template>
{{#if this.activeSectionLink}}
<MoreSectionLink @sectionLink={{this.activeSectionLink}} />
{{/if}}
<li class="sidebar-section-link-wrapper">
<button
{{on "click" this.toggleSectionLinks}}
aria-expanded={{if this.open "true" "false"}}
class="sidebar-section-link sidebar-row sidebar-more-section-links-details-summary --link-button"
<DMenu
@triggerClass="sidebar-section-link sidebar-more-section-links-details-summary sidebar-row --link-button"
@modalForMobile={{true}}
@autofocus={{true}}
@placement="bottom"
@inline={{true}}
@identifier="sidebar-more-section"
>
<:trigger>
<span class="sidebar-section-link-prefix icon">
{{icon "ellipsis-vertical"}}
</span>
<span class="sidebar-section-link-content-text">
{{i18n "sidebar.more"}}
</span>
</button>
</li>
</:trigger>
{{#if this.open}}
<div class="sidebar-more-section-links-details">
<div
{{didInsert this.registerClickListener}}
{{willDestroy this.unregisterClickListener}}
class="sidebar-more-section-links-details-content-wrapper"
>
<div class="sidebar-more-section-links-details-content">
<ul class="sidebar-more-section-links-details-content-main">
<:content as |menu|>
<DropdownMenu as |dropdown|>
{{#each this.sectionLinks as |sectionLink|}}
<MoreSectionLink @sectionLink={{sectionLink}} />
<MoreSectionLink
@sectionLink={{sectionLink}}
class="dropdown-menu__item"
{{on "click" (fn this.closeMenu menu)}}
/>
{{/each}}
</ul>
{{#if @moreButtonAction}}
<div class="sidebar-more-section-links-details-content-footer">
<dropdown.divider />
<dropdown.item>
<SectionLinkButton
@action={{@moreButtonAction}}
@icon={{@moreButtonIcon}}
@text={{@moreButtonText}}
@name="customize"
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
</div>
{{/if}}
</div>
</div>
</div>
</dropdown.item>
{{/if}}
</DropdownMenu>
</:content>
</DMenu>
</li>
</template>
}

View File

@ -1,10 +1,37 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import icon from "discourse/helpers/d-icon";
const SidebarSectionLinkButton = <template>
const MORE_MENU = "sidebar-more-section";
export default class SidebarSectionLinkButton extends Component {
@service menu;
@service header;
@service siteSettings;
@action
handleClick() {
const menuInstance = this.menu.getByIdentifier(MORE_MENU);
this.args.action();
this.menu.close(menuInstance);
if (this.args.toggleNavigationMenu) {
this.args.toggleNavigationMenu();
}
if (this.siteSettings.navigation_menu === "header dropdown") {
this.header.hamburgerVisible = false;
}
}
<template>
<div class="sidebar-section-link-wrapper">
<button
{{on "click" @action}}
{{on "click" this.handleClick}}
type="button"
class="sidebar-section-link sidebar-row --link-button"
data-list-item-name={{@text}}
@ -18,6 +45,5 @@ const SidebarSectionLinkButton = <template>
</span>
</button>
</div>
</template>;
export default SidebarSectionLinkButton;
</template>
}

View File

@ -7,9 +7,13 @@ const SidebarSections = <template>
@collapsableSections={{@collapsableSections}}
@panel={{@panel}}
@hideApiSections={{@hideApiSections}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
{{else}}
<AnonymousSections @collapsableSections={{@collapsableSections}} />
<AnonymousSections
@collapsableSections={{@collapsableSections}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
{{/if}}
</template>;

View File

@ -11,7 +11,11 @@ export default class SidebarUserSections extends Component {
<template>
<div class="sidebar-sections">
<CustomSections @collapsable={{@collapsableSections}} />
<CustomSections
@collapsable={{@collapsableSections}}
@toggleNavigationMenu={{@toggleNavigationMenu}}
/>
<CategoriesSection @collapsable={{@collapsableSections}} />
{{#if this.currentUser.display_sidebar_tags}}

View File

@ -232,16 +232,16 @@ acceptance("Admin Sidebar - Sections - Plugin API", function (needs) {
)
.doesNotExist();
await click(".sidebar-more-section-links-details-summary");
await click(".sidebar-more-section-trigger");
assert
.dom(
".sidebar-more-section-links-details-content .sidebar-section-link[data-link-name='primary']"
"sidebar-more-section-content .sidebar-section-link[data-link-name='primary']"
)
.doesNotExist();
assert
.dom(
".sidebar-more-section-links-details-content .sidebar-section-link[data-link-name='secondary']"
".sidebar-more-section-content .sidebar-section-link[data-link-name='secondary']"
)
.exists();
});

View File

@ -66,7 +66,7 @@ acceptance("Enforce Second Factor for unconfirmed session", function (needs) {
);
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click(

View File

@ -10,7 +10,7 @@ acceptance("Meta Tag Updater", function (needs) {
test("updates OG title and URL", async function (assert) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click("a[href='/about']");

View File

@ -40,11 +40,11 @@ acceptance("Sidebar - Anonymous user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
const sectionLinks = queryAll(
".sidebar-more-section-links-details-content-main .sidebar-section-link"
".sidebar-more-section-content .sidebar-section-link"
);
assert

View File

@ -47,54 +47,32 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-content"
".sidebar-section[data-section-name='community'] .sidebar-more-section-content"
)
.exists("additional section links are displayed");
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary[aria-expanded='true']"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger[aria-expanded='true']"
)
.exists(
"aria-expanded toggles to true when additional links are displayed"
);
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-content"
".sidebar-section[data-section-name='community'] .sidebar-more-section-content"
)
.doesNotExist("additional section links are hidden");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
);
await click("#main-outlet");
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-content"
)
.doesNotExist(
"additional section links are hidden when clicking outside"
);
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary[aria-expanded='false']"
)
.exists(
"aria-expanded toggles to false when additional links are hidden"
);
});
test("clicking on everything link", async function (assert) {
@ -252,7 +230,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
);
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click(
@ -279,7 +257,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary .sidebar-section-link-content-text"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger .sidebar-section-link-content-text"
)
.hasText(
i18n("sidebar.more"),
@ -301,7 +279,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
@ -315,7 +293,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click(
@ -335,7 +313,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
@ -357,7 +335,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
);
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click(
@ -384,7 +362,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary .sidebar-section-link-content-text"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger .sidebar-section-link-content-text"
)
.hasText(
i18n("sidebar.more"),
@ -408,7 +386,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
@ -422,7 +400,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click(
@ -448,7 +426,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click(
@ -468,7 +446,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
@ -860,7 +838,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
.doesNotExist("review link is not shown");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
@ -897,12 +875,12 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
);
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-content .sidebar-section-link[data-link-name='review']"
".sidebar-section[data-section-name='community'] .sidebar-more-section-content .sidebar-section-link[data-link-name='review']"
)
.exists("review link is displayed in the more drawer");
@ -923,12 +901,12 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
.hasText("34 pending", "displays the pending reviewable count");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-content .sidebar-section-link[data-link-name='review']"
".sidebar-section[data-section-name='community'] .sidebar-more-section-content .sidebar-section-link[data-link-name='review']"
)
.doesNotExist("review link is not displayed in the more drawer");
});
@ -947,7 +925,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
assert
@ -1009,7 +987,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
);
await click(".sidebar-section-link[data-link-name='user-summary']");

View File

@ -68,9 +68,8 @@ acceptance(
assert
.dom("[data-link-name='admin']")
.exists("the admin link is not within the 'more' dropdown");
assert
.dom(".sidebar-more-section-links-details-summary")
.dom(".sidebar-more-section-trigger")
.doesNotExist(
"the 'more' dropdown should not be present in header dropdown mode"
);

View File

@ -61,7 +61,7 @@ export default class DMenuInstance extends FloatKitInstance {
await super.close(...arguments);
if (this.site.mobileView && this.options.modalForMobile) {
if (this.site.mobileView && this.options.modalForMobile && this.expanded) {
await this.modal.close();
}

View File

@ -1,43 +1,11 @@
.sidebar-more-section-links-details-content {
background-color: var(--d-sidebar-background);
transition: background-color 0.25s;
box-shadow: var(--shadow-card);
border: 1px solid var(--d-sidebar-border-color);
margin: 0 calc(var(--d-sidebar-row-horizontal-padding) * 2 / 3);
.sidebar-more-section-content {
z-index: z("modal", "dropdown");
.sidebar-row {
padding: 0.33rem calc(var(--d-sidebar-row-horizontal-padding) / 3);
}
}
.sidebar-more-section-links-details-content-main {
position: sticky;
margin: 0;
}
.sidebar-more-section-links-details-content-footer {
border-top: 2px solid var(--d-sidebar-border-color);
display: flex;
width: 100%;
.sidebar-section-link-wrapper {
width: 100%;
}
}
.sidebar-more-section-links-details-content-wrapper {
position: absolute;
width: 100%;
z-index: z("modal", "content") + 1;
}
.sidebar-more-section-links-details {
position: relative;
@include breakpoint(tablet) {
grid-column-start: 1;
grid-column-end: 3;
.sidebar-more-section-links-details-content-main {
gap: 0 1em;
}
}
.sidebar-more-section-trigger {
justify-content: flex-start;
}

View File

@ -16,6 +16,10 @@
.sidebar-section-header-button {
font-size: var(--font-down-1);
> * {
pointer-events: none;
}
}
.btn.dropdown-select-box-header,
@ -142,9 +146,6 @@
.sidebar-section-content {
margin: 0;
hr {
margin: 0em 1.5em;
}
}
}

View File

@ -14,5 +14,6 @@
&__divider {
margin: 0rem;
height: 0;
}
}

View File

@ -48,7 +48,7 @@
color: var(--primary-medium);
}
}
ul {
.sidebar-section-content {
display: grid;
grid-template-columns: 1fr 1fr;
li {

View File

@ -11,6 +11,10 @@
border-radius: 10px;
}
.d-modal__body {
padding: 1em 0;
}
h3 {
padding-top: 0.25em;
}

View File

@ -34,14 +34,14 @@ module PageObjects
I18n.t("js.sidebar.sections.community.edit_section.sidebar"),
)
expect(community_section).to have_no_css(".sidebar-more-section-links-details")
expect(community_section).to have_no_css(".sidebar-more-section-content")
PageObjects::Modals::SidebarSectionForm.new
end
def click_community_section_more_button
community_section.click_button(class: "sidebar-more-section-links-details-summary")
expect(community_section).to have_css(".sidebar-more-section-links-details")
community_section.click_button(class: "sidebar-more-section-trigger")
expect(community_section).to have_css(".sidebar-more-section-content")
self
end

View File

@ -61,6 +61,37 @@ describe "Viewing sidebar as logged in user", type: :system do
end
end
describe "when viewing the 'more' content in the Community sidebar section" do
let(:more_trigger_selector) do
".sidebar-section[data-section-name='community'] .sidebar-more-section-trigger"
end
let(:more_links_selector) do
".sidebar-section[data-section-name='community'] .sidebar-more-section-content"
end
it "toggles the more menu and handles click outside to close it" do
visit("/latest")
find(more_trigger_selector).click
expect(page).to have_selector(more_links_selector, visible: true)
expect(page).to have_selector("#{more_trigger_selector}[aria-expanded='true']")
find(more_trigger_selector).click
expect(page).not_to have_selector(more_links_selector)
expect(page).to have_selector("#{more_trigger_selector}[aria-expanded='false']")
find(more_trigger_selector).click
find(".d-header-wrap").click
expect(page).not_to have_selector(more_links_selector)
end
end
describe "when viewing the tags section" do
fab!(:tag1) do
Fabricate(:tag, name: "tag 1", description: "tag 1 description <script>").tap do |tag|