diff --git a/package-lock.json b/package-lock.json
index f289d91eb2d..5229b0f2c51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45525,7 +45525,7 @@
},
"packages/calcite-components": {
"name": "@esri/calcite-components",
- "version": "1.12.0-next.6",
+ "version": "1.12.0-next.7",
"license": "SEE LICENSE.md",
"dependencies": {
"@floating-ui/dom": "1.5.3",
@@ -49404,10 +49404,10 @@
},
"packages/calcite-components-angular/projects/component-library": {
"name": "@esri/calcite-components-angular",
- "version": "1.12.0-next.6",
+ "version": "1.12.0-next.7",
"license": "SEE LICENSE.md",
"dependencies": {
- "@esri/calcite-components": "^1.12.0-next.6",
+ "@esri/calcite-components": "^1.12.0-next.7",
"tslib": "2.3.0"
},
"peerDependencies": {
@@ -49417,10 +49417,10 @@
},
"packages/calcite-components-react": {
"name": "@esri/calcite-components-react",
- "version": "1.12.0-next.6",
+ "version": "1.12.0-next.7",
"license": "SEE LICENSE.md",
"dependencies": {
- "@esri/calcite-components": "^1.12.0-next.6"
+ "@esri/calcite-components": "^1.12.0-next.7"
},
"peerDependencies": {
"react": ">=16.7",
@@ -52294,14 +52294,14 @@
"@esri/calcite-components-angular": {
"version": "file:packages/calcite-components-angular/projects/component-library",
"requires": {
- "@esri/calcite-components": "^1.12.0-next.6",
+ "@esri/calcite-components": "^1.12.0-next.7",
"tslib": "2.3.0"
}
},
"@esri/calcite-components-react": {
"version": "file:packages/calcite-components-react",
"requires": {
- "@esri/calcite-components": "^1.12.0-next.6"
+ "@esri/calcite-components": "^1.12.0-next.7"
}
},
"@esri/calcite-design-tokens": {
diff --git a/packages/calcite-components-angular/projects/component-library/CHANGELOG.md b/packages/calcite-components-angular/projects/component-library/CHANGELOG.md
index 75a71d57b8d..36dec1464a6 100644
--- a/packages/calcite-components-angular/projects/component-library/CHANGELOG.md
+++ b/packages/calcite-components-angular/projects/component-library/CHANGELOG.md
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [1.12.0-next.7](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-angular@1.12.0-next.6...@esri/calcite-components-angular@1.12.0-next.7) (2023-12-01)
+
+**Note:** Version bump only for package @esri/calcite-components-angular
+
## [1.12.0-next.6](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-angular@1.12.0-next.5...@esri/calcite-components-angular@1.12.0-next.6) (2023-12-01)
**Note:** Version bump only for package @esri/calcite-components-angular
diff --git a/packages/calcite-components-angular/projects/component-library/package.json b/packages/calcite-components-angular/projects/component-library/package.json
index ee27b1efee6..183158815ce 100644
--- a/packages/calcite-components-angular/projects/component-library/package.json
+++ b/packages/calcite-components-angular/projects/component-library/package.json
@@ -1,6 +1,6 @@
{
"name": "@esri/calcite-components-angular",
- "version": "1.12.0-next.6",
+ "version": "1.12.0-next.7",
"sideEffects": false,
"homepage": "https://developers.arcgis.com/calcite-design-system/",
"description": "A set of Angular components that wrap Esri's Calcite Components.",
@@ -20,7 +20,7 @@
"@angular/core": ">=16.0.0"
},
"dependencies": {
- "@esri/calcite-components": "^1.12.0-next.6",
+ "@esri/calcite-components": "^1.12.0-next.7",
"tslib": "2.3.0"
},
"lerna": {
diff --git a/packages/calcite-components-react/CHANGELOG.md b/packages/calcite-components-react/CHANGELOG.md
index fab8386b4c8..b64c3ef4043 100644
--- a/packages/calcite-components-react/CHANGELOG.md
+++ b/packages/calcite-components-react/CHANGELOG.md
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [1.12.0-next.7](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@1.12.0-next.6...@esri/calcite-components-react@1.12.0-next.7) (2023-12-01)
+
+**Note:** Version bump only for package @esri/calcite-components-react
+
## [1.12.0-next.6](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@1.12.0-next.5...@esri/calcite-components-react@1.12.0-next.6) (2023-12-01)
**Note:** Version bump only for package @esri/calcite-components-react
diff --git a/packages/calcite-components-react/package.json b/packages/calcite-components-react/package.json
index c3fbb6566a1..54f668864d1 100644
--- a/packages/calcite-components-react/package.json
+++ b/packages/calcite-components-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@esri/calcite-components-react",
"sideEffects": false,
- "version": "1.12.0-next.6",
+ "version": "1.12.0-next.7",
"homepage": "https://developers.arcgis.com/calcite-design-system/",
"description": "A set of React components that wrap calcite components",
"license": "SEE LICENSE.md",
@@ -20,7 +20,7 @@
"dist/"
],
"dependencies": {
- "@esri/calcite-components": "^1.12.0-next.6"
+ "@esri/calcite-components": "^1.12.0-next.7"
},
"peerDependencies": {
"react": ">=16.7",
diff --git a/packages/calcite-components/CHANGELOG.md b/packages/calcite-components/CHANGELOG.md
index fea510e8eb1..c796368a29d 100644
--- a/packages/calcite-components/CHANGELOG.md
+++ b/packages/calcite-components/CHANGELOG.md
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [1.12.0-next.7](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@1.12.0-next.6...@esri/calcite-components@1.12.0-next.7) (2023-12-01)
+
+### Bug Fixes
+
+- **label:** associate label to component when `for` prop is set after initialization ([#8309](https://github.com/Esri/calcite-design-system/issues/8309)) ([e81b650](https://github.com/Esri/calcite-design-system/commit/e81b650ae66b413d19eb567c3766f813dfa322c7)), closes [#7364](https://github.com/Esri/calcite-design-system/issues/7364)
+
## [1.12.0-next.6](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@1.12.0-next.5...@esri/calcite-components@1.12.0-next.6) (2023-12-01)
### Features
diff --git a/packages/calcite-components/package.json b/packages/calcite-components/package.json
index a166f03c3c8..2703068d594 100644
--- a/packages/calcite-components/package.json
+++ b/packages/calcite-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@esri/calcite-components",
- "version": "1.12.0-next.6",
+ "version": "1.12.0-next.7",
"homepage": "https://developers.arcgis.com/calcite-design-system/",
"description": "Web Components for Esri's Calcite Design System.",
"main": "dist/index.cjs.js",
diff --git a/packages/calcite-components/src/components/label/label.tsx b/packages/calcite-components/src/components/label/label.tsx
index bd24c6ddade..06f9d89d9c6 100644
--- a/packages/calcite-components/src/components/label/label.tsx
+++ b/packages/calcite-components/src/components/label/label.tsx
@@ -1,5 +1,19 @@
-import { Component, Element, Event, EventEmitter, h, Host, Prop, VNode } from "@stencil/core";
-import { labelConnectedEvent, labelDisconnectedEvent } from "../../utils/label";
+import {
+ Component,
+ Element,
+ Event,
+ EventEmitter,
+ h,
+ Host,
+ Prop,
+ VNode,
+ Watch,
+} from "@stencil/core";
+import {
+ associateExplicitLabelToUnlabeledComponent,
+ labelConnectedEvent,
+ labelDisconnectedEvent,
+} from "../../utils/label";
import { Alignment, Scale } from "../interfaces";
import { CSS } from "./resources";
@@ -24,6 +38,11 @@ export class Label {
/** Specifies the `id` of the component the label is bound to. Use when the component the label is bound to does not reside within the component. */
@Prop({ reflect: true }) for: string;
+ @Watch("for")
+ handleForChange(): void {
+ associateExplicitLabelToUnlabeledComponent(this.el);
+ }
+
/** Specifies the size of the component. */
@Prop({ reflect: true }) scale: Scale = "m";
diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts
index e208679342f..b70c0a50b0f 100644
--- a/packages/calcite-components/src/tests/commonTests.ts
+++ b/packages/calcite-components/src/tests/commonTests.ts
@@ -646,6 +646,30 @@ export function labelable(
shadowFocusTargetSelector,
});
});
+
+ it("is labelable when label's for is set after initialization", async () => {
+ const siblingHtml = html`
+ ${labelTitle}
+ ${componentHtml}
+ `;
+ const siblingPage: E2EPage = await newE2EPage();
+ beforeContent?.(siblingPage);
+
+ await siblingPage.setContent(siblingHtml);
+ await siblingPage.waitForChanges();
+
+ const label = await siblingPage.find("calcite-label");
+ label.setProperty("for", id);
+ await siblingPage.waitForChanges();
+
+ await assertLabelable({
+ page: siblingPage,
+ componentTag,
+ propertyToToggle,
+ focusTargetSelector,
+ shadowFocusTargetSelector,
+ });
+ });
});
}
diff --git a/packages/calcite-components/src/utils/component.spec.ts b/packages/calcite-components/src/utils/component.spec.ts
index 82cb4369348..ca7fa99d1bf 100644
--- a/packages/calcite-components/src/utils/component.spec.ts
+++ b/packages/calcite-components/src/utils/component.spec.ts
@@ -1,4 +1,6 @@
-import { getIconScale } from "./component";
+import { componentOnReady, getIconScale } from "./component";
+import { html } from "../../support/formatting";
+import { HTMLStencilElement } from "@stencil/core/internal";
describe("getIconScale", () => {
it('should return "m" when input is "l"', () => {
@@ -10,3 +12,40 @@ describe("getIconScale", () => {
expect(getIconScale("s")).toBe("s");
});
});
+
+describe("componentOnReady", () => {
+ let requestAnimationFrameSpy: jest.SpyInstance;
+ let fakeComponent: HTMLElement;
+
+ beforeEach(() => {
+ document.body.innerHTML = html` `;
+ fakeComponent = document.querySelector("fake-component");
+
+ const originalRaf = globalThis.requestAnimationFrame;
+ requestAnimationFrameSpy = jest
+ .spyOn(globalThis, "requestAnimationFrame")
+ .mockImplementation((callback) => originalRaf(callback));
+ });
+
+ afterEach(() => requestAnimationFrameSpy.mockRestore());
+
+ it("should call componentOnReady if it exists on the element (lazy-loaded)", async () => {
+ const componentOnReadyStub = ((fakeComponent as HTMLStencilElement).componentOnReady = jest.fn());
+
+ const promise = componentOnReady(fakeComponent);
+ expect(promise).toBeInstanceOf(Promise);
+
+ await promise;
+ expect(componentOnReadyStub).toHaveBeenCalled();
+ });
+
+ it("waits for an animation frame if componentOnReady does not exist on the element", async () => {
+ expect(requestAnimationFrameSpy).not.toHaveBeenCalled();
+
+ const promise = componentOnReady(fakeComponent);
+ expect(promise).toBeInstanceOf(Promise);
+
+ await promise;
+ expect(requestAnimationFrameSpy).toHaveBeenCalled();
+ });
+});
diff --git a/packages/calcite-components/src/utils/component.ts b/packages/calcite-components/src/utils/component.ts
index c8477fee4cc..645f3552131 100644
--- a/packages/calcite-components/src/utils/component.ts
+++ b/packages/calcite-components/src/utils/component.ts
@@ -1,5 +1,23 @@
import { Scale } from "../components/interfaces";
+import { HTMLStencilElement } from "@stencil/core/internal";
export function getIconScale(componentScale: Scale): Extract {
return componentScale === "l" ? "m" : "s";
}
+
+/**
+ * This util helps us wait for a component to be ready for both lazy-loading (`dist` output) and non-lazy-loading (`components` output) components.
+ *
+ * Based on https://github.com/ionic-team/ionic-framework/blob/1a8bd6d/core/src/utils/helpers.ts#L60C1-L79C3
+ *
+ * @param el - the host element to wait for
+ */
+export async function componentOnReady(el: HTMLElement): Promise {
+ await (isStencilEl(el)
+ ? el.componentOnReady()
+ : new Promise((resolve) => requestAnimationFrame(() => resolve())));
+}
+
+function isStencilEl(el: HTMLElement): el is HTMLStencilElement {
+ return typeof (el as HTMLStencilElement).componentOnReady === "function";
+}
diff --git a/packages/calcite-components/src/utils/label.ts b/packages/calcite-components/src/utils/label.ts
index aeb3dd6968f..00c0a6f4f31 100644
--- a/packages/calcite-components/src/utils/label.ts
+++ b/packages/calcite-components/src/utils/label.ts
@@ -1,4 +1,5 @@
import { closestElementCrossShadowBoundary, isBefore, queryElementRoots } from "./dom";
+import { componentOnReady } from "./component";
export interface LabelableComponent {
/**
@@ -41,7 +42,7 @@ const labelToLabelables = new WeakMap();
const onLabelConnectedMap = new WeakMap();
const onLabelDisconnectedMap = new WeakMap();
-const unlabeledComponents = new WeakSet();
+const unlabeledComponents = new Set();
const findLabelForComponent = (componentEl: HTMLElement): HTMLCalciteLabelElement | null => {
const { id } = componentEl;
@@ -70,7 +71,7 @@ function hasAncestorCustomElements(label: HTMLCalciteLabelElement, componentEl:
let traversedElements: HTMLElement[];
const customElementAncestorCheckEventType = "custom-element-ancestor-check";
- const listener = (event) => {
+ const listener = (event: CustomEvent) => {
event.stopImmediatePropagation();
const composedPath = event.composedPath() as HTMLElement[];
traversedElements = composedPath.slice(composedPath.indexOf(componentEl), composedPath.indexOf(label));
@@ -94,6 +95,10 @@ function hasAncestorCustomElements(label: HTMLCalciteLabelElement, componentEl:
* @param component
*/
export function connectLabel(component: LabelableComponent): void {
+ if (!component) {
+ return;
+ }
+
const labelEl = findLabelForComponent(component.el);
if (
@@ -132,6 +137,10 @@ export function connectLabel(component: LabelableComponent): void {
* @param component
*/
export function disconnectLabel(component: LabelableComponent): void {
+ if (!component) {
+ return;
+ }
+
unlabeledComponents.delete(component);
document.removeEventListener(labelConnectedEvent, onLabelConnectedMap.get(component));
document.removeEventListener(labelDisconnectedEvent, onLabelDisconnectedMap.get(component));
@@ -202,3 +211,33 @@ function onLabelDisconnected(this: LabelableComponent): void {
onLabelConnectedMap.set(this, boundOnLabelConnected);
document.addEventListener(labelConnectedEvent, boundOnLabelConnected);
}
+
+/**
+ * Helper to associate an explicit label (i.e., using `for`) with a labelable component that does not have an associated label.
+ *
+ * @param label - the label element
+ */
+export async function associateExplicitLabelToUnlabeledComponent(label: HTMLCalciteLabelElement): Promise {
+ await componentOnReady(label);
+
+ const alreadyLabeled = labelToLabelables.has(label);
+
+ if (alreadyLabeled) {
+ return;
+ }
+
+ const forComponentEl = label.ownerDocument?.getElementById(label.for);
+
+ if (!forComponentEl) {
+ return;
+ }
+
+ requestAnimationFrame(() => {
+ for (const labelable of unlabeledComponents) {
+ if (labelable.el === forComponentEl) {
+ connectLabel(labelable);
+ break;
+ }
+ }
+ });
+}