import { h, Component, Prop, Watch, State, Listen, Element } from '@stencil/core'; import { ConsoleMessageClass } from '../../utils/console-message/console-message'; import { TaskStatus, TaskBadgeColour, TaskToBadgeColour } from '../../utils/common/task-statuses.enum'; import { validateLanguage, validateValueAgainstArray } from '../../utils/validation/validation-functions'; import { Hint } from '../../utils/common/common.interface'; import { Language } from '../../utils/common/language-types'; import { constructHintTextObject } from '../../utils/components/hints/hints'; import translations from '../../translations/global.i18n.json'; import { HeadingLevel } from '../../utils/common/common.interface'; import { HeaderLanguageToggleEventDetails } from '../../utils/events/common-events.interface'; export type TaskHeadingLevel = Extract; /** * Ontario Task represents an individual task item and status within a task list. * * For component guidance, see: * - https://designsystem.ontario.ca/components/detail/task-list.html * - https://designsystem.ontario.ca/developer-docs/components/ontario-task/ */ @Component({ tag: 'ontario-task', styleUrl: 'ontario-task.scss', shadow: true, }) export class OntarioTask { @Element() el: HTMLElement; // Reference to the ontario-hint-text element for this task. hintTextRef?: HTMLOntarioHintTextElement | undefined; /** * Specifies the label of the task. * * This is required to provide the name of the task. */ @Prop() label: string; /** * A unique id for the task. * * This is required. */ @Prop() taskId: string; /** * Specifies an optional link associated with the task. * * If provided, clicking the task will navigate to this URL. */ @Prop() link?: string; /** * Disables the task link when set to `true`. * * Default is `false`, meaning the link will be active if provided. */ @Prop() deactivateLink: boolean = false; /** * 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 no language is passed, it will default to English. */ @Prop({ mutable: true }) language?: Language; /** * Used to include the ontario-hint-text component for the task. * * This is optional. */ @Prop({ mutable: true }) hintText?: string | Hint; /** * Defines the status of the task. * * Falls back to the default status if not provided, or if an unrecognized value is passed. */ @Prop() taskStatus: TaskStatus = TaskStatus.NotStarted; /** * Allows consumers to define the heading level for the task label. * * Accepts 'h2', 'h3' or 'h4'. Default is 'h3'. */ @Prop() headingLevel: TaskHeadingLevel = 'h3'; /** * The hint text options are re-assigned to the internalHintText state. */ @State() private internalHintText: Hint; /** * Mutable variable, for internal use only. * * Set the task's status state depending on validation result. */ @State() private taskStatusState: TaskStatus = TaskStatus.NotStarted; /** * Watch for changes in `headingLevel` prop to validate its value. */ @Watch('headingLevel') validateHeadingLevel(newValue: string) { const allowedValues: TaskHeadingLevel[] = ['h2', 'h3', 'h4']; // 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('h3') .addRegularText(' is assumed.') .printMessage(); this.headingLevel = 'h3'; } } /** * Watch for changes in `hintText` prop and parse it if available. * * If a `hintText` prop is passed, the `constructHintTextObject` function will convert it to the correct format, * and the result will be stored in the `internalHintText` state. */ @Watch('hintText') private parseHintText() { if (this.hintText) { const hintTextObject = constructHintTextObject(this.hintText); this.internalHintText = hintTextObject; } } /** * Watch for changes in `taskStatus` prop to validate its value. */ @Watch('taskStatus') validateTaskStatus(newValue: TaskStatus) { const validStatuses = Object.values(TaskStatus); const isValidStatus = validStatuses.includes(newValue); if (isValidStatus) { this.taskStatusState = newValue as TaskStatus; } else { this.taskStatusState = this.warnAndGetDefaultTaskStatus(); } // Update the `data-task-status` attribute this.el.setAttribute('data-task-status', this.taskStatusState); } /** * Listen for app language settings on the window to update the component language. */ @Listen('setAppLanguage', { target: 'window' }) handleSetAppLanguage(event: CustomEvent) { if (!this.language) { this.language = validateLanguage(event); } } /** * Handles an update to the language should the user request a language update from the language toggle. * @param {CustomEvent} - The language that has been selected. */ @Listen('headerLanguageToggled', { target: 'window' }) handleHeaderLanguageToggled(event: CustomEvent) { this.language = validateLanguage(event.detail.newLanguage); } /** * Determines the badge colour based on the current `taskStatusState`. * * @returns {TaskBadgeColour} */ private getBadgeColour(): TaskBadgeColour { return TaskToBadgeColour[this.taskStatusState] || 'grey'; } /** * Provides a translated task status string based on the current language. */ private getTranslatedTaskStatus(): string { const resolvedLanguage = validateLanguage(this.language); return translations.taskStatus[this.taskStatusState][resolvedLanguage]; } /** * Renders hint text if available. */ private renderHintText() { if (this.internalHintText) { return ( (this.hintTextRef = el)} > ); } return null; } /** * Returns the class name(s) for the task label. */ private getClass(): string { return ['ontario-task__label', this.hintText && 'ontario-task__hint-text--true'].filter(Boolean).join(' '); } /** * Logs a warning to the console if the `taskStatus` prop is set to an invalid value. * * This function informs developers that the provided `taskStatus` is not recognized * and resets the status to the default value of `'notStarted'`. The warning message * specifies the valid task statuses to help guide correct usage. * * @returns The default task status `'notStarted'`. */ private warnAndGetDefaultTaskStatus(): TaskStatus { const validStatuses = Object.values(TaskStatus).join(', '); const message = new ConsoleMessageClass(); message .addDesignSystemTag() .addMonospaceText(' taskStatus ') .addRegularText('on') .addMonospaceText(' ') .addRegularText('was set to an invalid taskStatus; only ') .addMonospaceText(validStatuses) .addRegularText(' are supported. The default taskStatus ') .addMonospaceText(TaskStatus.NotStarted) .addRegularText(' is assumed.') .printMessage(); return TaskStatus.NotStarted; } /** * Renders the task label and status content. * * This includes dynamically rendering the heading element (h2, h3, or h4) based on the `headingLevel` prop, * as well as the badge indicating the task status. */ private renderTaskContent() { const headingProps: any = { id: `task-label--${this.taskId}`, class: this.getClass(), }; return (
{h(this.headingLevel, headingProps, this.label)} {this.renderHintText()}
{this.taskStatusState && ( {this.getTranslatedTaskStatus()} )}
); } /** * After the component loads, retrieve the hint text ID (if available) from the hintText component, * and set it for the `aria-describedby` attribute. */ async componentDidLoad() { if (this.hintTextRef) { let hintTextId = this.hintTextRef.elementId; if (!hintTextId) { hintTextId = `hint-text--${this.taskId}`; this.hintTextRef.elementId = hintTextId; } } } /** * Lifecycle method: before the component loads, parse the hint text and * validate language and task status and heading level. */ async componentWillLoad() { this.parseHintText(); this.language = validateLanguage(this.language); this.validateHeadingLevel(this.headingLevel); // Ensure the taskStatus is set initially based on the prop this.validateTaskStatus(this.taskStatus); } render() { const isLinkActive = this.link && !this.deactivateLink; const taskStatusClass = `ontario-task-status--${this.taskStatusState.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`; const describedBy = this.internalHintText ? `hint-text--${this.taskId}` : undefined; return (
  • {isLinkActive ? ( {this.renderTaskContent()} ) : (
    {this.renderTaskContent()}
    )}
  • ); } }