import { h, Component, Element, Prop, State, Watch } from '@stencil/core'; import { Language } from '../../utils/common/language-types'; import { ConsoleMessageClass } from '../../utils/console-message/console-message'; import { validateLanguage } from '../../utils/validation/validation-functions'; import translations from '../../translations/global.i18n.json'; import { HeadingLevel } from '../../utils/common/common.interface'; import { validateValueAgainstArray } from '../../utils/validation/validation-functions'; import { TaskStatus } from '../../utils/common/task-statuses.enum'; export type TaskListHeadingLevel = 'h1' | Exclude; /** * Ontario Task List groups and summarizes related tasks. * * For component guidance, see: * - https://designsystem.ontario.ca/components/detail/task-list.html * - https://designsystem.ontario.ca/developer-docs/components/ontario-task-list/ */ @Component({ tag: 'ontario-task-list', styleUrl: 'ontario-task-list.scss', shadow: true, }) export class OntarioTaskList { @Element() el: HTMLElement; /** * The label prop used for the task list heading. */ @Prop() label: string; /** * Allows consumers to define the heading level for the task list component. * * Accepts 'h1', 'h2', 'h3' or 'h4'. Default is 'h2'. */ @Prop() headingLevel: TaskListHeadingLevel = 'h2'; /** * The language of the component. * This is used for translations, and is by default set through event listeners checking for a language property from the header. If none are passed, it will default to English. */ @Prop({ mutable: true }) language?: Language = 'en'; /** * State to track the number of completed tasks. */ @State() completedTasks: number = 0; /** * State to track the total number of tasks. */ @State() totalTasks: number = 0; /** * Class-level constant for task count delay in milliseconds. * * This delay ensures that the `ontario-task` custom elements are fully upgraded and rendered * in the DOM before the `countTasks` method is executed. Without this delay, the component * might attempt to query or count tasks before they are properly initialized, leading to * inaccurate task counts. */ private static readonly TASK_COUNT_DELAY_MS = 50; /** * Watch for changes in `headingLevel` prop to validate its value. */ @Watch('headingLevel') validateHeadingLevel(newValue: string) { const allowedValues: TaskListHeadingLevel[] = ['h1', 'h2', 'h3', 'h4', 'h5']; // Validate the new value against the allowed values const isValid = validateValueAgainstArray(newValue, allowedValues); if (!isValid) { const message = new ConsoleMessageClass(); message .addDesignSystemTag() .addMonospaceText(' headingLevel ') .addRegularText('on') .addMonospaceText(' ') .addRegularText('was set to an invalid value; only ') .addMonospaceText(allowedValues.join(', ')) .addRegularText(' are supported. The default value ') .addMonospaceText('h2') .addRegularText(' is assumed.') .printMessage(); this.headingLevel = 'h2'; } } /** * Counts the total tasks and completed tasks by querying for `ontario-task` elements. */ countTasks() { const slot = this.el.shadowRoot?.querySelector('slot'); const tasks = slot ? (slot.assignedElements().filter((el) => el.tagName.toLowerCase() === 'ontario-task') as HTMLElement[]) : []; this.totalTasks = tasks.length; this.completedTasks = tasks.filter((task) => { // Get the status directly from the light DOM const status = task.getAttribute('data-task-status'); return status === TaskStatus.Completed; }).length; } /** * Lifecycle method: before the component loads, validate heading level. */ async componentWillLoad() { this.validateHeadingLevel(this.headingLevel); } /** * Ensure tasks are counted after custom elements are fully upgraded. */ connectedCallback() { // Wait for the custom element to be fully defined before counting customElements.whenDefined('ontario-task').then(() => { setTimeout(() => { this.countTasks(); }, OntarioTaskList.TASK_COUNT_DELAY_MS); }); } /** * Use slotchange and MutationObserver to track changes in slot content. */ componentDidLoad() { const slot = this.el.shadowRoot?.querySelector('slot'); if (slot) { slot.addEventListener('slotchange', () => { this.countTasks(); }); } // Observe changes to the light DOM for accurate counting const observer = new MutationObserver((_mutations) => { this.countTasks(); }); observer.observe(this.el, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-task-status'], }); } componentDidRender() { setTimeout(() => { this.countTasks(); }, OntarioTaskList.TASK_COUNT_DELAY_MS); } render() { // Resolve the language to ensure valid translations are used. const resolvedLanguage = validateLanguage(this.language); const headingProps = { class: { 'ontario-task-list__heading': true }, }; return (
{h(this.headingLevel, headingProps, this.label)}

{translations.taskGroup.completed[resolvedLanguage]}  {this.completedTasks}  {translations.taskGroup.outOf[resolvedLanguage]}  {this.totalTasks}  {translations.taskGroup.tasks[resolvedLanguage]}

); } }