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 { deterministicIdString } 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 = deterministicIdString(
`button-${typeof label === "string" ? label : "btn"}-${href || ""}`,
"label-",
);
const iconElement = icon
? html`<span class="icon" part="icon">${icon}</span>`
: nothing;
Expand Down
7 changes: 5 additions & 2 deletions components/compat-table/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { unsafeHTML } from "lit/directives/unsafe-html.js";

import { L10nMixin } from "../../l10n/mixin.js";

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

import { DEFAULT_LOCALE, ISSUE_METADATA_TEMPLATE } from "./constants.js";
import styles from "./element.css?lit";
Expand Down Expand Up @@ -469,7 +469,10 @@ export class MDNCompatTable extends L10nMixin(LitElement) {
version_added: false,
};

const timelineId = randomIdString("timeline-");
const timelineId = deterministicIdString(
`timeline-${name}-${browserName}`,
"timeline-",
);
const supportClassName = getSupportClassName(support, browser);
const notes = this._renderNotes(browser, support);

Expand Down
9 changes: 7 additions & 2 deletions components/dropdown/element.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { LitElement, html } from "lit";

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

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

// Counter for generating unique dropdown IDs during SSR
let dropdownCounter = 0;

/**
* This element has two slots, which should take a single element each.
* The element in the `dropdown` slot is hidden by default,
Expand All @@ -30,6 +33,8 @@ export class MDNDropdown extends LitElement {
super();
this.open = false;
this.loaded = false;
// Assign a unique instance ID for deterministic SSR rendering
this._instanceId = dropdownCounter++;
}

get _buttonSlotElements() {
Expand Down Expand Up @@ -70,7 +75,7 @@ export class MDNDropdown extends LitElement {
_setAriaAttributes() {
let id = this._dropdownSlotElements.find((element) => element.id)?.id;
if (!id) {
id = randomIdString("uid_");
id = deterministicIdString(this._instanceId, "dropdown-");
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 { ref } from "lit/directives/ref.js";

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

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

Expand Down Expand Up @@ -111,7 +111,7 @@ export const InteractiveExampleWithChoices = (Base) =>
}

#render() {
const id = randomIdString();
const id = deterministicIdString(`choices-${this.name}`, "ix-");

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,7 +11,7 @@ import "../ix-tab/element.js";
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 { deterministicIdString } from "../utils/index.js";

/**
* @import { InteractiveExampleBase } from "./element.js";
Expand All @@ -24,7 +24,7 @@ import { randomIdString } from "../utils/index.js";
export const InteractiveExampleWithConsole = (Base) =>
class extends L10nMixin(Base) {
#render() {
const id = randomIdString();
const id = deterministicIdString(`console-${this.name}`, "ix-");

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,7 +9,7 @@ import "../ix-tab/element.js";
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 { deterministicIdString } from "../utils/index.js";

/**
* @import { InteractiveExampleBase } from "./element.js";
Expand All @@ -22,7 +22,7 @@ import { randomIdString } from "../utils/index.js";
export const InteractiveExampleWithTabs = (Base) =>
class extends L10nMixin(Base) {
#render() {
const id = randomIdString();
const id = deterministicIdString(`tabbed-${this.name}`, "ix-");

return html`
<mdn-play-controller
Expand Down
33 changes: 33 additions & 0 deletions components/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@
/**
* Simple FNV-1a hash implementation for generating deterministic IDs.
* @param {string} str - String to hash
* @returns {string} - Hexadecimal hash string
*/
function hashString(str) {
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 | number | undefined} content - Content to hash for ID generation
* @param {string} prefix - Prefix for the ID
* @returns {string}
*/
export function deterministicIdString(content, prefix = "id-") {
if (content === undefined || content === null || content === "") {
// Fallback to random for backwards compatibility when no content provided
return Math.random().toString(36).replace("0.", prefix);
}
return `${prefix}${hashString(String(content))}`;
}

/**
* @deprecated Use deterministicIdString instead
* Used to generate a random element id by combining a prefix with a random string.
*
* @param {string} prefix
Expand Down