implement tab navigation for speedgrader navigation
fixes EGG-82 flag=react_discussion flag=speed_grader Test Plan: 0) Can fully navigate all of a student's entries, with TAB navigation buttons. 1) Open a speedgrader discussion. 2) Go to highlighted entry, 3) Press tab PAST the reply and Mark as Read buttons and the speedgrader buttons will appear! Change-Id: I93b4f0d6dca29f82b6c2f67b75e2c41060297eb4 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/357932 Reviewed-by: Dave Wenzlick <david.wenzlick@instructure.com> Reviewed-by: Omar Soto-Fortuño <omar.soto@instructure.com> QA-Review: Jason Gillett <jason.gillett@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Product-Review: Sam Garza <sam.garza@instructure.com>
This commit is contained in:
parent
3c55379a03
commit
596188ba60
|
@ -152,5 +152,66 @@ describe "Screenreader Gradebook grading" do
|
|||
wait_for_ajaximations
|
||||
end
|
||||
end
|
||||
|
||||
it "next tab keeps focus" do
|
||||
get "/courses/#{@course.id}/gradebook/speed_grader?assignment_id=#{@checkpointed_assignment.id}&student_id=#{@student2.id}&entry_id=#{@entry.id}"
|
||||
|
||||
in_frame("speedgrader_iframe") do
|
||||
in_frame("discussion_preview_iframe") do
|
||||
# tab to prev button
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
# tab to next button
|
||||
driver.action.send_keys(:tab).perform
|
||||
|
||||
3.times do |i|
|
||||
expect(f("div[data-testid='isHighlighted']").text).to include(@student2.name)
|
||||
expect(f("div[data-testid='isHighlighted']").text).to include("reply to topic j#{2 - i}")
|
||||
# page 1
|
||||
expect(f("body").text).to include("reply to topic j2")
|
||||
expect(f("body").text).to_not include("reply to topic i0")
|
||||
# notice enter key successfully navigates entries
|
||||
driver.action.send_keys(:enter).perform
|
||||
end
|
||||
# page 2
|
||||
expect(f("body").text).to include("reply to topic i0")
|
||||
expect(f("body").text).to_not include("reply to topic j2")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "prev tab keeps focus" do
|
||||
get "/courses/#{@course.id}/gradebook/speed_grader?assignment_id=#{@checkpointed_assignment.id}&student_id=#{@student2.id}&entry_id=#{@entry.id}"
|
||||
|
||||
in_frame("speedgrader_iframe") do
|
||||
in_frame("discussion_preview_iframe") do
|
||||
# tab to prev button
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
driver.action.send_keys(:tab).perform
|
||||
|
||||
expect(f("div[data-testid='isHighlighted']").text).to include(@student2.name)
|
||||
expect(f("div[data-testid='isHighlighted']").text).to include("reply to topic j2")
|
||||
# page 1 is selected
|
||||
expect(f("body").text).to include("reply to topic j2")
|
||||
expect(f("body").text).to_not include("reply to topic i0")
|
||||
|
||||
3.times do |i|
|
||||
# notice enter key successfully navigates entries
|
||||
driver.action.send_keys(:enter).perform
|
||||
expect(f("div[data-testid='isHighlighted']").text).to include(@student2.name)
|
||||
expect(f("div[data-testid='isHighlighted']").text).to include("reply to entry i#{i}")
|
||||
# page 2 is selected
|
||||
expect(f("body").text).to include("reply to topic i0")
|
||||
expect(f("body").text).to_not include("reply to topic j2")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -104,7 +104,8 @@ export const STUDENT_DISCUSSION_QUERY = gql`
|
|||
}
|
||||
discussionEntriesConnection(sortOrder: $sort, userSearchId: $userSearchId) {
|
||||
nodes {
|
||||
...DiscussionEntry
|
||||
_id
|
||||
rootEntryId
|
||||
anonymousAuthor {
|
||||
...AnonymousUser
|
||||
}
|
||||
|
@ -119,7 +120,6 @@ export const STUDENT_DISCUSSION_QUERY = gql`
|
|||
}
|
||||
${AnonymousUser.fragment}
|
||||
${Discussion.fragment}
|
||||
${DiscussionEntry.fragment}
|
||||
${PageInfo.fragment}
|
||||
`
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {DISCUSSION_QUERY, STUDENT_DISCUSSION_QUERY} from '../graphql/Queries'
|
||||
import {DISCUSSION_QUERY} from '../graphql/Queries'
|
||||
import {DiscussionTopicToolbarContainer} from './containers/DiscussionTopicToolbarContainer/DiscussionTopicToolbarContainer'
|
||||
import {DiscussionTopicRepliesContainer} from './containers/DiscussionTopicRepliesContainer/DiscussionTopicRepliesContainer'
|
||||
import {DiscussionTopicHeaderContainer} from './containers/DiscussionTopicHeaderContainer/DiscussionTopicHeaderContainer'
|
||||
|
@ -66,6 +66,7 @@ const DiscussionTopicManager = props => {
|
|||
const [showTranslationControl, setShowTranslationControl] = useState(false)
|
||||
// Start as null, populate when ready.
|
||||
const [translateTargetLanguage, setTranslateTargetLanguage] = useState(null)
|
||||
const [focusSelector, setFocusSelector] = useState('')
|
||||
|
||||
const searchContext = {
|
||||
searchTerm,
|
||||
|
@ -82,6 +83,8 @@ const DiscussionTopicManager = props => {
|
|||
setAllThreadsStatus,
|
||||
expandedThreads,
|
||||
setExpandedThreads,
|
||||
discussionID: props.discussionTopicId,
|
||||
perPage: ENV.per_page,
|
||||
}
|
||||
const [userSplitScreenPreference, setUserSplitScreenPreference] = useState(
|
||||
ENV.DISCUSSION?.preferences?.discussions_splitscreen_view || false
|
||||
|
@ -133,6 +136,11 @@ const DiscussionTopicManager = props => {
|
|||
setUserSplitScreenPreference,
|
||||
highlightEntryId,
|
||||
setHighlightEntryId,
|
||||
setPageNumber,
|
||||
expandedThreads,
|
||||
setExpandedThreads,
|
||||
focusSelector,
|
||||
setFocusSelector,
|
||||
setIsGradedDiscussion,
|
||||
isGradedDiscussion,
|
||||
usedThreadingToolbarChildRef,
|
||||
|
@ -231,72 +239,25 @@ const DiscussionTopicManager = props => {
|
|||
skip: waitForUnreadFilter,
|
||||
})
|
||||
|
||||
const speedGraderHook = useSpeedGrader()
|
||||
|
||||
const studenTopicVariables = {
|
||||
discussionID: discussionTopicQuery?.data?.legacyNode?._id,
|
||||
userSearchId: speedGraderHook.currentStudentId,
|
||||
perPage: ENV.per_page,
|
||||
sort,
|
||||
}
|
||||
|
||||
const studentTopicQuery = useQuery(STUDENT_DISCUSSION_QUERY, {
|
||||
variables: studenTopicVariables,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
skip: !(
|
||||
speedGraderHook.isInSpeedGrader &&
|
||||
speedGraderHook.currentStudentId &&
|
||||
studenTopicVariables.discussionID
|
||||
),
|
||||
})
|
||||
|
||||
const onMessage = useCallback(
|
||||
e => {
|
||||
const message = e.data
|
||||
if (highlightEntryId) {
|
||||
switch (message.subject) {
|
||||
case 'DT.previousStudentReply': {
|
||||
const previousStudentEntry = speedGraderHook.getStudentPreviousEntry(
|
||||
highlightEntryId,
|
||||
studentTopicQuery
|
||||
)
|
||||
setHighlightEntryId(previousStudentEntry?._id)
|
||||
setPageNumber(previousStudentEntry?.rootEntryPageNumber)
|
||||
if (previousStudentEntry?.rootEntryId){
|
||||
setExpandedThreads([...expandedThreads, previousStudentEntry.rootEntryId])
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'DT.nextStudentReply': {
|
||||
const nextStudentEntry = speedGraderHook.getStudentNextEntry(
|
||||
highlightEntryId,
|
||||
studentTopicQuery
|
||||
)
|
||||
setHighlightEntryId(nextStudentEntry?._id)
|
||||
setPageNumber(nextStudentEntry?.rootEntryPageNumber)
|
||||
if (nextStudentEntry?.rootEntryId){
|
||||
setExpandedThreads([...expandedThreads, nextStudentEntry.rootEntryId])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[studentTopicQuery.loading, highlightEntryId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (highlightEntryId && !isPersistEnabled) {
|
||||
setTimeout(() => {
|
||||
setHighlightEntryId(null)
|
||||
}, HIGHLIGHT_TIMEOUT)
|
||||
}
|
||||
}, [highlightEntryId, discussionTopicQuery.loading])
|
||||
|
||||
window.addEventListener('message', onMessage)
|
||||
return () => {
|
||||
window.removeEventListener('message', onMessage)
|
||||
}
|
||||
}, [highlightEntryId, discussionTopicQuery.loading, onMessage])
|
||||
useSpeedGrader({
|
||||
highlightEntryId,
|
||||
setHighlightEntryId,
|
||||
setPageNumber,
|
||||
expandedThreads,
|
||||
setExpandedThreads,
|
||||
setFocusSelector,
|
||||
discussionID: props.discussionTopicId,
|
||||
perPage: ENV.per_page,
|
||||
sort,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setIsGradedDiscussion(!!discussionTopicQuery?.data?.legacyNode?.assignment)
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
|
||||
import classNames from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {useLayoutEffect, useRef} from 'react'
|
||||
import React, {useLayoutEffect, useRef, useContext} from 'react'
|
||||
import {DiscussionManagerUtilityContext} from '../../utils/constants'
|
||||
import theme from '@instructure/canvas-theme'
|
||||
|
||||
export function Highlight({...props}) {
|
||||
|
@ -26,12 +27,37 @@ export function Highlight({...props}) {
|
|||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const isPersistEnabled = urlParams.get('persist') === '1'
|
||||
const className = isPersistEnabled ? 'highlight-discussion' : 'highlight-fadeout'
|
||||
const {focusSelector, setFocusSelector} = useContext(DiscussionManagerUtilityContext)
|
||||
|
||||
const triggerFocus = element => {
|
||||
let eventType = "onfocusin" in element ? "focusin" : "focus";
|
||||
let bubbles = "onfocusin" in element;
|
||||
let event;
|
||||
|
||||
if ("createEvent" in document) {
|
||||
event = document.createEvent("Event");
|
||||
event.initEvent(eventType, bubbles, true);
|
||||
}
|
||||
else if ("Event" in window) {
|
||||
event = new Event(eventType, { bubbles: bubbles, cancelable: true });
|
||||
}
|
||||
|
||||
element.focus();
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (props.isHighlighted && highlightRef.current) {
|
||||
setTimeout(() => {
|
||||
highlightRef.current?.scrollIntoView({behavior: 'smooth', block: 'center'})
|
||||
highlightRef.current?.querySelector('button').focus({preventScroll: true})
|
||||
if (focusSelector) {
|
||||
const speedGraderDiv = highlightRef.current?.querySelector('#speedgrader-navigator')
|
||||
triggerFocus(speedGraderDiv)
|
||||
highlightRef.current?.querySelector(focusSelector).focus({preventScroll: true})
|
||||
setFocusSelector('')
|
||||
} else {
|
||||
highlightRef.current?.querySelector('button').focus({preventScroll: true})
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}, [props.isHighlighted, highlightRef])
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
*/
|
||||
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import React, {useState, useRef} from 'react'
|
||||
import React, {useState, useRef, useContext} from 'react'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {Button} from '@instructure/ui-buttons'
|
||||
import useSpeedGrader from '../../hooks/useSpeedGrader'
|
||||
import {DiscussionManagerUtilityContext, SearchContext} from '../../utils/constants'
|
||||
|
||||
const I18n = useI18nScope('speed_grader')
|
||||
|
||||
|
@ -40,16 +41,34 @@ export const SpeedGraderNavigator = () => {
|
|||
const containerRef = useRef(null)
|
||||
|
||||
const {
|
||||
handlePreviousStudentReply,
|
||||
handleNextStudentReply,
|
||||
handleJumpFocusToSpeedGrader
|
||||
} = useSpeedGrader()
|
||||
highlightEntryId,
|
||||
setHighlightEntryId,
|
||||
setPageNumber,
|
||||
expandedThreads,
|
||||
setExpandedThreads,
|
||||
setFocusSelector,
|
||||
} = useContext(DiscussionManagerUtilityContext)
|
||||
|
||||
const {sort, perPage, discussionID} = useContext(SearchContext)
|
||||
|
||||
const {handlePreviousStudentReply, handleNextStudentReply, handleJumpFocusToSpeedGrader} =
|
||||
useSpeedGrader({
|
||||
highlightEntryId,
|
||||
setHighlightEntryId,
|
||||
setPageNumber,
|
||||
expandedThreads,
|
||||
setExpandedThreads,
|
||||
setFocusSelector,
|
||||
discussionID,
|
||||
perPage,
|
||||
sort,
|
||||
})
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsVisible(true)
|
||||
}
|
||||
|
||||
const handleBlur = (event) => {
|
||||
const handleBlur = event => {
|
||||
if (!containerRef.current?.contains(event.relatedTarget)) {
|
||||
setIsVisible(false)
|
||||
}
|
||||
|
@ -59,7 +78,7 @@ export const SpeedGraderNavigator = () => {
|
|||
if (!handler) return null
|
||||
return (
|
||||
<Flex.Item padding="0 x-small">
|
||||
<Button data-testid={testId} onClick={handler}>
|
||||
<Button data-testid={testId} id={testId} onClick={handler}>
|
||||
{text}
|
||||
</Button>
|
||||
</Flex.Item>
|
||||
|
@ -69,6 +88,7 @@ export const SpeedGraderNavigator = () => {
|
|||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
id={'speedgrader-navigator'}
|
||||
style={isVisible ? {} : visuallyHiddenStyles}
|
||||
aria-hidden={!isVisible}
|
||||
onFocus={handleFocus}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {responsiveQuerySizes} from '../../../utils'
|
|||
import {ThreadingToolbar} from '../ThreadingToolbar'
|
||||
|
||||
jest.mock('../../../utils')
|
||||
jest.mock('../../../hooks/useSpeedGrader', () => jest.fn(() => false))
|
||||
|
||||
beforeAll(() => {
|
||||
window.matchMedia = jest.fn().mockImplementation(() => {
|
||||
|
@ -42,7 +43,7 @@ beforeEach(() => {
|
|||
}))
|
||||
})
|
||||
|
||||
describe('PostToolbar', () => {
|
||||
describe('ThreadingToolbar', () => {
|
||||
it('renders "Go to Reply" button when filter is set to unread', () => {
|
||||
const {getByText} = render(
|
||||
<ThreadingToolbar searchTerm="" filter="unread">
|
||||
|
|
|
@ -16,9 +16,21 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useEffect, useState, useCallback} from 'react'
|
||||
import {STUDENT_DISCUSSION_QUERY} from '../../graphql/Queries'
|
||||
import {useQuery} from 'react-apollo'
|
||||
|
||||
export default function useSpeedGrader() {
|
||||
export default function useSpeedGrader({
|
||||
highlightEntryId = '',
|
||||
setHighlightEntryId = () => {},
|
||||
setPageNumber = () => {},
|
||||
expandedThreads,
|
||||
setExpandedThreads = () => {},
|
||||
setFocusSelector = () => {},
|
||||
sort = 'desc',
|
||||
discussionID = '',
|
||||
perPage = 20,
|
||||
} = {}) {
|
||||
const [isInSpeedGrader, setIsInSpeedGrader] = useState(false)
|
||||
const [currentStudentId, setCurrentStudentId] = useState(null)
|
||||
|
||||
|
@ -37,7 +49,21 @@ export default function useSpeedGrader() {
|
|||
checkSpeedGrader()
|
||||
}, [])
|
||||
|
||||
const getStudentEntries = studentTopicQuery => {
|
||||
// perPage and sort should match discussionTopicQuery
|
||||
const studentTopicVariables = {
|
||||
discussionID,
|
||||
userSearchId: currentStudentId,
|
||||
perPage,
|
||||
sort,
|
||||
}
|
||||
|
||||
const studentTopicQuery = useQuery(STUDENT_DISCUSSION_QUERY, {
|
||||
variables: studentTopicVariables,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
skip: !(isInSpeedGrader && currentStudentId && studentTopicVariables.discussionID),
|
||||
})
|
||||
|
||||
const getStudentEntries = useCallback(() => {
|
||||
if (!currentStudentId) {
|
||||
return []
|
||||
}
|
||||
|
@ -50,31 +76,68 @@ export default function useSpeedGrader() {
|
|||
studentTopicQuery?.data?.legacyNode?.discussionEntriesConnection?.nodes || []
|
||||
|
||||
return studentEntries
|
||||
}
|
||||
}, [studentTopicQuery, currentStudentId])
|
||||
|
||||
function getStudentPreviousEntry(currentEntryId, studentTopicQuery) {
|
||||
const navigateToEntry = useCallback(
|
||||
newEntry => {
|
||||
setHighlightEntryId(newEntry?._id || highlightEntryId)
|
||||
setPageNumber(newEntry?.rootEntryPageNumber)
|
||||
if (newEntry?.rootEntryId) {
|
||||
setExpandedThreads([...expandedThreads, newEntry.rootEntryId])
|
||||
}
|
||||
},
|
||||
[expandedThreads, highlightEntryId, setExpandedThreads, setHighlightEntryId, setPageNumber]
|
||||
)
|
||||
|
||||
const getStudentPreviousEntry = useCallback(() => {
|
||||
const studentEntries = getStudentEntries(studentTopicQuery)
|
||||
const studentEntriesIds = studentEntries.map(entry => entry._id)
|
||||
const currentEntryIndex = studentEntriesIds.indexOf(currentEntryId)
|
||||
const currentEntryIndex = studentEntriesIds.indexOf(highlightEntryId)
|
||||
let prevEntryIndex = currentEntryIndex - 1
|
||||
if (currentEntryIndex === 0) {
|
||||
prevEntryIndex = studentEntriesIds.length - 1
|
||||
}
|
||||
const previousEntry = studentEntries[prevEntryIndex]
|
||||
return previousEntry || studentEntries[currentEntryId]
|
||||
}
|
||||
navigateToEntry(previousEntry)
|
||||
}, [getStudentEntries, studentTopicQuery, highlightEntryId, navigateToEntry])
|
||||
|
||||
function getStudentNextEntry(currentEntryId, studentTopicQuery) {
|
||||
const getStudentNextEntry = useCallback(() => {
|
||||
const studentEntries = getStudentEntries(studentTopicQuery)
|
||||
const studentEntriesIds = studentEntries.map(entry => entry._id)
|
||||
const currentEntryIndex = studentEntriesIds.indexOf(currentEntryId)
|
||||
const currentEntryIndex = studentEntriesIds.indexOf(highlightEntryId)
|
||||
let nextEntryIndex = currentEntryIndex + 1
|
||||
if (currentEntryIndex === studentEntriesIds.length - 1) {
|
||||
nextEntryIndex = 0
|
||||
}
|
||||
const nextEntry = studentEntries[nextEntryIndex]
|
||||
return nextEntry || studentEntries[currentEntryId]
|
||||
}
|
||||
navigateToEntry(nextEntry)
|
||||
}, [getStudentEntries, studentTopicQuery, highlightEntryId, navigateToEntry])
|
||||
|
||||
const onMessage = useCallback(
|
||||
e => {
|
||||
const message = e.data
|
||||
if (highlightEntryId) {
|
||||
switch (message.subject) {
|
||||
case 'DT.previousStudentReply': {
|
||||
getStudentPreviousEntry()
|
||||
break
|
||||
}
|
||||
case 'DT.nextStudentReply': {
|
||||
getStudentNextEntry()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[highlightEntryId, getStudentPreviousEntry, getStudentNextEntry]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', onMessage)
|
||||
return () => {
|
||||
window.removeEventListener('message', onMessage)
|
||||
}
|
||||
}, [highlightEntryId, onMessage])
|
||||
|
||||
const handleJumpFocusToSpeedGrader = () => {
|
||||
window.top.postMessage(
|
||||
|
@ -85,14 +148,23 @@ export default function useSpeedGrader() {
|
|||
)
|
||||
}
|
||||
|
||||
// These will be implemented later
|
||||
const handlePreviousStudentReply = null
|
||||
const handleNextStudentReply = null
|
||||
function sendPostMessage(message) {
|
||||
window.postMessage(message, '*')
|
||||
}
|
||||
|
||||
function handlePreviousStudentReply() {
|
||||
const message = {subject: 'DT.previousStudentReply'}
|
||||
sendPostMessage(message)
|
||||
setFocusSelector('#previous-in-speedgrader')
|
||||
}
|
||||
|
||||
const handleNextStudentReply = () => {
|
||||
const message = {subject: 'DT.nextStudentReply'}
|
||||
sendPostMessage(message)
|
||||
setFocusSelector('#next-in-speedgrader')
|
||||
}
|
||||
|
||||
return {
|
||||
currentStudentId,
|
||||
getStudentPreviousEntry,
|
||||
getStudentNextEntry,
|
||||
isInSpeedGrader,
|
||||
handlePreviousStudentReply,
|
||||
handleNextStudentReply,
|
||||
|
|
|
@ -48,6 +48,9 @@ const searchFilter = {
|
|||
setAllThreadsStatus: () => {},
|
||||
expandedThreads: [],
|
||||
setExpandedThreads: () => {},
|
||||
sort: 'desc',
|
||||
perPage: '',
|
||||
discussionID: '',
|
||||
}
|
||||
export const SearchContext = React.createContext(searchFilter)
|
||||
|
||||
|
@ -58,6 +61,11 @@ const discussionManagerUtilityContext = {
|
|||
setUserSplitScreenPreference: () => {},
|
||||
highlightEntryId: '',
|
||||
setHighlightEntryId: () => {},
|
||||
expandedThreads: '',
|
||||
setExpandedThreads: () => {},
|
||||
focusSelector: '',
|
||||
setFocusSelector: () => {},
|
||||
setPageNumber: () => {},
|
||||
isGradedDiscussion: false,
|
||||
setIsGradedDiscussion: () => {},
|
||||
usedThreadingToolbarChildRef: null,
|
||||
|
|
Loading…
Reference in New Issue