@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
295 lines (293 loc) • 10.8 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import "../../globals/settings.js";
import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.js";
import coachmark_default$1 from "./coachmark.scss.js";
import { makeDraggable } from "../../utilities/makeDraggable/makeDraggable.js";
import { resetCoachmarkDetailsSignal, updateCoachmarkDetailsSignal } from "./coachmark-context.js";
import { LitElement, html } from "lit";
import { property, state } from "lit/decorators.js";
import HostListenerMixin from "@carbon/web-components/es/globals/mixins/host-listener.js";
import { carbonElement } from "@carbon/web-components/es/globals/decorators/carbon-element.js";
import { SignalWatcher } from "@lit-labs/signals";
import '@carbon/web-components/es-custom/components/popover/index.js';
import { POPOVER_ALIGNMENT } from "@carbon/web-components/es/components/popover/defs.js";
//#region src/components/coachmark/coachmark.ts
/**
* @license
*
* Copyright IBM Corp. 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const blockClass = `c4p--coachmark`;
/**
* coachmark main component
* @element c4p-coachmark
* @fires c4p-coachmark-opened
* The custom event fired when the coachmark is opened.
* This event can be used to perform actions such as focusing elements when the coachmark becomes visible.
* @fires c4p-coachmark-closed
* The custom event fired when the coachmark is closed.
* This event can be used to perform actions such as restoring focus when the coachmark is dismissed.
*/
let CDSCoachmark = class CDSCoachmark extends SignalWatcher(HostListenerMixin(LitElement)) {
constructor(..._args) {
super(..._args);
this.open = false;
this.align = POPOVER_ALIGNMENT.TOP;
this.floating = false;
this.highContrast = false;
this.dropShadow = false;
this.caret = false;
this.dragAriaLabel = "Picked up the draggable coachmark";
this.dragCleanup = null;
this.keydownHandler = null;
this.moveCounter = 0;
this.isDragging = null;
this.moveAnnouncement = "";
this.handlePopoverClosed = () => {
if (this.open) this.open = false;
};
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.dragCleanup) this.dragCleanup();
if (this.keydownHandler) document.removeEventListener("keydown", this.keydownHandler);
resetCoachmarkDetailsSignal();
}
async setupDraggable() {
const popoverContent = (this.shadowRoot?.querySelector(`.${blockClass}--popover`)).querySelector("cds-custom-popover-content");
const header = (popoverContent.querySelector(`.${blockClass}--wrapper`).querySelector("slot")?.assignedElements({ flatten: true }))?.find((el) => el.tagName.toLowerCase() === `c4p-coachmark-header`);
if (header?.updateComplete) await header.updateComplete;
requestAnimationFrame(() => {
const dragHandle = header.shadowRoot?.querySelector(`.c4p--coachmark-header-drag-handle`);
const dragInactiveInstruction = "Press Enter or Space to activate drag mode.";
const dragActiveInstruction = "Use arrow keys to move the coachmark. Press Enter or Space to exit drag mode.";
if (dragHandle) dragHandle.setAttribute("aria-label", dragInactiveInstruction);
const draggable = makeDraggable({
el: popoverContent,
dragHandle: header,
focusableDragHandle: dragHandle
});
const onDragStart = () => {
this.isDragging = true;
this.moveCounter = 0;
this.moveAnnouncement = "";
popoverContent.classList.add(`${blockClass}--is-dragging`);
if (this.dragAriaLabel) popoverContent.setAttribute("aria-label", this.dragAriaLabel);
if (dragHandle) dragHandle.setAttribute("aria-label", dragActiveInstruction);
updateCoachmarkDetailsSignal({
name: "isDragging",
detail: true
});
};
const onDragEnd = () => {
this.isDragging = false;
this.moveCounter = 0;
this.moveAnnouncement = "";
popoverContent.classList.remove(`${blockClass}--is-dragging`);
popoverContent.removeAttribute("aria-label");
if (dragHandle) dragHandle.setAttribute("aria-label", dragInactiveInstruction);
updateCoachmarkDetailsSignal({
name: "isDragging",
detail: false
});
};
const onDragMove = (event) => {
const { direction, distance } = event.detail;
this.moveCounter += 1;
const uniqueMarker = "".repeat(this.moveCounter);
this.moveAnnouncement = `Moved ${direction} ${distance} pixels${uniqueMarker}`;
};
popoverContent.addEventListener("dragstart", onDragStart);
popoverContent.addEventListener("dragend", onDragEnd);
popoverContent.addEventListener("dragmove", onDragMove);
this.dragCleanup = () => {
popoverContent.removeEventListener("dragstart", onDragStart);
popoverContent.removeEventListener("dragend", onDragEnd);
popoverContent.removeEventListener("dragmove", onDragMove);
draggable.cleanup?.();
};
});
}
firstUpdated() {
this.keydownHandler = (event) => {
if (event.key === "Escape" && this.open) this.open = false;
};
document.addEventListener("keydown", this.keydownHandler);
}
updated(changedProps) {
if (changedProps.has("floating")) if (this.floating) {
this.classList.add(`${blockClass}--floating`);
this.setupDraggable();
updateCoachmarkDetailsSignal({
name: "floating",
detail: this.floating
});
} else {
this.classList.remove(`${blockClass}--floating`);
if (this.dragCleanup) {
this.dragCleanup();
this.dragCleanup = null;
}
updateCoachmarkDetailsSignal({
name: "floating",
detail: false
});
}
if (changedProps.has("position")) {
const { x = 0, y = 0 } = this.position ?? {};
if (x !== 0 || y !== 0) this.style.transform = `translate(${x}px, ${y}px)`;
}
if (changedProps.has("open")) {
const init = {
bubbles: true,
composed: true,
detail: { open: this.open }
};
if (this.open) {
this.dispatchEvent(new CustomEvent(this.constructor.eventOpen, init));
updateCoachmarkDetailsSignal({
name: "floating",
detail: this.floating ?? false
});
if (this.floating) {
if (this.dragCleanup) {
this.dragCleanup();
this.dragCleanup = null;
}
this.setupDraggable();
}
setTimeout(() => {
requestAnimationFrame(() => {
let elementToFocus = null;
if (this.selectorPrimaryFocus) {
const raw = this.selectorPrimaryFocus.trim();
const selector = raw.startsWith("#") || raw.startsWith(".") || raw.startsWith("[") ? raw : `.${raw}`;
const found = document.querySelector(selector);
elementToFocus = found?.shadowRoot?.querySelector("button, [tabindex]") ?? found;
}
if (!elementToFocus) {
const header = (this.shadowRoot?.querySelector(`.${blockClass}--popover cds-custom-popover-content .${blockClass}--wrapper`))?.querySelector("slot")?.assignedElements({ flatten: true })?.find((el) => el.tagName.toLowerCase() === `c4p-coachmark-header`);
if (header?.shadowRoot) {
const dragButton = this.floating ? header.shadowRoot.querySelector(`.c4p--coachmark-header-drag-handle`) : null;
const closeButton = header.shadowRoot.querySelector(`.c4p--coachmark-header-close-button`);
elementToFocus = dragButton || closeButton;
}
}
if (elementToFocus) elementToFocus.focus();
});
}, 100);
} else {
this.dispatchEvent(new CustomEvent(this.constructor.eventClose, init));
updateCoachmarkDetailsSignal({
name: "floating",
detail: false
});
requestAnimationFrame(() => {
((this.shadowRoot?.querySelector("slot[name=\"trigger\"]"))?.assignedElements({ flatten: true })[0])?.focus();
});
}
}
}
render() {
const caretValue = this.caret !== void 0 ? this.caret : this.floating === true ? false : true;
return html`
<cds-custom-popover
part="popover"
exportparts="popover-container"
class="${blockClass}--popover"
?open=${this.open}
.caret=${caretValue}
?highContrast=${this.highContrast}
align=${this.align}
.dropShadow=${this.dropShadow}
-custom-popover-closed=${this.handlePopoverClosed}
>
<slot name="trigger"></slot>
<cds-custom-popover-content part="popover-content" exportparts="content">
<div
role="status"
aria-live="polite"
aria-atomic="true"
class="${"c4p"}--visually-hidden"
>
${this.floating ? this.isDragging === true ? "Drag mode active." : this.isDragging === false ? "Drag mode ended." : "" : ""}
</div>
<div
role="status"
aria-live="assertive"
aria-atomic="true"
class="${"c4p"}--visually-hidden"
>
${this.floating ? this.moveAnnouncement : ""}
</div>
<div class="${blockClass}--wrapper">
<slot></slot>
</div>
</cds-custom-popover-content>
</cds-custom-popover>
`;
}
/**
* The name of the custom event fired when this coachmark is opened.
*/
static get eventOpen() {
return `c4p-coachmark-opened`;
}
/**
* The name of the custom event fired when this coachmark is closed.
*/
static get eventClose() {
return `c4p-coachmark-closed`;
}
static {
this.styles = coachmark_default$1;
}
};
__decorate([property({
type: Boolean,
reflect: true
})], CDSCoachmark.prototype, "open", void 0);
__decorate([property({
reflect: true,
type: String
})], CDSCoachmark.prototype, "align", void 0);
__decorate([property({ type: Object })], CDSCoachmark.prototype, "position", void 0);
__decorate([property({
type: Boolean,
reflect: true
})], CDSCoachmark.prototype, "floating", void 0);
__decorate([property({
type: Boolean,
reflect: true
})], CDSCoachmark.prototype, "highContrast", void 0);
__decorate([property({
type: Boolean,
reflect: true
})], CDSCoachmark.prototype, "dropShadow", void 0);
__decorate([property({
type: Boolean,
reflect: true
})], CDSCoachmark.prototype, "caret", void 0);
__decorate([property({
attribute: "selector-primary-focus",
reflect: true
})], CDSCoachmark.prototype, "selectorPrimaryFocus", void 0);
__decorate([property({
attribute: "drag-aria-label",
reflect: true
})], CDSCoachmark.prototype, "dragAriaLabel", void 0);
__decorate([state()], CDSCoachmark.prototype, "isDragging", void 0);
__decorate([state()], CDSCoachmark.prototype, "moveAnnouncement", void 0);
CDSCoachmark = __decorate([carbonElement(`c4p-coachmark`)], CDSCoachmark);
var coachmark_default = CDSCoachmark;
//#endregion
export { blockClass, coachmark_default as default };
//# sourceMappingURL=coachmark.js.map