Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4e96dcc
Add setting to enable non-dragging movements
sjd210 May 12, 2026
d523a32
Add hook to enable non-dragging drop zones
sjd210 May 12, 2026
e3455d2
Add toggle to switch drag-and-drop mode per page
sjd210 May 12, 2026
a652e2a
Clean up toggling so it respects dropzone portals
sjd210 May 12, 2026
3b77a84
Rename setting to NON_DRAGGING_INPUTS
sjd210 May 13, 2026
a90da56
Move dnd input toggle to question metadata
sjd210 May 13, 2026
547e8e6
Add basic accessibility type redux structures
sjd210 May 13, 2026
602c7f7
Update test and add test with hydrate
sjd210 May 14, 2026
3a16039
Revert "Update test and add test with hydrate"
sjd210 May 14, 2026
03de124
Add rest of accessibility_type reducer settings
sjd210 May 15, 2026
24f8065
Clean up components and imports between files
sjd210 May 15, 2026
59a3be7
Add white background to non-empty cloze-dropdowns
sjd210 May 15, 2026
e0e0026
Add checkbox instead of toggle for Ada
sjd210 May 15, 2026
ad2a286
Also apply white background to Ada dropzones
sjd210 May 18, 2026
40ee28a
Add default to reducer case to fix tests
sjd210 May 18, 2026
8fe0158
Move checkbox on Ada question pages
sjd210 May 18, 2026
f32e355
Remove second input toggle from Isaac
sjd210 May 18, 2026
c189897
Update VRT baselines
actions-user May 18, 2026
d89b2ba
Merge pull request #2160 from isaacphysics/vrt/feature/non-dragging-i…
sjd210 May 18, 2026
7bfa1c4
Update VRT baselines
actions-user May 19, 2026
959615a
Merge pull request #2162 from isaacphysics/vrt/feature/non-dragging-i…
sjd210 May 19, 2026
95f5618
Re-allow questions switching input mode for width
sjd210 May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/IsaacAppTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export type Action =
| {type: ACTION_TYPE.USER_PREFERENCES_REQUEST}
| {type: ACTION_TYPE.USER_PREFERENCES_RESPONSE_SUCCESS; userPreferences: UserPreferencesDTO}
| {type: ACTION_TYPE.USER_PREFERENCES_RESPONSE_FAILURE; errorMessage: string}

| {type: ACTION_TYPE.ACCESSIBILITY_TYPE_SET; accessibilityType: AccessibilitySettings}
| {type: ACTION_TYPE.USER_LOG_IN_REQUEST; provider: ApiTypes.AuthenticationProvider}
| {type: ACTION_TYPE.USER_LOG_IN_RESPONSE_SUCCESS; authResponse: ApiTypes.AuthenticationResponseDTO}
| {type: ACTION_TYPE.USER_LOG_IN_RESPONSE_FAILURE; errorMessage: string}
Expand Down Expand Up @@ -232,6 +232,7 @@ export interface AccessibilitySettings {
PREFER_MATHML?: boolean;
REDUCED_MOTION?: boolean;
SHOW_INACCESSIBLE_WARNING?: boolean;
NON_DRAGGING_INPUTS?: boolean;
}

export interface UserConsent {
Expand Down Expand Up @@ -462,6 +463,7 @@ export const DragAndDropRegionContext = React.createContext<(
nonSelectedItems: Immutable<ReplaceableItem>[],
allItems: Immutable<ReplaceableItem>[],
zoneIds: Set<string>,
dragAndDropEnabled: boolean;
} | undefined>(undefined);

export const InlineContext = React.createContext<{
Expand Down
11 changes: 8 additions & 3 deletions src/app/components/content/IsaacClozeQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
isDefined,
isTouchDevice,
useCurrentQuestionAttempt,
useDeviceSize
useDeviceSize,
} from "../../services";
import {customKeyboardCoordinates} from "../../services/clozeQuestionKeyboardCoordinateGetter";
import {IsaacContentValueOrChildren} from "./IsaacContentValueOrChildren";
Expand All @@ -42,6 +42,7 @@ import {DragAndDropRegionContext, IsaacQuestionProps, ReplaceableItem} from "../
import {v4 as uuid_v4} from "uuid";
import {Immutable} from "immer";
import {arraySwap, SortableContext} from "@dnd-kit/sortable";
import { selectors, useAppSelector } from "../../state";

const DropZoneItem = lazy(() => import("../elements/DnDItem"));

Expand Down Expand Up @@ -134,10 +135,13 @@ const useAutoScroll = ({active, acceleration, interval}: {active: boolean; accel
};

const IsaacClozeQuestion = ({doc, questionId, readonly, validationResponse}: IsaacQuestionProps<IsaacClozeQuestionDTO, ItemValidationResponseDTO>) => {
const deviceSize = useDeviceSize();
const { currentAttempt: rawCurrentAttempt, dispatchSetCurrentAttempt } = useCurrentQuestionAttempt<ItemChoiceDTO>(questionId);
const currentAttempt = useMemo(() => rawCurrentAttempt ? {...rawCurrentAttempt, items: replaceNullItems(rawCurrentAttempt.items)} : undefined, [rawCurrentAttempt]);

const deviceSize = useDeviceSize();
const accessibilityType = useAppSelector(selectors.accessibility.type);
const dragAndDropEnabled = isDefined(accessibilityType) ? !accessibilityType?.NON_DRAGGING_INPUTS : !(deviceSize === "xs" || (isTouchDevice() && below['md'](deviceSize)));

const cssFriendlyQuestionPartId = questionId?.replace(/\|/g, '-') ?? ""; // Maybe we should clean up IDs more?
const withReplacement = doc.withReplacement ?? false;

Expand Down Expand Up @@ -488,6 +492,7 @@ const IsaacClozeQuestion = ({doc, questionId, readonly, validationResponse}: Isa
nonSelectedItems,
allItems,
zoneIds: new Set<string>(),
dragAndDropEnabled
}}>
<DndContext
sensors={sensors}
Expand All @@ -502,7 +507,7 @@ const IsaacClozeQuestion = ({doc, questionId, readonly, validationResponse}: Isa
{doc.children}
</IsaacContentValueOrChildren>

{(!(deviceSize === "xs" || (isTouchDevice() && below['md'](deviceSize)))) && <>
{dragAndDropEnabled && <>
{/* The item attached to the users cursor while dragging (just for display, shouldn't contain useDraggable/useSortable hooks) */}
<DragOverlay>
{activeItem && <Badge className="p-1 cloze-item cloze-bg is-dragging" color="theme">
Expand Down
19 changes: 17 additions & 2 deletions src/app/components/content/IsaacDragAndDropQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {DragAndDropRegionContext, IsaacQuestionProps, ReplaceableItem} from "../
import {v4 as uuid_v4} from "uuid";
import {Immutable} from "immer";
import {arraySwap, SortableContext} from "@dnd-kit/sortable";
import { useAccessibilitySettings } from "../../services/accessibility";
import { selectors, useAppSelector } from "../../state";

const DropZoneItem = lazy(() => import("../elements/DnDItem"));

Expand Down Expand Up @@ -134,8 +136,16 @@ const useAutoScroll = ({active, acceleration, interval}: {active: boolean; accel
}, [active]);
};

const IsaacDragAndDropQuestion = ({doc, questionId, readonly, validationResponse}: IsaacQuestionProps<IsaacDragAndDropQuestionDTO, DndValidationResponseDTO>) => {
export const useDefaultDragAndDropInputMode = () => {
const accessibilitySettings = useAccessibilitySettings();
const deviceSize = useDeviceSize();
const accessibilityType = useAppSelector(selectors.accessibility.type);
console.log("ac", accessibilityType);

return !(deviceSize === "xs" || (isTouchDevice() && below['md'](deviceSize)) || accessibilitySettings.NON_DRAGGING_INPUTS || false);
};

const IsaacDragAndDropQuestion = ({doc, questionId, readonly, validationResponse}: IsaacQuestionProps<IsaacDragAndDropQuestionDTO, DndValidationResponseDTO>) => {
const { currentAttempt: rawCurrentAttempt, dispatchSetCurrentAttempt } = useCurrentQuestionAttempt<DndChoiceDTO>(questionId);
const currentAttempt = useMemo(() => rawCurrentAttempt ? {...rawCurrentAttempt, items: replaceNullItems(rawCurrentAttempt.items)} : undefined, [rawCurrentAttempt]);

Expand All @@ -155,6 +165,10 @@ const IsaacDragAndDropQuestion = ({doc, questionId, readonly, validationResponse
.map(({replacementId: _, ...item}) => item);
};

const deviceSize = useDeviceSize();
const accessibilityType = useAppSelector(selectors.accessibility.type);
const dragAndDropEnabled = isDefined(accessibilityType) ? !accessibilityType?.NON_DRAGGING_INPUTS : !(deviceSize === "xs" || (isTouchDevice() && below['md'](deviceSize)));

const cssFriendlyQuestionPartId = questionId?.replace(/\|/g, '-') ?? ""; // Maybe we should clean up IDs more?
const withReplacement = doc.withReplacement ?? false;

Expand Down Expand Up @@ -511,6 +525,7 @@ const IsaacDragAndDropQuestion = ({doc, questionId, readonly, validationResponse
nonSelectedItems,
allItems,
zoneIds: new Set<string>(),
dragAndDropEnabled
}}>
<DndContext
sensors={sensors}
Expand All @@ -525,7 +540,7 @@ const IsaacDragAndDropQuestion = ({doc, questionId, readonly, validationResponse
{doc.children}
</IsaacContentValueOrChildren>

{(!(deviceSize === "xs" || (isTouchDevice() && below['md'](deviceSize)))) && <>
{dragAndDropEnabled && <>
{/* The item attached to the users cursor while dragging (just for display, shouldn't contain useDraggable/useSortable hooks) */}
<DragOverlay>
{activeItem && <Badge className="p-1 cloze-item cloze-bg is-dragging" color="theme">
Expand Down
46 changes: 43 additions & 3 deletions src/app/components/elements/PageMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ import { TeacherNotes } from './TeacherNotes';
import { useLocation } from 'react-router';
import { SidebarButton } from './SidebarButton';
import { HelpButton } from './HelpButton';
import { above, below, isAda, isPhy, useDeviceSize } from '../../services';
import { above, ACTION_TYPE, below, isAda, isPhy, siteSpecific, useDeviceSize } from '../../services';
import type { Location } from 'history';
import classNames from 'classnames';
import { UserContextPicker } from './inputs/UserContextPicker';
import { LLMFreeTextQuestionIndicator } from './LLMFreeTextQuestionIndicator';
import { CrossTopicQuestionIndicator } from './CrossTopicQuestionIndicator';
import { selectors, useAppSelector } from '../../state';
import { selectors, useAppDispatch, useAppSelector } from '../../state';
import { BookmarkButton } from './BookmarkButton';
import { FeatureFlag, FeatureFlagWrapper } from '../../services/featureFlag';
import { Spacer } from './Spacer';
import StyledToggle from "../elements/inputs/StyledToggle";
import { StyledCheckbox } from './inputs/StyledCheckbox';

type PageMetadataProps = {
doc?: SeguePageDTO;
Expand All @@ -40,6 +43,23 @@ type PageMetadataProps = {
}
);

export const DragAndDropInputModeToggle = ({dragAndDropEnabled, toggleDragAndDropEnabled}: {dragAndDropEnabled: boolean, toggleDragAndDropEnabled: () => void}) => {
return siteSpecific(<div className="d-flex flex-column align-items-center w-min-content mb-1">
<span>Question input mode</span>
<Spacer />
<StyledToggle
checked={dragAndDropEnabled}
falseLabel="Dropdown"
trueLabel="Drag and drop"
onChange={toggleDragAndDropEnabled}
/>
</div>,
<div className="mt-1 ms-1">
<StyledCheckbox checked={!dragAndDropEnabled} onChange={toggleDragAndDropEnabled} label={<span className="text-muted">Use dropdowns for drag and drop questions</span>} />
</div>
);
};

interface ActionButtonsProps extends React.HTMLAttributes<HTMLDivElement> {
location: Location;
isQuestion: boolean;
Expand Down Expand Up @@ -73,13 +93,18 @@ interface TagStackProps extends React.HTMLAttributes<HTMLDivElement> {
const TagStack = ({doc, className}: TagStackProps) => {
const isCrossTopic = doc?.tags?.includes("cross_topic");
const pageContainsLLMFreeTextQuestion = useAppSelector(selectors.questions.includesLLMFreeTextQuestion);
const displayDragAndDropToggle = useAppSelector(selectors.questions.includesClozeOrDragAndDropQuestion) && isAda;

return <div className={className}>
{(isCrossTopic || pageContainsLLMFreeTextQuestion) && <div className="d-lg-flex align-items-center gap-3 me-3">
{isAda && isCrossTopic && <CrossTopicQuestionIndicator/>}
{pageContainsLLMFreeTextQuestion && <LLMFreeTextQuestionIndicator/>}
{displayDragAndDropToggle && <DragAndDropInputModeToggle dragAndDropEnabled={true} toggleDragAndDropEnabled={() => {}} />}
</div>}
<EditContentButton doc={doc}/>
<div>
<EditContentButton doc={doc}/>
{displayDragAndDropToggle && !(isCrossTopic || pageContainsLLMFreeTextQuestion) && <DragAndDropInputModeToggle dragAndDropEnabled={true} toggleDragAndDropEnabled={() => {}} />}
</div>
</div>;
};

Expand Down Expand Up @@ -117,6 +142,15 @@ export const PageMetadata = (props: PageMetadataProps) => {
const deviceSize = useDeviceSize();
const actionButtonsFloat = noTitle && children;

const dispatch = useAppDispatch();
const pageContainsClozeOrDragAndDropQuestion = useAppSelector(selectors.questions.includesClozeOrDragAndDropQuestion);

const accessibilityType = useAppSelector(selectors.accessibility.type);
const dragAndDropEnabled = !accessibilityType?.NON_DRAGGING_INPUTS;
const toggleDragAndDropEnabled = () => {
dispatch({type: ACTION_TYPE.ACCESSIBILITY_TYPE_SET, accessibilityType: {"NON_DRAGGING_INPUTS": dragAndDropEnabled}});
};

return <>
{isPhy && showSidebarButton && sidebarInTitle && below['md'](deviceSize) && <SidebarButton buttonTitle={sidebarButtonText} absolute/>}
<div className="page-metadata">
Expand All @@ -140,10 +174,16 @@ export const PageMetadata = (props: PageMetadataProps) => {
<div className="d-flex align-items-end">
{isPhy && <TagStack doc={doc} className="d-flex align-items-end gap-3"/>}
{isConcept && <UserContextPicker className={classNames("flex-grow-1", {"mt-3": isAda})}/>}
{isPhy && pageContainsClozeOrDragAndDropQuestion && <>
<Spacer />
<DragAndDropInputModeToggle dragAndDropEnabled={dragAndDropEnabled} toggleDragAndDropEnabled={toggleDragAndDropEnabled} />
</>
}
</div>

{isPhy && <TeacherNotes notes={doc?.teacherNotes} />}
</div>
{isPhy && showSidebarButton && !sidebarInTitle && below['md'](deviceSize) && <SidebarButton className="my-2" buttonTitle={sidebarButtonText}/>}
</>;
};

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, {useContext, useEffect, useRef, useState} from "react";
import {Dropdown, DropdownItem, DropdownMenu, DropdownToggle} from "reactstrap";
import {useDroppable} from "@dnd-kit/core";
import classNames from "classnames";
import {CLOZE_DROP_ZONE_ID_PREFIX, NULL_CLOZE_ITEM, below, isAda, isDefined, isPhy, isTouchDevice, useDeviceSize} from "../../../../services";
import {CLOZE_DROP_ZONE_ID_PREFIX, NULL_CLOZE_ITEM, isAda, isDefined, isPhy} from "../../../../services";
import { Markup } from "..";
import DropZoneItem from "../../DnDItem";

Expand All @@ -20,7 +20,6 @@ interface InlineDropRegionProps {
// Inline droppables rendered for each registered drop region
function InlineDropRegion({divId, zoneId, emptyWidth, emptyHeight, rootElement, skipPortalling}: InlineDropRegionProps) {
const dropRegionContext = useContext(DragAndDropRegionContext);
const deviceSize = useDeviceSize();
const [isOpen, setIsOpen] = useState<boolean>(false);
const droppableId = CLOZE_DROP_ZONE_ID_PREFIX + zoneId;
const dropdownItems = dropRegionContext?.allItems ?? [];
Expand Down Expand Up @@ -114,8 +113,7 @@ function InlineDropRegion({divId, zoneId, emptyWidth, emptyHeight, rootElement,
</Dropdown>;

if (dropRegionContext && droppableTarget) {
const result = (deviceSize === "xs" || (isTouchDevice() && below['md'](deviceSize)))
? dropdownZone : draggableDropZone;
const result = dropRegionContext?.dragAndDropEnabled ? draggableDropZone : dropdownZone;
return skipPortalling ? result : ReactDOM.createPortal(result, droppableTarget);
}
return null;
Expand Down
17 changes: 15 additions & 2 deletions src/app/components/elements/panels/UserAccessibilitySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ export const UserAccessibilitySettings = ({ accessibilitySettings, setAccessibil
/></b>
<p>{`Enabling this will reduce motion effects on the platform. Browser preference will take priority over this setting.`}</p>
</WithLinkableSetting>
<div className="pt-2"/>
<WithLinkableSetting id={"prefer-mathml-feature"}>
<WithLinkableSetting id={"show-inaccessible-warning-feature"}>
<b><StyledCheckbox
checked={accessibilitySettings.SHOW_INACCESSIBLE_WARNING ?? isTeacherOrAbove(user)}
onChange={e => {
Expand All @@ -49,6 +48,20 @@ export const UserAccessibilitySettings = ({ accessibilitySettings, setAccessibil
/></b>
<p id="show-inaccessible-helptext">{`Enabling this will display warnings on certain content that may be inaccessible to assistive technologies.`}</p>
</WithLinkableSetting>
<WithLinkableSetting id={"non-dragging-movement-feature"}>
<b><StyledCheckbox checked={accessibilitySettings.NON_DRAGGING_INPUTS ?? false}
onChange={e => {
setAccessibilitySettings((oldDs) => ({...oldDs, NON_DRAGGING_INPUTS: e.target.checked}));
}}
color={siteSpecific("primary", "")}
label={<p>Enable non-dragging alternative inputs</p>}
id={"non-dragging-movement"}
aria-describedby="non-dragging-movement-helptext"
removeVerticalOffset
/></b>
<p id="non-dragging-helptext">{`Enabling this will allow you to use alternative input methods that don't require dragging for certain question types (e.g. drag-and-drop).`}</p>
</WithLinkableSetting>
{/* Seperate maths-specific setting from the general site-wide accessibility settings */}
<div className="section-divider" />
<div className="pt-2"/>
<WithLinkableSetting id={"prefer-mathml-feature"}>
Expand Down
2 changes: 2 additions & 0 deletions src/app/services/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export enum ACTION_TYPE {
USER_PREFERENCES_RESPONSE_SUCCESS= "USER_PREFERENCES_RESPONSE_SUCCESS",
USER_PREFERENCES_RESPONSE_FAILURE = "USER_PREFERENCES_RESPONSE_FAILURE",

ACCESSIBILITY_TYPE_SET = "ACCESSIBILITY_TYPE_SET",

USER_PASSWORD_RESET_REQUEST= "USER_PASSWORD_RESET_REQUEST",
USER_PASSWORD_RESET_RESPONSE_SUCCESS ="USER_PASSWORD_RESET_RESPONSE_SUCCESS",
USER_PASSWORD_RESET_RESPONSE_FAILURE = "USER_PASSWORD_RESET_RESPONSE_FAILURE",
Expand Down
4 changes: 4 additions & 0 deletions src/app/state/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
topicSlice,
linkableSettingSlice,
sidebarSlice,
accessibilityType,
} from "../index";

export const rootReducer = combineReducers({
Expand Down Expand Up @@ -90,6 +91,9 @@ export const rootReducer = combineReducers({
// Linkable settings
linkableSetting: linkableSettingSlice.reducer,

// Accessibility
accessibilityType,

// API reducer
[isaacApi.reducerPath]: isaacApi.reducer
});
Expand Down
14 changes: 13 additions & 1 deletion src/app/state/reducers/userState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Action, UserPreferencesDTO} from "../../../IsaacAppTypes";
import {AccessibilitySettings, Action, UserPreferencesDTO} from "../../../IsaacAppTypes";
import {ACTION_TYPE} from "../../services";
import {UserAuthenticationSettingsDTO} from "../../../IsaacApiTypes";
import {userApi} from "../index";
Expand Down Expand Up @@ -40,3 +40,15 @@ export const totpChallengePending = (totpChallengePending: TotpChallengePendingS
return totpChallengePending;
}
};

type AccessibilityTypeState = AccessibilitySettings | null;
export const accessibilityType = (accessibilityType: AccessibilityTypeState = null, action: Action) => {
switch (action.type) {
case ACTION_TYPE.USER_PREFERENCES_RESPONSE_SUCCESS:
return action.userPreferences.ACCESSIBILITY ?? accessibilityType;
case ACTION_TYPE.ACCESSIBILITY_TYPE_SET:
return action.accessibilityType;
default:
return accessibilityType;
}
};
7 changes: 7 additions & 0 deletions src/app/state/selectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export const selectors = {
},
includesLLMFreeTextQuestion: (state: AppState) => {
return !!state?.questions?.questions.some(q => q.type === "isaacLLMFreeTextQuestion");
},
includesClozeOrDragAndDropQuestion: (state: AppState) => {
return !!state?.questions?.questions.some(q => q.type === "isaacClozeQuestion" || q.type === "isaacDndQuestion");
}
},

Expand Down Expand Up @@ -91,6 +94,10 @@ export const selectors = {
previousContext: (state: AppState) => state?.pageContext?.previousContext ?? undefined,
stage: (state: AppState) => state?.pageContext?.stage,
subject: (state: AppState) => state?.pageContext?.subject,
},

accessibility: {
type: (state: AppState) => state?.accessibilityType
}
};

Expand Down
4 changes: 4 additions & 0 deletions src/scss/common/questions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ figure .inline-container {
.cloze-dropdown > button {
min-width: 40px !important; // Bootstrap thinks its important

&.btn-secondary, &.btn-outline-secondary {
background-color: white;
}

&.empty.empty {
&, &:hover, &:active, &:focus { // Apply regardless of hover
border: grey solid 1px !important;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading