diff --git a/.eslintrc.js b/.eslintrc.js
index 9e749f1436..74c9677e37 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -340,6 +340,7 @@ module.exports = {
{
files: [
'*.test.js',
+ '*.test.ts',
'test/endtoend/*.js',
'bin/**.js',
],
diff --git a/packages/@uppy/webcam/src/CameraIcon.jsx b/packages/@uppy/webcam/src/CameraIcon.jsx
deleted file mode 100644
index add8393463..0000000000
--- a/packages/@uppy/webcam/src/CameraIcon.jsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { h } from 'preact'
-
-export default () => {
- return (
-
- )
-}
diff --git a/packages/@uppy/webcam/src/CameraIcon.tsx b/packages/@uppy/webcam/src/CameraIcon.tsx
new file mode 100644
index 0000000000..09084a86b6
--- /dev/null
+++ b/packages/@uppy/webcam/src/CameraIcon.tsx
@@ -0,0 +1,19 @@
+import { h, type ComponentChild } from 'preact'
+
+export default function CameraIcon(): ComponentChild {
+ return (
+
+ )
+}
diff --git a/packages/@uppy/webcam/src/CameraScreen.jsx b/packages/@uppy/webcam/src/CameraScreen.jsx
deleted file mode 100644
index 479a67b9c8..0000000000
--- a/packages/@uppy/webcam/src/CameraScreen.jsx
+++ /dev/null
@@ -1,119 +0,0 @@
-/* eslint-disable jsx-a11y/media-has-caption */
-import { h, Component } from 'preact'
-import SnapshotButton from './SnapshotButton.jsx'
-import RecordButton from './RecordButton.jsx'
-import RecordingLength from './RecordingLength.jsx'
-import VideoSourceSelect from './VideoSourceSelect.jsx'
-import SubmitButton from './SubmitButton.jsx'
-import DiscardButton from './DiscardButton.jsx'
-
-function isModeAvailable (modes, mode) {
- return modes.includes(mode)
-}
-
-class CameraScreen extends Component {
- componentDidMount () {
- const { onFocus } = this.props
- onFocus()
- }
-
- componentWillUnmount () {
- const { onStop } = this.props
- onStop()
- }
-
- render () {
- const {
- src,
- recordedVideo,
- recording,
- modes,
- supportsRecording,
- videoSources,
- showVideoSourceDropdown,
- showRecordingLength,
- onSubmit,
- i18n,
- mirror,
- onSnapshot,
- onStartRecording,
- onStopRecording,
- onDiscardRecordedVideo,
- recordingLengthSeconds,
- } = this.props
-
- const hasRecordedVideo = !!recordedVideo
- const shouldShowRecordButton = !hasRecordedVideo && supportsRecording && (
- isModeAvailable(modes, 'video-only')
- || isModeAvailable(modes, 'audio-only')
- || isModeAvailable(modes, 'video-audio')
- )
- const shouldShowSnapshotButton = !hasRecordedVideo && isModeAvailable(modes, 'picture')
- const shouldShowRecordingLength = supportsRecording && showRecordingLength && !hasRecordedVideo
- const shouldShowVideoSourceDropdown = showVideoSourceDropdown && videoSources && videoSources.length > 1
-
- const videoProps = {
- playsinline: true,
- }
-
- if (recordedVideo) {
- videoProps.muted = false
- videoProps.controls = true
- videoProps.src = recordedVideo
-
- // reset srcObject in dom. If not resetted, stream sticks in element
- if (this.videoElement) {
- this.videoElement.srcObject = undefined
- }
- } else {
- videoProps.muted = true
- videoProps.autoplay = true
- videoProps.srcObject = src
- }
-
- return (
-
-
-
-
-
- {shouldShowVideoSourceDropdown
- ? VideoSourceSelect(this.props)
- : null}
-
-
- {shouldShowSnapshotButton && }
-
- {shouldShowRecordButton && (
-
- )}
-
- {hasRecordedVideo && }
-
- {hasRecordedVideo && }
-
-
-
- {shouldShowRecordingLength && (
-
- )}
-
-
-
- )
- }
-}
-
-export default CameraScreen
diff --git a/packages/@uppy/webcam/src/CameraScreen.tsx b/packages/@uppy/webcam/src/CameraScreen.tsx
new file mode 100644
index 0000000000..c7b7cf275e
--- /dev/null
+++ b/packages/@uppy/webcam/src/CameraScreen.tsx
@@ -0,0 +1,164 @@
+/* eslint-disable jsx-a11y/media-has-caption */
+import type { I18n } from '@uppy/utils/lib/Translator'
+import { h, Component, type ComponentChild } from 'preact'
+import type { HTMLAttributes } from 'preact/compat'
+import SnapshotButton from './SnapshotButton.tsx'
+import RecordButton from './RecordButton.tsx'
+import RecordingLength from './RecordingLength.tsx'
+import VideoSourceSelect, {
+ type VideoSourceSelectProps,
+} from './VideoSourceSelect.tsx'
+import SubmitButton from './SubmitButton.tsx'
+import DiscardButton from './DiscardButton.tsx'
+
+function isModeAvailable(modes: T[], mode: any): mode is T {
+ return modes.includes(mode)
+}
+
+interface CameraScreenProps extends VideoSourceSelectProps {
+ onFocus: () => void
+ onStop: () => void
+
+ src: MediaStream | null
+ recording: boolean
+ modes: string[]
+ supportsRecording: boolean
+ showVideoSourceDropdown: boolean
+ showRecordingLength: boolean
+ onSubmit: () => void
+ i18n: I18n
+ mirror: boolean
+ onSnapshot: () => void
+ onStartRecording: () => void
+ onStopRecording: () => void
+ onDiscardRecordedVideo: () => void
+ recordingLengthSeconds: number
+}
+
+class CameraScreen extends Component {
+ private videoElement: HTMLVideoElement
+
+ refs: any
+
+ componentDidMount(): void {
+ const { onFocus } = this.props
+ onFocus()
+ }
+
+ componentWillUnmount(): void {
+ const { onStop } = this.props
+ onStop()
+ }
+
+ render(): ComponentChild {
+ const {
+ src,
+ // @ts-expect-error TODO: remove unused
+ recordedVideo,
+ recording,
+ modes,
+ supportsRecording,
+ videoSources,
+ showVideoSourceDropdown,
+ showRecordingLength,
+ onSubmit,
+ i18n,
+ mirror,
+ onSnapshot,
+ onStartRecording,
+ onStopRecording,
+ onDiscardRecordedVideo,
+ recordingLengthSeconds,
+ } = this.props
+
+ const hasRecordedVideo = !!recordedVideo
+ const shouldShowRecordButton =
+ !hasRecordedVideo &&
+ supportsRecording &&
+ (isModeAvailable(modes, 'video-only') ||
+ isModeAvailable(modes, 'audio-only') ||
+ isModeAvailable(modes, 'video-audio'))
+ const shouldShowSnapshotButton =
+ !hasRecordedVideo && isModeAvailable(modes, 'picture')
+ const shouldShowRecordingLength =
+ supportsRecording && showRecordingLength && !hasRecordedVideo
+ const shouldShowVideoSourceDropdown =
+ showVideoSourceDropdown && videoSources && videoSources.length > 1
+
+ const videoProps: HTMLAttributes = {
+ playsInline: true,
+ }
+
+ if (recordedVideo) {
+ videoProps.muted = false
+ videoProps.controls = true
+ videoProps.src = recordedVideo
+
+ // reset srcObject in dom. If not resetted, stream sticks in element
+ if (this.videoElement) {
+ this.videoElement.srcObject = null
+ }
+ } else {
+ videoProps.muted = true
+ videoProps.autoPlay = true
+ // @ts-expect-error srcObject does not exist on