var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { property } from 'lit/decorators.js';
import { v4 as uuidv4 } from 'uuid';
import { Component } from '../../models';
import { debounce } from '../../utils/debounce';
import { DEFAULTS } from './screenreaderannouncer.constants';
import styles from './screenreaderannouncer.styles';
// AI-Assisted
/**
 * Module-scope refcount: tracks how many live ScreenreaderAnnouncer instances
 * reference each identity. When the count drops to zero the DOM node created
 * by createAnnouncementAriaLiveRegion() is removed.
 */
const identityRefCount = new Map();
// End AI-Assisted
/**
 * `mdc-screenreaderannouncer` can be used to announce messages with the screen reader.
 *
 * To make an announcement set `announcement` attribute on the `mdc-screenreaderannouncer` element.
 *
 * Consumers can also use the public `announce` function to trigger announcements programmatically
 * by passing an options object where `announcement` is required and all other fields are optional.
 *
 * **Internal logic**
 *
 * When the screenreader announcer is connected to the DOM, if the `identity` attribute is not
 * provided, it is set to `mdc-screenreaderannouncer-identity` and a `<div>` element with this id is created
 * in the DOM. If the `identity` attribute is provided, the identity element is used and no new element
 * is created in the DOM.
 *
 * If you provide a custom `identity`, you must ensure that the element exists and is visually hidden.
 *
 * Example CSS:
 *
 * ```css
 * #your-custom-announcer-id {
 *   clip: rect(0 0 0 0);
 *   clip-path: inset(50%);
 *   height: 1px;
 *   overflow: hidden;
 *   position: absolute;
 *   white-space: nowrap;
 *   width: 1px;
 * }
 * ```
 *
 * When the `announcement` attribute is set, the screenreader announcer will create a `<div>` element with
 * `aria-live` attribute set to the value of `data-aria-live` attribute and append it to the `identity` element.
 * After delay of `delay` milliseconds, a `<p>` element with the announcement text is appended to the `<div>` element.
 *
 * The announcement `<div>` element is removed from the DOM after `timeout` milliseconds.
 *
 * When the screen announcer component is disconnected from the DOM, all the timeouts are cleared and
 * all the announcement elements added are removed from the DOM and timeouts cleared.
 *
 * **Note**
 * 1. The default delay of 150 miliseconds is used as we dynamically generate the
 * aria-live region in the DOM and add the announcement text to it.
 * 2. If multiple `mdc-screenreaderannouncer` instances use the same `identity`, `data-aria-live`
 * for that identity is effectively determined by the first instance that creates announcements for it.
 * Changing `data-aria-live` in later instances for the same identity will not update already-created
 * live-region containers.
 * 3. If no `identity` is provided, all the screen reader components will create and use only one
 * `<div>` element with id `mdc-screenreaderannouncer-identity` in the DOM.
 *
 * Reference: https://patrickhlauke.github.io/aria/tests/live-regions/
 *
 * @tagname mdc-screenreaderannouncer
 */
class ScreenreaderAnnouncer extends Component {
    constructor() {
        super(...arguments);
        /**
         * The announcement attribute is a string that is used to announce messages to the screen reader.
         * The announcement is made when the announcement attribute is set to a non-empty string.
         *
         * @default ''
         */
        this.announcement = '';
        /**
         * The id of the element in the light dom, to which announcement elements will be appended.
         *
         * If id is not provided, it will be set to `mdc-screenreaderannouncer-identity` and
         * a div element with this id will be created in the light dom.
         */
        this.identity = '';
        /**
         * Aria live value for announcement.
         *
         * For a shared `identity`, this value should be treated as immutable after initial usage.
         * The first `mdc-screenreaderannouncer` instance that creates announcements for that
         * identity determines the `aria-live` value for the created live-region containers.
         *
         * @default 'polite'
         */
        this.dataAriaLive = DEFAULTS.ARIA_LIVE;
        /**
         * Milliseconds to wait before adding the announcement to the identiy region in
         * DOM, which will announce the message to the screen reader.
         *
         * @default 150
         */
        this.delay = DEFAULTS.DELAY;
        /**
         * Milliseconds to wait after which the announcement element will be removed from
         * identity region in DOM, causing the screen reader to not announcing the message.
         * @default 20_000
         */
        this.timeout = DEFAULTS.TIMEOUT;
        /**
         * The debounce time delay in milliseconds for announcements.
         *
         * @default 500
         */
        this.debounceTime = DEFAULTS.DEBOUNCE;
        /**
         * Whether this instance currently holds a ref for its identity.
         * @internal
         */
        this.hasIdentityRef = false;
        /**
         * Array to store timeOutIds for clearing timeouts later.
         * @internal
         */
        this.timeOutIds = [];
        /**
         * Array to store the ids of the announcement elements.
         * @internal
         */
        this.ariaLiveAnnouncementIds = [];
    }
    /**
     * Announces the given announcement to the screen reader.
     *
     * A div element with aria-live attribute set to the given ariaLive value is created
     * and a p element with the announcement text is appended to it.
     *
     * The div element is appended to the element in the DOM identified with id as
     * identity attribute.
     *
     * @param options - Announcement configuration object with the following fields:
     *   - `announcement` (required): The announcement to be made.
     *   - `delay` (optional): The delay in milliseconds before announcing the message.
     *   - `timeout` (optional): The timeout in milliseconds before removing the announcement.
     *   - `ariaLive` (optional): The aria live value for the announcement.
     */
    announce({ announcement, delay, timeout, ariaLive, }) {
        var _a;
        if (announcement.length > 0) {
            const announcementId = `mdc-screenreaderannouncer-announcement-${uuidv4()}`;
            const announcementContainer = document.createElement('div');
            announcementContainer.setAttribute('id', announcementId);
            announcementContainer.setAttribute('aria-live', ariaLive !== null && ariaLive !== void 0 ? ariaLive : this.dataAriaLive);
            (_a = this.getElementByIdAcrossShadowRoot(this.identity)) === null || _a === void 0 ? void 0 : _a.appendChild(announcementContainer);
            const timeOutId = window.setTimeout(() => {
                const announcementElement = document.createElement('p');
                announcementElement.textContent = announcement;
                announcementContainer.appendChild(announcementElement);
                this.ariaLiveAnnouncementIds.push(announcementId);
                const timeOutId = window.setTimeout(() => {
                    announcementContainer.remove();
                }, timeout !== null && timeout !== void 0 ? timeout : this.timeout);
                this.timeOutIds.push(timeOutId);
            }, delay !== null && delay !== void 0 ? delay : this.delay);
            this.timeOutIds.push(timeOutId);
        }
    }
    /**
     * Clears all timeouts and removes all announcements from the screen reader.
     */
    clearTimeOutsAndAnnouncements() {
        this.timeOutIds.forEach(timeOutId => {
            window.clearTimeout(timeOutId);
        });
        this.ariaLiveAnnouncementIds.forEach(announcementId => {
            var _a;
            (_a = this.getElementByIdAcrossShadowRoot(announcementId)) === null || _a === void 0 ? void 0 : _a.remove();
        });
    }
    /**
     * Gets an element by ID, searching in both light DOM and modal dialog shadow root.
     * @internal
     */
    getElementByIdAcrossShadowRoot(id) {
        var _a;
        const element = document.getElementById(id);
        if (element) {
            return element;
        }
        const modalDialog = this.findModalAncestor();
        return ((_a = modalDialog === null || modalDialog === void 0 ? void 0 : modalDialog.shadowRoot) === null || _a === void 0 ? void 0 : _a.getElementById(id)) || null;
    }
    /**
     * Creates a div element with id as identity attribute in the DOM.
     *
     * If the identity attribute is not provided, it is set internally to
     * `mdc-screenreaderannouncer-identity`.
     */
    createAnnouncementAriaLiveRegion() {
        let liveRegionLightDom = this.getElementByIdAcrossShadowRoot(this.identity);
        if (!liveRegionLightDom) {
            liveRegionLightDom = document.createElement('div');
            liveRegionLightDom.id = this.identity;
            const styleElement = document.createElement('style');
            styleElement.textContent = `
        .mdc-screenreaderannouncer__visually-hidden {
          clip: rect(0 0 0 0);
          clip-path: inset(50%);
          height: 1px;
          overflow: hidden;
          position: absolute;
          white-space: nowrap;
          width: 1px;
        }
      `;
            liveRegionLightDom.appendChild(styleElement);
            liveRegionLightDom.classList.add('mdc-screenreaderannouncer__visually-hidden');
            // If inside a modal dialog, append to its shadow root, otherwise to document.body
            const modalDialog = this.findModalAncestor();
            if (modalDialog === null || modalDialog === void 0 ? void 0 : modalDialog.shadowRoot) {
                modalDialog.shadowRoot.appendChild(liveRegionLightDom);
            }
            else {
                document.body.appendChild(liveRegionLightDom);
            }
        }
    }
    /**
     * Finds the closest modal dialog ancestor, traversing through shadow DOM boundaries.
     * @internal
     */
    findModalAncestor() {
        let element = this;
        while (element) {
            const modal = element.closest('[aria-modal="true"]');
            if (modal) {
                return modal;
            }
            // Traverse up through shadow DOM boundary
            const root = element.getRootNode();
            if (root instanceof ShadowRoot && root.host) {
                element = root.host;
            }
            else {
                break;
            }
        }
        return null;
    }
    /**
     * Creates a single debounced function that will read the latest this.announcement when executed.
     *
     * This function is used to debounce the announcement so that it will only be made after
     * this.debounceTime milliseconds have passed since the last time this.announcement was updated.
     */
    setupDebouncedAnnounce() {
        // create single debounced function that will read latest this.announcement when executed
        this.debouncedAnnounce = debounce(() => {
            if (this.announcement && this.announcement.length > 0) {
                this.announce({
                    announcement: this.announcement,
                    delay: this.delay,
                    timeout: this.timeout,
                    ariaLive: this.dataAriaLive,
                });
                this.announcement = '';
            }
        }, this.debounceTime);
    }
    // AI-Assisted
    /**
     * Increments the refcount for the current identity.
     */
    acquireIdentityRef() {
        var _a;
        if (this.hasIdentityRef)
            return;
        identityRefCount.set(this.identity, ((_a = identityRefCount.get(this.identity)) !== null && _a !== void 0 ? _a : 0) + 1);
        this.hasIdentityRef = true;
    }
    /**
     * Decrements the refcount for the current identity.
     * When the count reaches zero the live-region DOM node is removed.
     */
    releaseIdentityRef() {
        var _a;
        if (!this.hasIdentityRef)
            return;
        const next = ((_a = identityRefCount.get(this.identity)) !== null && _a !== void 0 ? _a : 1) - 1;
        if (next <= 0) {
            identityRefCount.delete(this.identity);
            const node = this.getElementByIdAcrossShadowRoot(this.identity);
            // Only remove nodes WE created (they carry our class).
            if (node === null || node === void 0 ? void 0 : node.classList.contains('mdc-screenreaderannouncer__visually-hidden')) {
                node.remove();
            }
        }
        else {
            identityRefCount.set(this.identity, next);
        }
        this.hasIdentityRef = false;
    }
    // End AI-Assisted
    connectedCallback() {
        super.connectedCallback();
        if (this.identity.length === 0) {
            this.identity = DEFAULTS.IDENTITY;
        }
        this.createAnnouncementAriaLiveRegion();
        this.acquireIdentityRef();
        this.setupDebouncedAnnounce();
    }
    disconnectedCallback() {
        var _a;
        super.disconnectedCallback();
        this.clearTimeOutsAndAnnouncements();
        // cancel any pending debounced action and clear DOM timeouts
        (_a = this.debouncedAnnounce) === null || _a === void 0 ? void 0 : _a.cancel();
        this.releaseIdentityRef();
    }
    updated(changedProperties) {
        var _a;
        if (changedProperties.has('identity') && this.identity.length === 0) {
            this.identity = DEFAULTS.IDENTITY;
            this.createAnnouncementAriaLiveRegion();
        }
        if (changedProperties.has('debounceTime')) {
            // Reinitiate debounced function if debounceTime changed
            this.setupDebouncedAnnounce();
        }
        if (changedProperties.has('announcement') && this.announcement && this.announcement.length > 0) {
            (_a = this.debouncedAnnounce) === null || _a === void 0 ? void 0 : _a.call(this);
        }
    }
}
ScreenreaderAnnouncer.styles = [...Component.styles, ...styles];
__decorate([
    property({ type: String, reflect: true }),
    __metadata("design:type", String)
], ScreenreaderAnnouncer.prototype, "announcement", void 0);
__decorate([
    property({ type: String, reflect: true }),
    __metadata("design:type", String)
], ScreenreaderAnnouncer.prototype, "identity", void 0);
__decorate([
    property({ type: String, reflect: true, attribute: 'data-aria-live' }),
    __metadata("design:type", String)
], ScreenreaderAnnouncer.prototype, "dataAriaLive", void 0);
__decorate([
    property({ type: Number, reflect: true }),
    __metadata("design:type", Number)
], ScreenreaderAnnouncer.prototype, "delay", void 0);
__decorate([
    property({ type: Number, reflect: true }),
    __metadata("design:type", Number)
], ScreenreaderAnnouncer.prototype, "timeout", void 0);
__decorate([
    property({ type: Number, reflect: true, attribute: 'debounce-time' }),
    __metadata("design:type", Number)
], ScreenreaderAnnouncer.prototype, "debounceTime", void 0);
export default ScreenreaderAnnouncer;
