Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions components/button/pure.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";

import { randomIdString } from "../utils/index.js";
import { createElementId } from "../utils/index.js";

/**
* @param {object} options
Expand All @@ -28,7 +28,10 @@ export default function Button({
variant = "primary",
action,
}) {
const labelId = randomIdString("label-");
const labelId = createElementId(
"label",
`${typeof label === "string" ? label : "btn"}-${href || ""}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely to cause collisions: we can realistically have two buttons with the same href on the same page.

We only need this ID to set aria-labelledby for icon-only buttons. Perhaps we set aria-label for icon-only buttons instead, and enforce that label is a string with types when iconOnly is true?

);
const iconElement = icon
? html`<span class="icon" part="icon">${icon}</span>`
: nothing;
Expand Down
2 changes: 1 addition & 1 deletion components/color-theme/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class MDNColorTheme extends L10nMixin(LitElement) {

render() {
return html`<div class="color-theme">
<mdn-dropdown>
<mdn-dropdown name="color-theme">
<button
part="button"
slot="button"
Expand Down
7 changes: 5 additions & 2 deletions components/dropdown/element.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LitElement, html } from "lit";

import { randomIdString } from "../utils/index.js";
import { createElementId } from "../utils/index.js";

import styles from "./element.css?lit";

Expand All @@ -13,6 +13,7 @@ import styles from "./element.css?lit";
*
* @element mdn-dropdown
*
* @attr {string} name - Unique name used to generate a stable ID for the dropdown slot element (for aria-controls). Required when the dropdown slot element has no explicit id.
* @attr {boolean} open - Whether the dropdown is open or not.
*
* @slot button - The element used to toggle the dropdown.
Expand All @@ -22,12 +23,14 @@ export class MDNDropdown extends LitElement {
static styles = styles;

static properties = {
name: { type: String, required: true },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required: true doesn't do anything

open: { type: Boolean },
loaded: { type: Boolean, reflect: true },
};

constructor() {
super();
this.name = "";
this.open = false;
this.loaded = false;
}
Expand Down Expand Up @@ -70,7 +73,7 @@ export class MDNDropdown extends LitElement {
_setAriaAttributes() {
let id = this._dropdownSlotElements.find((element) => element.id)?.id;
if (!id) {
id = randomIdString("uid_");
id = createElementId("dropdown", this.name);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unnecessary, why not construct the string directly?

this._dropdownSlotElements[0]?.setAttribute("id", id);
}
for (const element of this._buttonSlotElements) {
Expand Down
4 changes: 2 additions & 2 deletions components/interactive-example/with-choices.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { L10nMixin } from "../../l10n/mixin.js";
import { MDNPlayEditor } from "../play-editor/element.js";
import { randomIdString } from "../utils/index.js";
import { createElementId } from "../utils/index.js";

import { isCSSSupported } from "./utils.js";

Expand All @@ -17,7 +17,7 @@
*/

/**
* @template {new (...args: any[]) => InteractiveExampleBase} TBase

Check warning on line 20 in components/interactive-example/with-choices.js

View workflow job for this annotation

GitHub Actions / lint

Prefer a more specific type to `any`
* @param {TBase} Base
*/
export const InteractiveExampleWithChoices = (Base) =>
Expand All @@ -28,7 +28,7 @@
__choiceUpdated: { state: true },
};

/** @param {any[]} _args */

Check warning on line 31 in components/interactive-example/with-choices.js

View workflow job for this annotation

GitHub Actions / lint

Prefer a more specific type to `any`
constructor(..._args) {
super();
/** @type {number} */
Expand Down Expand Up @@ -111,7 +111,7 @@
}

#render() {
const id = randomIdString();
const id = createElementId("ix-choices", this.name);

return html`
<div class="template-choices" aria-labelledby=${id}>
Expand Down
4 changes: 2 additions & 2 deletions components/interactive-example/with-console.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@
import "../ix-tab-panel/element.js";
import "../ix-tab-wrapper/element.js";
import { L10nMixin } from "../../l10n/mixin.js";
import { randomIdString } from "../utils/index.js";
import { createElementId } from "../utils/index.js";

/**
* @import { InteractiveExampleBase } from "./element.js";
*/

/**
* @template {new (...args: any[]) => InteractiveExampleBase} TBase

Check warning on line 21 in components/interactive-example/with-console.js

View workflow job for this annotation

GitHub Actions / lint

Prefer a more specific type to `any`
* @param {TBase} Base
*/
export const InteractiveExampleWithConsole = (Base) =>
class extends L10nMixin(Base) {
#render() {
const id = randomIdString();
const id = createElementId("ix-console", this.name);

return html`
<mdn-play-controller ${ref(this._controller)}>
Expand Down
4 changes: 2 additions & 2 deletions components/interactive-example/with-tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
import "../ix-tab-panel/element.js";
import "../ix-tab-wrapper/element.js";
import { L10nMixin } from "../../l10n/mixin.js";
import { randomIdString } from "../utils/index.js";
import { createElementId } from "../utils/index.js";

/**
* @import { InteractiveExampleBase } from "./element.js";
*/

/**
* @template {new (...args: any[]) => InteractiveExampleBase} TBase

Check warning on line 19 in components/interactive-example/with-tabs.js

View workflow job for this annotation

GitHub Actions / lint

Prefer a more specific type to `any`
* @param {TBase} Base
*/
export const InteractiveExampleWithTabs = (Base) =>
class extends L10nMixin(Base) {
#render() {
const id = randomIdString();
const id = createElementId("ix-tabbed", this.name);

return html`
<mdn-play-controller
Expand Down
2 changes: 1 addition & 1 deletion components/language-switcher/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class MDNLanguageSwitcher extends L10nMixin(LitElement) {
}

return html`<div class="language-switcher">
<mdn-dropdown>
<mdn-dropdown name="language-switcher">
<button
part="button"
slot="button"
Expand Down
2 changes: 1 addition & 1 deletion components/menu/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class Menu extends ServerComponent {
data-glean-id=${`menu_click_link: top-level -> ${tab.href}`}
>${tab.buttonText}</a
>`
: html`<mdn-dropdown>
: html`<mdn-dropdown name=${`menu-${tab.id}`}>
<button class="menu__tab-button" type="button" slot="button">
${typeof tab.buttonText === "string"
? html`<span class="menu__tab-label"
Expand Down
2 changes: 1 addition & 1 deletion components/observatory-results/rating.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

/**
*
* @param {{result: import("@observatory").Result, host: string, rescan: Function}} props

Check warning on line 13 in components/observatory-results/rating.js

View workflow job for this annotation

GitHub Actions / lint

Prefer a more specific type to `Function`
* @returns {import("@lit").TemplateResult}
*/
export function Rating({ result, host, rescan }) {
Expand All @@ -22,7 +22,7 @@
<section class="scan-result">
<section class="grade-trend">
<div class="overall">
<mdn-dropdown>
<mdn-dropdown name="observatory-results">
<button
slot="button"
aria-label="show info tooltip"
Expand Down
2 changes: 1 addition & 1 deletion components/user-menu/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class MDNUserMenu extends L10nMixin(LitElement) {
return user.isAuthenticated
? html`
<div class="user-menu">
<mdn-dropdown>
<mdn-dropdown name="user-menu">
<button slot="button" class="user-menu__button">
${user.avatarUrl
? html`<img
Expand Down
30 changes: 26 additions & 4 deletions components/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
/**
* Used to generate a random element id by combining a prefix with a random string.
* Simple FNV-1a hash implementation for generating deterministic IDs.
* @param {string} str - String to hash
* @returns {string} - Hexadecimal hash string
*/
function hashString(str) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what hashing gives us here: if the input isn't unique, the output won't be either - this seems unnecessary

let hash = 2_166_136_261; // FNV offset basis
for (let i = 0; i < str.length; i++) {
// eslint-disable-next-line unicorn/prefer-code-point -- charCodeAt is sufficient for hash
hash ^= str.charCodeAt(i);
hash = Math.imul(hash, 16_777_619); // FNV prime
}
// Convert to unsigned 32-bit and then to hex
return (hash >>> 0).toString(36);
}

/**
* Used to generate a deterministic element id by hashing the provided content.
* Falls back to random generation if no content is provided (for backwards compatibility).
*
* @param {string} prefix
* @param {string} prefix - Prefix for the ID
* @param {string} content - Content to hash for ID generation
* @returns {string}
*/
export function randomIdString(prefix = "id-") {
return Math.random().toString(36).replace("0.", prefix);
export function createElementId(prefix, content) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function itself seems unnecessary: where we can set a static, unique ID in call sites, let's do that instead of routing it through here.

if (!content) {
// Fallback to random for backwards compatibility when no content provided
return Math.random().toString(36).replace("0.", prefix);
}
return `${prefix}-${hashString(String(content))}`;
}
Loading