/* * Copyright (C) 2025 - present Instructure, Inc. * * This file is part of Canvas. * * Canvas is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, version 3 of the License. * * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along * with this program. If not, see . */ import {Alert} from '@instructure/ui-alerts' import {Button} from '@instructure/ui-buttons' import {Flex} from '@instructure/ui-flex' import {Heading} from '@instructure/ui-heading' import {Text} from '@instructure/ui-text' import {useEffect, useRef, useState} from 'react' import CanvasSelect from '../shared/CanvasSelect' import {trackPendoEvent} from '../utils/trackPendoEvent' import { type ClosedCaptionPanelTranslations, defaultClosedCaptionPanelTranslations, } from './translations' interface AutoCaptioningProps { onCancel: () => void onPrimary: (selectedLanguageId: string) => void liveRegion: () => HTMLElement | null languages: {id: string; label: string}[] mountNode?: HTMLElement | (() => HTMLElement | null) onDirtyStateChanged?: (isDirty: boolean) => void elementRef?: (el: Element | null) => void translations?: ClosedCaptionPanelTranslations } const LANGUAGE_ERROR_ID = 'cc-asr-language-error' export const AutoCaptioning = ({ onCancel, onPrimary, liveRegion, languages, mountNode, onDirtyStateChanged, elementRef, translations = defaultClosedCaptionPanelTranslations, }: AutoCaptioningProps) => { const [selectedLanguageId, setSelectedLanguageId] = useState('') const [showLanguageError, setShowLanguageError] = useState(false) const languageInputRef = useRef(null) useEffect(() => { languageInputRef.current?.focus() }, []) const handleLanguageChange = (_event: React.SyntheticEvent, languageId: string) => { if (languageId) { setSelectedLanguageId(languageId) setShowLanguageError(false) } onDirtyStateChanged?.(Boolean(languageId)) } const handlePrimaryClick = () => { if (!selectedLanguageId) { trackPendoEvent('canvas_caption_validation_error', { flow_type: 'request_auto', error_type: 'missing_language', }) setShowLanguageError(true) languageInputRef.current?.focus() return } onPrimary(selectedLanguageId) } return ( {translations.autoCaptioningHeading()} {translations.autoCaptioningDescription()} { languageInputRef.current = el }} label={translations.languageSpokenLabel()} placeholder={translations.selectLanguagePlaceholder()} value={selectedLanguageId} mountNode={mountNode} translatedStrings={{ USE_ARROWS: translations.useArrowKeys(), LIST_COLLAPSED: translations.listCollapsed(), LIST_EXPANDED: translations.listExpanded(), OPTION_SELECTED: '{option} selected.', }} onChange={handleLanguageChange} aria-invalid={showLanguageError ? 'true' : undefined} aria-describedby={showLanguageError ? LANGUAGE_ERROR_ID : undefined} messages={ showLanguageError ? [ { type: 'newError', text: translations.pleaseSelectLanguage(), }, ] : [] } liveRegion={liveRegion} > {languages.map(option => ( // @ts-expect-error - CanvasSelect.Option is a JS component without TS definitions {option.label} ))} {showLanguageError && ( {translations.pleaseSelectLanguage()} )} ) }