From 411203c078bc6a7e11c84ec79b1bce5a36c4a843 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 17 Oct 2025 23:37:18 +0200 Subject: [PATCH] fix(utils): replace `randomIdString()` with `deterministicIdString()` --- components/button/pure.js | 7 ++-- components/compat-table/element.js | 7 ++-- components/dropdown/element.js | 9 +++-- .../interactive-example/with-choices.js | 4 +-- .../interactive-example/with-console.js | 4 +-- components/interactive-example/with-tabs.js | 4 +-- components/utils/index.js | 33 +++++++++++++++++++ 7 files changed, 56 insertions(+), 12 deletions(-) diff --git a/components/button/pure.js b/components/button/pure.js index 768016a01..d85e6dc88 100644 --- a/components/button/pure.js +++ b/components/button/pure.js @@ -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 @@ -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`${icon}` : nothing; diff --git a/components/compat-table/element.js b/components/compat-table/element.js index b50aadbb8..668ec2f7a 100644 --- a/components/compat-table/element.js +++ b/components/compat-table/element.js @@ -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"; @@ -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); diff --git a/components/dropdown/element.js b/components/dropdown/element.js index d4f1bb98c..ad08304b5 100644 --- a/components/dropdown/element.js +++ b/components/dropdown/element.js @@ -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, @@ -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() { @@ -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) { diff --git a/components/interactive-example/with-choices.js b/components/interactive-example/with-choices.js index 50bc6d472..b6facefc5 100644 --- a/components/interactive-example/with-choices.js +++ b/components/interactive-example/with-choices.js @@ -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"; @@ -111,7 +111,7 @@ export const InteractiveExampleWithChoices = (Base) => } #render() { - const id = randomIdString(); + const id = deterministicIdString(`choices-${this.name}`, "ix-"); return html`
diff --git a/components/interactive-example/with-console.js b/components/interactive-example/with-console.js index c22a7d032..3c2eacd4a 100644 --- a/components/interactive-example/with-console.js +++ b/components/interactive-example/with-console.js @@ -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"; @@ -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` diff --git a/components/interactive-example/with-tabs.js b/components/interactive-example/with-tabs.js index 9a460b7f8..68a8edf7b 100644 --- a/components/interactive-example/with-tabs.js +++ b/components/interactive-example/with-tabs.js @@ -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"; @@ -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` >> 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