/*
* 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()}
)}
)
}