/* *
 *
 *  (c) 2009-2026 Highsoft AS
 *  Author: Øystein Moseng
 *
 *  Accessibility component for chart info region and table.
 *
 *  A commercial license may be required depending on use.
 *  See www.highcharts.com/license
 *
 *
 * */
'use strict';
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
import A11yI18n from '../A11yI18n.js';
import AccessibilityComponent from '../AccessibilityComponent.js';
import Announcer from '../Utils/Announcer.js';
import AnnotationsA11y from './AnnotationsA11y.js';
var getAnnotationsInfoHTML = AnnotationsA11y.getAnnotationsInfoHTML;
import AST from '../../Core/Renderer/HTML/AST.js';
import CU from '../Utils/ChartUtilities.js';
var getAxisDescription = CU.getAxisDescription, getAxisRangeDescription = CU.getAxisRangeDescription, getChartTitle = CU.getChartTitle, unhideChartElementFromAT = CU.unhideChartElementFromAT;
import F from '../../Core/Templating.js';
var format = F.format;
import H from '../../Core/Globals.js';
var doc = H.doc;
import HU from '../Utils/HTMLUtilities.js';
var addClass = HU.addClass, getElement = HU.getElement, getHeadingTagNameForElement = HU.getHeadingTagNameForElement, stripHTMLTagsFromString = HU.stripHTMLTagsFromString, visuallyHideElement = HU.visuallyHideElement;
import U from '../../Core/Utilities.js';
var attr = U.attr, pick = U.pick, replaceNested = U.replaceNested;
/* *
 *
 *  Functions
 *
 * */
/* eslint-disable valid-jsdoc */
/**
 * @private
 */
function getTableSummary(chart) {
    return chart.langFormat('accessibility.table.tableSummary', { chart: chart });
}
/**
 * @private
 */
function getTypeDescForMapChart(chart, formatContext) {
    return formatContext.mapTitle ?
        chart.langFormat('accessibility.chartTypes.mapTypeDescription', formatContext) :
        chart.langFormat('accessibility.chartTypes.unknownMap', formatContext);
}
/**
 * @private
 */
function getTypeDescForCombinationChart(chart, formatContext) {
    return chart.langFormat('accessibility.chartTypes.combinationChart', formatContext);
}
/**
 * @private
 */
function getTypeDescForEmptyChart(chart, formatContext) {
    return chart.langFormat('accessibility.chartTypes.emptyChart', formatContext);
}
/**
 * @private
 */
function buildTypeDescriptionFromSeries(chart, types, context) {
    var firstType = types[0], typeExplanation = chart.langFormat('accessibility.seriesTypeDescriptions.' + firstType, context), multi = chart.series && chart.series.length < 2 ? 'Single' : 'Multiple';
    return (chart.langFormat('accessibility.chartTypes.' + firstType + multi, context) ||
        chart.langFormat('accessibility.chartTypes.default' + multi, context)) + (typeExplanation ? ' ' + typeExplanation : '');
}
/**
 * Return simplified explanation of chart type. Some types will not be
 * familiar to most users, but in those cases we try to add an explanation
 * of the type.
 *
 * @private
 * @function Highcharts.Chart#getTypeDescription
 * @param {Array<string>} types The series types in this chart.
 * @return {string} The text description of the chart type.
 */
function getTypeDescription(chart, types) {
    var firstType = types[0], firstSeries = chart.series && chart.series[0] || {}, mapTitle = chart.mapView && chart.mapView.geoMap &&
        chart.mapView.geoMap.title, formatContext = {
        numSeries: chart.series.length,
        numPoints: firstSeries.points && firstSeries.points.length,
        chart: chart,
        mapTitle: mapTitle
    };
    if (!firstType) {
        return getTypeDescForEmptyChart(chart, formatContext);
    }
    if (firstType === 'map' || firstType === 'tiledwebmap') {
        return getTypeDescForMapChart(chart, formatContext);
    }
    if (chart.types.length > 1) {
        return getTypeDescForCombinationChart(chart, formatContext);
    }
    return buildTypeDescriptionFromSeries(chart, types, formatContext);
}
/**
 * @private
 */
function stripEmptyHTMLTags(str) {
    // Scan alert #[71]: Loop for nested patterns
    return replaceNested(str, [/<([\w\-.:!]+)\b[^<>]*>\s*<\/\1>/g, '']);
}
/* *
 *
 *  Class
 *
 * */
/**
 * The InfoRegionsComponent class
 *
 * @private
 * @class
 * @name Highcharts.InfoRegionsComponent
 */
var InfoRegionsComponent = /** @class */ (function (_super) {
    __extends(InfoRegionsComponent, _super);
    function InfoRegionsComponent() {
        /* *
         *
         *  Properties
         *
         * */
        var _this = _super !== null && _super.apply(this, arguments) || this;
        _this.screenReaderSections = {};
        return _this;
    }
    /* *
     *
     *  Functions
     *
     * */
    /* eslint-disable valid-jsdoc */
    /**
     * Init the component
     * @private
     */
    InfoRegionsComponent.prototype.init = function () {
        var chart = this.chart;
        var component = this;
        this.initRegionsDefinitions();
        this.addEvent(chart, 'afterGetTableAST', function (e) {
            component.onDataTableCreated(e);
        });
        this.addEvent(chart, 'afterViewData', function (e) {
            if (e.wasHidden) {
                component.dataTableDiv = e.element;
                // Use a small delay to give browsers & AT time to
                // register the new table.
                setTimeout(function () {
                    component.focusDataTable();
                }, 300);
            }
        });
        this.addEvent(chart, 'afterHideData', function () {
            if (component.viewDataTableButton) {
                component.viewDataTableButton
                    .setAttribute('aria-expanded', 'false');
            }
        });
        if (chart.exporting) {
            // Needed when print logic in exporting does not trigger
            // rerendering thus repositioning of screen reader DOM elements
            // (#21554)
            this.addEvent(chart, 'afterPrint', function () {
                component.updateAllScreenReaderSections();
            });
        }
        this.announcer = new Announcer(chart, 'assertive');
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.initRegionsDefinitions = function () {
        var component = this, accessibilityOptions = this.chart.options.accessibility;
        this.screenReaderSections = {
            before: {
                element: null,
                buildContent: function (chart) {
                    var formatter = accessibilityOptions.screenReaderSection
                        .beforeChartFormatter;
                    return formatter ? formatter(chart) :
                        component.defaultBeforeChartFormatter(chart);
                },
                insertIntoDOM: function (el, chart) {
                    chart.renderTo.insertBefore(el, chart.renderTo.firstChild);
                },
                afterInserted: function () {
                    if (typeof component.sonifyButtonId !== 'undefined') {
                        component.initSonifyButton(component.sonifyButtonId);
                    }
                    if (typeof component.dataTableButtonId !== 'undefined') {
                        component.initDataTableButton(component.dataTableButtonId);
                    }
                }
            },
            after: {
                element: null,
                buildContent: function (chart) {
                    var formatter = accessibilityOptions.screenReaderSection
                        .afterChartFormatter;
                    return formatter ? formatter(chart) :
                        component.defaultAfterChartFormatter();
                },
                insertIntoDOM: function (el, chart) {
                    chart.renderTo.insertBefore(el, chart.container.nextSibling);
                },
                afterInserted: function () {
                    if (component.chart.accessibility &&
                        accessibilityOptions.keyboardNavigation.enabled) {
                        component.chart.accessibility
                            .keyboardNavigation.updateExitAnchor(); // #15986
                    }
                }
            }
        };
    };
    /**
     * Called on chart render. Have to update the sections on render, in order
     * to get a11y info from series.
     */
    InfoRegionsComponent.prototype.onChartRender = function () {
        this.linkedDescriptionElement = this.getLinkedDescriptionElement();
        this.setLinkedDescriptionAttrs();
        this.updateAllScreenReaderSections();
    };
    InfoRegionsComponent.prototype.updateAllScreenReaderSections = function () {
        var component = this;
        Object.keys(this.screenReaderSections).forEach(function (regionKey) {
            component.updateScreenReaderSection(regionKey);
        });
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getLinkedDescriptionElement = function () {
        var chartOptions = this.chart.options, linkedDescOption = chartOptions.accessibility.linkedDescription;
        if (!linkedDescOption) {
            return;
        }
        if (typeof linkedDescOption !== 'string') {
            return linkedDescOption;
        }
        var query = format(linkedDescOption, this.chart), queryMatch = doc.querySelectorAll(query);
        if (queryMatch.length === 1) {
            return queryMatch[0];
        }
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.setLinkedDescriptionAttrs = function () {
        var el = this.linkedDescriptionElement;
        if (el) {
            el.setAttribute('aria-hidden', 'true');
            addClass(el, 'highcharts-linked-description');
        }
    };
    /**
     * @private
     * @param {string} regionKey
     * The name/key of the region to update
     */
    InfoRegionsComponent.prototype.updateScreenReaderSection = function (regionKey) {
        var chart = this.chart;
        var region = this.screenReaderSections[regionKey];
        var content = region.buildContent(chart);
        var sectionDiv = region.element = (region.element || this.createElement('div'));
        var hiddenDiv = (sectionDiv.firstChild || this.createElement('div'));
        if (content) {
            this.setScreenReaderSectionAttribs(sectionDiv, regionKey);
            AST.setElementHTML(hiddenDiv, content);
            sectionDiv.appendChild(hiddenDiv);
            region.insertIntoDOM(sectionDiv, chart);
            if (chart.styledMode) {
                addClass(hiddenDiv, 'highcharts-visually-hidden');
            }
            else {
                visuallyHideElement(hiddenDiv);
            }
            unhideChartElementFromAT(chart, hiddenDiv);
            if (region.afterInserted) {
                region.afterInserted();
            }
        }
        else {
            if (sectionDiv.parentNode) {
                sectionDiv.parentNode.removeChild(sectionDiv);
            }
            region.element = null;
        }
    };
    /**
     * Apply a11y attributes to a screen reader info section
     * @private
     * @param {Highcharts.HTMLDOMElement} sectionDiv The section element
     * @param {string} regionKey Name/key of the region we are setting attrs for
     */
    InfoRegionsComponent.prototype.setScreenReaderSectionAttribs = function (sectionDiv, regionKey) {
        var chart = this.chart, labelText = chart.langFormat('accessibility.screenReaderSection.' + regionKey +
            'RegionLabel', { chart: chart, chartTitle: getChartTitle(chart) }), sectionId = "highcharts-screen-reader-region-".concat(regionKey, "-").concat(chart.index);
        attr(sectionDiv, {
            id: sectionId,
            'aria-label': labelText || void 0
        });
        // Sections are wrapped to be positioned relatively to chart in case
        // elements inside are tabbed to.
        sectionDiv.style.position = 'relative';
        if (labelText) {
            sectionDiv.setAttribute('role', chart.options.accessibility.landmarkVerbosity === 'all' ?
                'region' : 'group');
        }
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.defaultBeforeChartFormatter = function () {
        var _a;
        var chart = this.chart, format = chart.options.accessibility.screenReaderSection
            .beforeChartFormat;
        if (!format) {
            return '';
        }
        var axesDesc = this.getAxesDescription(), shouldHaveSonifyBtn = (chart.sonify &&
            chart.options.sonification &&
            chart.options.sonification.enabled), sonifyButtonId = 'highcharts-a11y-sonify-data-btn-' +
            chart.index, dataTableButtonId = 'hc-linkto-highcharts-data-table-' +
            chart.index, annotationsList = getAnnotationsInfoHTML(chart), annotationsTitleStr = chart.langFormat('accessibility.screenReaderSection.annotations.heading', { chart: chart }), context = {
            headingTagName: getHeadingTagNameForElement(chart.renderTo),
            chartTitle: getChartTitle(chart),
            typeDescription: this.getTypeDescriptionText(),
            chartSubtitle: this.getSubtitleText(),
            chartLongdesc: this.getLongdescText(),
            xAxisDescription: axesDesc.xAxis,
            yAxisDescription: axesDesc.yAxis,
            playAsSoundButton: shouldHaveSonifyBtn ?
                this.getSonifyButtonText(sonifyButtonId) : '',
            viewTableButton: ((_a = chart.exporting) === null || _a === void 0 ? void 0 : _a.getCSV) ?
                this.getDataTableButtonText(dataTableButtonId) : '',
            annotationsTitle: annotationsList ? annotationsTitleStr : '',
            annotationsList: annotationsList
        }, formattedString = A11yI18n.i18nFormat(format, context, chart);
        this.dataTableButtonId = dataTableButtonId;
        this.sonifyButtonId = sonifyButtonId;
        return stripEmptyHTMLTags(formattedString);
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.defaultAfterChartFormatter = function () {
        var chart = this.chart;
        var format = chart.options.accessibility.screenReaderSection
            .afterChartFormat;
        if (!format) {
            return '';
        }
        var context = { endOfChartMarker: this.getEndOfChartMarkerText() };
        var formattedString = A11yI18n.i18nFormat(format, context, chart);
        return stripEmptyHTMLTags(formattedString);
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getLinkedDescription = function () {
        var el = this.linkedDescriptionElement, content = el && el.innerHTML || '';
        return stripHTMLTagsFromString(content, this.chart.renderer.forExport);
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getLongdescText = function () {
        var chartOptions = this.chart.options, captionOptions = chartOptions.caption, captionText = captionOptions && captionOptions.text, linkedDescription = this.getLinkedDescription();
        return (chartOptions.accessibility.description ||
            linkedDescription ||
            captionText ||
            '');
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getTypeDescriptionText = function () {
        var chart = this.chart;
        return chart.types ?
            chart.options.accessibility.typeDescription ||
                getTypeDescription(chart, chart.types) : '';
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getDataTableButtonText = function (buttonId) {
        var chart = this.chart, buttonText = chart.langFormat('accessibility.table.viewAsDataTableButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
        return '<button id="' + buttonId + '">' + buttonText + '</button>';
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getSonifyButtonText = function (buttonId) {
        var chart = this.chart;
        if (chart.options.sonification &&
            chart.options.sonification.enabled === false) {
            return '';
        }
        var buttonText = chart.langFormat('accessibility.sonification.playAsSoundButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
        return '<button id="' + buttonId + '">' + buttonText + '</button>';
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getSubtitleText = function () {
        var subtitle = (this.chart.options.subtitle);
        return stripHTMLTagsFromString(subtitle && subtitle.text || '', this.chart.renderer.forExport);
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getEndOfChartMarkerText = function () {
        var endMarkerId = "highcharts-end-of-chart-marker-".concat(this.chart.index), endMarker = getElement(endMarkerId);
        if (endMarker) {
            return endMarker.outerHTML;
        }
        var chart = this.chart, markerText = chart.langFormat('accessibility.screenReaderSection.endOfChartMarker', { chart: chart }), id = 'highcharts-end-of-chart-marker-' + chart.index;
        return '<div id="' + id + '">' + markerText + '</div>';
    };
    /**
     * @private
     * @param {Highcharts.Dictionary<string>} e
     */
    InfoRegionsComponent.prototype.onDataTableCreated = function (e) {
        var chart = this.chart;
        if (chart.options.accessibility.enabled) {
            if (this.viewDataTableButton) {
                this.viewDataTableButton.setAttribute('aria-expanded', 'true');
            }
            var attributes = e.tree.attributes || {};
            attributes.tabindex = -1;
            attributes.summary = getTableSummary(chart);
            e.tree.attributes = attributes;
        }
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.focusDataTable = function () {
        var tableDiv = this.dataTableDiv, table = tableDiv && tableDiv.getElementsByTagName('table')[0];
        if (table && table.focus) {
            table.focus();
        }
    };
    /**
     * @private
     * @param {string} sonifyButtonId
     */
    InfoRegionsComponent.prototype.initSonifyButton = function (sonifyButtonId) {
        var _this = this;
        var el = this.sonifyButton = getElement(sonifyButtonId);
        var chart = this.chart;
        var defaultHandler = function (e) {
            if (el) {
                el.setAttribute('aria-hidden', 'true');
                el.setAttribute('aria-label', '');
            }
            e.preventDefault();
            e.stopPropagation();
            var announceMsg = chart.langFormat('accessibility.sonification.playAsSoundClickAnnouncement', { chart: chart });
            _this.announcer.announce(announceMsg);
            setTimeout(function () {
                if (el) {
                    el.removeAttribute('aria-hidden');
                    el.removeAttribute('aria-label');
                }
                if (chart.sonify) {
                    chart.sonify();
                }
            }, 1000); // Delay to let screen reader speak the button press
        };
        if (el && chart) {
            el.setAttribute('tabindex', -1);
            el.onclick = function (e) {
                var onPlayAsSoundClick = (chart.options.accessibility &&
                    chart.options.accessibility.screenReaderSection
                        .onPlayAsSoundClick);
                (onPlayAsSoundClick || defaultHandler).call(this, e, chart);
            };
        }
    };
    /**
     * Set attribs and handlers for default viewAsDataTable button if exists.
     * @private
     * @param {string} tableButtonId
     */
    InfoRegionsComponent.prototype.initDataTableButton = function (tableButtonId) {
        var el = this.viewDataTableButton = getElement(tableButtonId), chart = this.chart, tableId = tableButtonId.replace('hc-linkto-', '');
        if (el) {
            attr(el, {
                tabindex: -1,
                'aria-expanded': !!getElement(tableId)
            });
            el.onclick = chart.options.accessibility
                .screenReaderSection.onViewDataTableClick ||
                function () {
                    var _a;
                    (_a = chart.exporting) === null || _a === void 0 ? void 0 : _a.viewData();
                };
        }
    };
    /**
     * Return object with text description of each of the chart's axes.
     * @private
     */
    InfoRegionsComponent.prototype.getAxesDescription = function () {
        var chart = this.chart, shouldDescribeColl = function (collectionKey, defaultCondition) {
            var axes = chart[collectionKey];
            return axes.length > 1 || axes[0] &&
                pick(axes[0].options.accessibility &&
                    axes[0].options.accessibility.enabled, defaultCondition);
        }, hasNoMap = !!chart.types &&
            chart.types.indexOf('map') < 0 &&
            chart.types.indexOf('treemap') < 0 &&
            chart.types.indexOf('tilemap') < 0, hasCartesian = !!chart.hasCartesianSeries, showXAxes = shouldDescribeColl('xAxis', !chart.angular && hasCartesian && hasNoMap), showYAxes = shouldDescribeColl('yAxis', hasCartesian && hasNoMap), desc = {};
        if (showXAxes) {
            desc.xAxis = this.getAxisDescriptionText('xAxis');
        }
        if (showYAxes) {
            desc.yAxis = this.getAxisDescriptionText('yAxis');
        }
        return desc;
    };
    /**
     * @private
     */
    InfoRegionsComponent.prototype.getAxisDescriptionText = function (collectionKey) {
        var chart = this.chart;
        var axes = chart[collectionKey];
        return chart.langFormat('accessibility.axis.' + collectionKey + 'Description' + (axes.length > 1 ? 'Plural' : 'Singular'), {
            chart: chart,
            names: axes.map(function (axis) {
                return getAxisDescription(axis);
            }),
            ranges: axes.map(function (axis) {
                return getAxisRangeDescription(axis);
            }),
            numAxes: axes.length
        });
    };
    /**
     * Remove component traces
     */
    InfoRegionsComponent.prototype.destroy = function () {
        if (this.announcer) {
            this.announcer.destroy();
        }
    };
    return InfoRegionsComponent;
}(AccessibilityComponent));
/* *
 *
 *  Default Export
 *
 * */
export default InfoRegionsComponent;
