diff --git a/src/app/components/content/IsaacParsonsQuestion.tsx b/src/app/components/content/IsaacParsonsQuestion.tsx index 10327d0b04..7fa77eda33 100644 --- a/src/app/components/content/IsaacParsonsQuestion.tsx +++ b/src/app/components/content/IsaacParsonsQuestion.tsx @@ -4,38 +4,36 @@ import {IsaacParsonsQuestionDTO, ParsonsChoiceDTO, ParsonsItemDTO} from "../../. import {Col, Row} from "reactstrap"; import { DragDropContext, - Draggable, - DraggableProvided, - DraggableStateSnapshot, - DraggingStyle, DragStart, DragUpdate, Droppable, DroppableProvided, DropResult, - NotDraggingStyle, } from "@hello-pangea/dnd"; import _differenceBy from "lodash/differenceBy"; -import {isDefined, useCurrentQuestionAttempt} from "../../services"; +import {isDefined, PARSONS_INDENT_STEP, PARSONS_MAX_INDENT, useCurrentQuestionAttempt} from "../../services"; import {IsaacQuestionProps} from "../../../IsaacAppTypes"; import classNames from "classnames"; import {Immutable} from "immer"; - -// REMINDER: If you change this, you also have to change $parsons-step in questions.scss -const PARSONS_MAX_INDENT = 3; -const PARSONS_INDENT_STEP = 45; +import { handleParsonsItemDrag, ParsonsDraggableItem } from "../elements/ParsonsDraggableItem"; const IsaacParsonsQuestion = ({doc, questionId, readonly} : IsaacQuestionProps) => { + const {currentAttempt, dispatchSetCurrentAttempt} = useCurrentQuestionAttempt(questionId); + const [availableItems, setAvailableItems] = useState[]>([...doc.items ?? []]); + const attemptItems = (currentAttempt?.items || []) as Immutable[]; + const setAttemptItems = (items: Immutable[]) => { + if (currentAttempt) { + dispatchSetCurrentAttempt({...currentAttempt, items}); + } else { + dispatchSetCurrentAttempt({type: "parsonsChoice", items}); + } + }; - const { currentAttempt, dispatchSetCurrentAttempt } = useCurrentQuestionAttempt(questionId); - - const [ availableItems, setAvailableItems ] = useState[]>([...doc.items ?? []]); - const [ draggedElement, setDraggedElement ] = useState(null); - const [ initialX, setInitialX ] = useState(null); - const [ currentIndent, setCurrentIndent ] = useState(null); - const [ currentMaxIndent, setCurrentMaxIndent ] = useState(0); - const [ currentDestinationIndex, setCurrentDestinationIndex ] = useState(null); - + const [draggedElement, setDraggedElement] = useState(null); + const [initialX, setInitialX] = useState(null); + const [currentIndent, setCurrentIndent] = useState(null); + const [currentMaxIndent, setCurrentMaxIndent] = useState(0); + const [currentDestinationIndex, setCurrentDestinationIndex] = useState(null); const canIndent = (!isDefined(doc.disableIndentation) || !doc.disableIndentation) && !readonly; // WARNING: There's a limit to how far to the right we can drag an element, presumably due to @hello-pangea/dnd @@ -94,51 +92,16 @@ const IsaacParsonsQuestion = ({doc, questionId, readonly} : IsaacQuestionProps[] | undefined, fromIndex: number, dst: Immutable[] | undefined, toIndex: number, indent: number) => { - if (!src || !dst) return; - const srcItem = src.splice(fromIndex, 1)[0]; - dst.splice(toIndex, 0, {...srcItem, indentation: indent}); - }; - const onDragStart = (initial: DragStart) => { const draggedElement: HTMLElement | null = document.getElementById(initial.draggableId); const choiceElement: HTMLElement | null = document.getElementById("parsons-choice-area"); setDraggedElement(draggedElement); setInitialX(choiceElement && choiceElement.getBoundingClientRect().left); + setCurrentIndent(draggedElement?.className.match(/indent-([0-3])/g)?.map((match) => parseInt(match.split('-')[1]))?.[0] || 0); }; const onDragEnd = (result: DropResult) => { - if (!result.source || !result.destination) { - return; - } - if (result.source.droppableId === result.destination.droppableId && result.destination.droppableId === 'answerItems' && currentAttempt) { - // Reorder currentAttempt - const items = [...(currentAttempt?.items || [])]; - moveItem(items, result.source.index, items, result.destination.index, currentIndent || 0); - dispatchSetCurrentAttempt({...currentAttempt, items}); - } else if (result.source.droppableId === result.destination.droppableId && result.destination.droppableId === 'availableItems') { - // Reorder availableItems - const items = [...availableItems]; - moveItem(items, result.source.index, items, result.destination.index, 0); - setAvailableItems(items); - } else if (result.source.droppableId === 'availableItems' && result.destination.droppableId === 'answerItems') { - // Move from availableItems to currentAttempt - const srcItems = [...availableItems]; - const dstItems = [...(currentAttempt?.items || [])]; - moveItem(srcItems, result.source.index, dstItems, result.destination.index, currentIndent || 0); - // We can't guarantee that `currentAttempt` is defined, so we have to explicitly state `type: "parsonsChoice"` here. - dispatchSetCurrentAttempt({type: "parsonsChoice", items: dstItems}); - setAvailableItems(srcItems); - } else if (result.source.droppableId === 'answerItems' && result.destination.droppableId === 'availableItems' && currentAttempt) { - // Move from currentAttempt to availableItems - const srcItems = [...(currentAttempt?.items || [])]; - const dstItems = [...availableItems]; - moveItem(srcItems, result.source.index, dstItems, result.destination.index, 0); - dispatchSetCurrentAttempt({...currentAttempt, items: srcItems }); - setAvailableItems(dstItems); - } else { - console.error("Not sure how we got here..."); - } + handleParsonsItemDrag(result, availableItems, setAvailableItems, attemptItems, setAttemptItems, true, currentIndent); setDraggedElement(null); setInitialX(null); setCurrentIndent(null); @@ -154,61 +117,33 @@ const IsaacParsonsQuestion = ({doc, questionId, readonly} : IsaacQuestionProps { - if (!snapshot.isDropAnimating) { - return style; - } - return { - ...style, - // cannot be 0, but make it super tiny - transitionDuration: `0.001s`, + useEffect(() => { + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('touchmove', onMouseMove); + window.addEventListener('keyup', onKeyUp); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('touchmove', onMouseMove); + window.removeEventListener('keyup', onKeyUp); }; - }; - - const getPreviousItemIndentation = (index: number) => { - if (!currentAttempt?.items) return -1; - const items = [...(currentAttempt.items || [])]; - return items[Math.max(0, index-1)].indentation || 0; - }; - - const reduceIndentation = (index: number) => { - if (!currentAttempt?.items || doc.disableIndentation) return; - - const items = [...(currentAttempt.items || [])]; - if (isDefined(items[index].indentation)) { - const indentedItem = {...items[index], indentation: Math.max((items[index].indentation || 0) - 1, 0)}; - items.splice(index, 1, indentedItem); - } - dispatchSetCurrentAttempt({...currentAttempt, items}); - }; - - const increaseIndentation = (index: number) => { - if (index === 0 || !currentAttempt?.items || doc.disableIndentation) return; - - const items = [...(currentAttempt.items || [])]; - // This condition is insane but of course 0, undefined, and null are all false-y. - if (isDefined(items[index].indentation)) { - const indentedItem = {...items[index], indentation: Math.min((items[index].indentation || 0) + 1, Math.min((items[Math.max(index-1, 0)].indentation || 0) + 1, PARSONS_MAX_INDENT))}; - items.splice(index, 1, indentedItem); - } - dispatchSetCurrentAttempt({...currentAttempt, items}); - }; + }, [onMouseMove, onKeyUp]); - const onCurrentAttemptUpdate = (newCurrentAttempt?: Immutable, newAvailableItems?: Immutable[]) => { - if (!newCurrentAttempt) { + useEffect(() => { + if (!currentAttempt) { const defaultAttempt: ParsonsChoiceDTO = { type: "parsonsChoice", items: [], }; dispatchSetCurrentAttempt(defaultAttempt); } - if (newCurrentAttempt) { + + if (currentAttempt) { // This makes sure that available items and current attempt items contain different items. // This is because available items always start from the document's available items (see constructor) // and the current attempt is assigned afterwards, so we need to carve it out of the available items. // This also takes care of updating the two lists when a user moves items from one to the other. let fixedAvailableItems: ParsonsItemDTO[] = []; - const currentAttemptItems = newCurrentAttempt.items || []; + const currentAttemptItems = currentAttempt.items || []; if (doc.items) { fixedAvailableItems = doc.items.filter(item => { let found = false; @@ -223,30 +158,14 @@ const IsaacParsonsQuestion = ({doc, questionId, readonly} : IsaacQuestionProps 0) { setAvailableItems(fixedAvailableItems); } } - }; - - useEffect(() => { - window.addEventListener('mousemove', onMouseMove); - window.addEventListener('touchmove', onMouseMove); - window.addEventListener('keyup', onKeyUp); - return () => { - window.removeEventListener('mousemove', onMouseMove); - window.removeEventListener('touchmove', onMouseMove); - window.removeEventListener('keyup', onKeyUp); - }; - }, [onMouseMove, onKeyUp]); - - useEffect(() => { - onCurrentAttemptUpdate(currentAttempt, availableItems); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentAttempt, availableItems]); + }, [currentAttempt, availableItems, dispatchSetCurrentAttempt, doc.items]); return
@@ -254,90 +173,32 @@ const IsaacParsonsQuestion = ({doc, questionId, readonly} : IsaacQuestionProps
- {/* TODO Accessibility */} - +

Available items

{(provided: DroppableProvided) => { - return
0 ? "" : "empty"}`}> - {availableItems && availableItems.map((item, index) => { - return - {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => { - return
-
{item.value}
-
; - }} -
; - })} + return
0), "is-dragging": draggedElement})}> + {availableItems && availableItems.map((item, index) => + + )} {(!availableItems || availableItems.length === 0) &&
 
} {provided.placeholder}
; }} - -

Your answer

+ +

Your answer

{(provided: DroppableProvided) => { - return
0)})}> - {currentAttempt && currentAttempt.items && currentAttempt.items.map((item, index) => { - const canDecreaseIndentation = canIndent && isDefined(item?.indentation) && item.indentation > 0; - const canIncreaseIndentation = canIndent && isDefined(item?.indentation) && index !== 0 && item.indentation <= getPreviousItemIndentation(index) && item.indentation < PARSONS_MAX_INDENT; - return - {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => { - // eslint-disable-next-line jsx-a11y/no-static-element-interactions - return
(e.target as HTMLElement).classList.add('show-controls')} - onMouseLeave={e => (e.target as HTMLElement).classList.remove('show-controls')} - id={`${item.id || index}|parsons-item-choice`} - className={`parsons-item indent-${item.indentation}`} - ref={provided.innerRef} - {...provided.draggableProps} - {...provided.dragHandleProps} - style={getStyle(provided.draggableProps.style, snapshot)} - > -
-                                                    {item.value}
-                                                    {canIndent && 
- - -
} -
-
; - }} -
; - })} + return
0), "is-dragging": draggedElement})}> + {currentAttempt && currentAttempt.items && currentAttempt.items.map((item, index) => + + )} {(!currentAttempt || currentAttempt?.items?.length === 0) &&
{readonly ? "No answer entered" : "Drag items across to build your answer"} diff --git a/src/app/components/content/IsaacReorderQuestion.tsx b/src/app/components/content/IsaacReorderQuestion.tsx index f65e47ef48..aa65a44efe 100644 --- a/src/app/components/content/IsaacReorderQuestion.tsx +++ b/src/app/components/content/IsaacReorderQuestion.tsx @@ -2,84 +2,30 @@ import React, {useEffect, useState} from "react"; import {IsaacContentValueOrChildren} from "./IsaacContentValueOrChildren"; import {IsaacReorderQuestionDTO, ItemChoiceDTO, ItemDTO} from "../../../IsaacApiTypes"; import {Col, Row} from "reactstrap"; -import {DragDropContext, Draggable, Droppable, DropResult} from "@hello-pangea/dnd"; +import {DragDropContext, Droppable, DropResult} from "@hello-pangea/dnd"; import _differenceBy from "lodash/differenceBy"; import {useCurrentQuestionAttempt} from "../../services"; import {IsaacQuestionProps} from "../../../IsaacAppTypes"; import classNames from "classnames"; -import {Markup} from "../elements/markup"; import {Immutable} from "immer"; - -const ReorderDraggableItem = ({item, index, inAvailableItems, readonly}: {item: Immutable; index: number; inAvailableItems?: boolean; readonly?: boolean}) => { - return - {(provided) => { - return
- - {item?.value ?? ""} - -
; - }} -
; -}; +import { handleParsonsItemDrag, ParsonsDraggableItem } from "../elements/ParsonsDraggableItem"; const IsaacReorderQuestion = ({doc, questionId, readonly} : IsaacQuestionProps) => { - const {currentAttempt, dispatchSetCurrentAttempt} = useCurrentQuestionAttempt(questionId); - const [availableItems, setAvailableItems] = useState[]>([...doc.items ?? []]); - - const moveItem = (src: Immutable[] | undefined, fromIndex: number, dst: Immutable[] | undefined, toIndex: number) => { - if (!src || !dst) return; - const srcItem = src.splice(fromIndex, 1)[0]; - dst.splice(toIndex, 0, srcItem); - }; - - const onDragEnd = (result: DropResult) => { - if (!result.source || !result.destination) { - return; - } - if (result.source.droppableId === result.destination.droppableId && result.destination.droppableId === 'answerItems' && currentAttempt) { - // Reorder currentAttempt - const items = [...(currentAttempt?.items || [])]; - moveItem(items, result.source.index, items, result.destination.index); + const attemptItems = (currentAttempt?.items || []) as Immutable[]; + const setAttemptItems = (items: Immutable[]) => { + if (currentAttempt) { dispatchSetCurrentAttempt({...currentAttempt, items}); - } else if (result.source.droppableId === result.destination.droppableId && result.destination.droppableId === 'availableItems') { - // Reorder availableItems - const items = [...availableItems]; - moveItem(items, result.source.index, items, result.destination.index); - setAvailableItems(items); - } else if (result.source.droppableId === 'availableItems' && result.destination.droppableId === 'answerItems') { - // Move from availableItems to currentAttempt - const srcItems = [...availableItems]; - const dstItems = [...(currentAttempt?.items || [])]; - moveItem(srcItems, result.source.index, dstItems, result.destination.index); - // We can't guarantee that `currentAttempt` is defined, so we have to explicitly state `type: "itemChoice"` here. - dispatchSetCurrentAttempt({type: "itemChoice", items: dstItems}); - setAvailableItems(srcItems); - } else if (result.source.droppableId === 'answerItems' && result.destination.droppableId === 'availableItems' && currentAttempt) { - // Move from currentAttempt to availableItems - const srcItems = [...(currentAttempt?.items || [])]; - const dstItems = [...availableItems]; - moveItem(srcItems, result.source.index, dstItems, result.destination.index); - dispatchSetCurrentAttempt({...currentAttempt, items: srcItems}); - setAvailableItems(dstItems); } else { - console.error("Not sure how we got here..."); + dispatchSetCurrentAttempt({type: "itemChoice", items}); } }; + const onDragEnd = (result: DropResult) => { + handleParsonsItemDrag(result, availableItems, setAvailableItems, attemptItems, setAttemptItems); + }; + const onCurrentAttemptUpdate = (newCurrentAttempt?: Immutable, newAvailableItems?: Immutable[]) => { if (!newCurrentAttempt) { const defaultAttempt: ItemChoiceDTO = { @@ -139,7 +85,8 @@ const IsaacReorderQuestion = ({doc, questionId, readonly} : IsaacQuestionProps 0), "drag-over": snapshot.isDraggingOver})} > {availableItems && availableItems.map((item, index) => - )} + )} {(!availableItems || availableItems.length === 0) ?
 
: provided.placeholder} @@ -155,7 +102,9 @@ const IsaacReorderQuestion = ({doc, questionId, readonly} : IsaacQuestionProps 0), "drag-over": snapshot.isDraggingOver})} > {currentAttempt && currentAttempt.items && currentAttempt.items.map((item, index) => - )} + []) => dispatchSetCurrentAttempt({...currentAttempt, items})} + items={(currentAttempt?.items || []) as Immutable[]}/>)} {(!currentAttempt || currentAttempt?.items?.length === 0) ?
{readonly ? "No answer entered" : "Drag items across to build your answer"} diff --git a/src/app/components/elements/ParsonsDraggableItem.tsx b/src/app/components/elements/ParsonsDraggableItem.tsx new file mode 100644 index 0000000000..a23d0b44a2 --- /dev/null +++ b/src/app/components/elements/ParsonsDraggableItem.tsx @@ -0,0 +1,243 @@ +import React, {Dispatch, SetStateAction} from "react"; +import {ParsonsItemDTO} from "../../../IsaacApiTypes"; +import { + Draggable, + DraggableProvided, + DraggableStateSnapshot, + DraggingStyle, + DropResult, + NotDraggingStyle, +} from "@hello-pangea/dnd"; +import _differenceBy from "lodash/differenceBy"; +import {isDefined, PARSONS_MAX_INDENT} from "../../services"; +import classNames from "classnames"; +import {Immutable} from "immer"; +import { Markup } from "../elements/markup"; +import { Spacer } from "../elements/Spacer"; + +export const moveParsonsItem = ( + src: Immutable[] | undefined, + fromIndex: number, + dst: Immutable[] | undefined, + toIndex: number, + indent?: number +) => { + if (!src || !dst) return; + const srcItem = src.splice(fromIndex, 1)[0]; + dst.splice(toIndex, 0, {...srcItem, indentation: indent}); +}; + +export const handleParsonsItemDrag = ( + result: DropResult, + availableItems: Immutable[], + setAvailableItems: Dispatch[]>>, + attemptItems: Immutable[], + setAttemptItems: Dispatch[]>> | ((items: Immutable[]) => void), + isParsons?: boolean, + currentIndent?: number | null +) => { + if (!result.source || !result.destination) { + return; + } + if (result.source.droppableId === result.destination.droppableId && result.destination.droppableId === 'answerItems' && attemptItems) { + // Reorder currentAttempt + const items = [...attemptItems]; + moveParsonsItem(items, result.source.index, items, result.destination.index, isParsons ? (currentIndent || 0) : undefined); + setAttemptItems(items); + } else if (result.source.droppableId === result.destination.droppableId && result.destination.droppableId === 'availableItems') { + // Reorder availableItems + const items = [...availableItems]; + moveParsonsItem(items, result.source.index, items, result.destination.index, isParsons ? 0 : undefined); + setAvailableItems(items); + } else if (result.source.droppableId === 'availableItems' && result.destination.droppableId === 'answerItems') { + // Move from availableItems to currentAttempt + const srcItems = [...availableItems]; + const dstItems = [...attemptItems]; + moveParsonsItem(srcItems, result.source.index, dstItems, result.destination.index, isParsons ? (currentIndent || 0) : undefined); + setAttemptItems(dstItems); + setAvailableItems(srcItems); + } else if (result.source.droppableId === 'answerItems' && result.destination.droppableId === 'availableItems' && attemptItems) { + // Move from currentAttempt to availableItems + const srcItems = [...attemptItems]; + const dstItems = [...availableItems]; + moveParsonsItem(srcItems, result.source.index, dstItems, result.destination.index, isParsons ? 0 : undefined); + setAttemptItems(srcItems); + setAvailableItems(dstItems); + } else { + console.error("Not sure how we got here..."); + } +}; + +interface ReorderButtonsProps { + index: number; + items: Immutable[]; + setItems: Dispatch[]>> | ((items: Immutable[]) => void); + isParsons?: boolean; + currentIndent?: number | null; +} + +const ReorderButtons = ({index, items, setItems, isParsons, currentIndent}: ReorderButtonsProps) => { + const canReorderUp = index !== 0; + const canReorderDown = index !== items.length - 1; + return
+ + +
; +}; + +interface IndentButtonsProps { + currentItem: Immutable; + index: number; + items: Immutable[]; + setItems: Dispatch[]>> | ((items: Immutable[]) => void); + canIndent?: boolean; +} + +const IndentButtons = ({currentItem, index, items, setItems, canIndent}: IndentButtonsProps) => { + const getPreviousItemIndentation = (index: number) => { + const newItems = [...(items || [])]; + + return newItems[Math.max(0, index-1)].indentation || 0; + }; + + const reduceIndentation = (index: number) => { + const newItems = [...(items || [])]; + + if (isDefined(newItems[index].indentation)) { + const indentedItem = {...newItems[index], indentation: Math.max((newItems[index].indentation || 0) - 1, 0)}; + newItems.splice(index, 1, indentedItem); + } + setItems(newItems); + }; + + const increaseIndentation = (index: number) => { + if (index === 0) return; + + const newItems = [...(items || [])]; + // This condition is insane but of course 0, undefined, and null are all false-y. + if (isDefined(newItems[index].indentation)) { + const indentedItem = {...newItems[index], indentation: Math.min( + (newItems[index].indentation || 0) + 1, Math.min((newItems[Math.max(index-1, 0)].indentation || 0) + 1, PARSONS_MAX_INDENT) + )}; + newItems.splice(index, 1, indentedItem); + } + setItems(newItems); + }; + + const canDecreaseIndentation = canIndent && isDefined(currentItem?.indentation) && currentItem.indentation > 0; + const canIncreaseIndentation = canIndent && isDefined(currentItem?.indentation) && index !== 0 && + currentItem.indentation <= getPreviousItemIndentation(index) && currentItem.indentation < PARSONS_MAX_INDENT; + return
+ + +
; +}; + +type BaseDraggableProps = { + currentItem: Immutable; + index: number; + items: Immutable[]; + setItems: Dispatch[]>> | ((items: Immutable[]) => void); + readonly?: boolean; +}; + +type AvailableItemsProps = { + inAvailableItems: true; + attemptItems: Immutable[]; + setAttemptItems: (items: Immutable[]) => void; + isParsons?: boolean; + canIndent?: false; +}; + +type AttemptItemsProps = { + inAvailableItems?: false; + attemptItems?: undefined; + setAttemptItems?: undefined; +} & ( + { isParsons: true; canIndent?: boolean; } | + { isParsons?: false; canIndent?: false; } +); + +export type ParsonsDraggableItemProps = BaseDraggableProps & (AvailableItemsProps | AttemptItemsProps); + +export const ParsonsDraggableItem = ({currentItem, index, items, setItems, inAvailableItems, readonly, attemptItems, setAttemptItems, canIndent, isParsons}: ParsonsDraggableItemProps) => { + const getStyle = (style: DraggingStyle | NotDraggingStyle | undefined, snapshot: DraggableStateSnapshot) => { + if (!snapshot.isDropAnimating || !isParsons) return style; + + return { + ...style, + // cannot be 0, but make it super tiny + transitionDuration: `0.001s`, + }; + }; + + const itemType = `${isParsons ? "parsons" : "reorder"}-item`; + return + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => { + return
+ +
+                    
+                        {currentItem.value}
+                    
+                
+ {inAvailableItems && <> + + + } + {canIndent && <> + + + } +
; + }} +
; +}; diff --git a/src/app/services/constants.ts b/src/app/services/constants.ts index 406ef9aa2c..b8f371d5c9 100644 --- a/src/app/services/constants.ts +++ b/src/app/services/constants.ts @@ -1301,6 +1301,10 @@ export const NULL_CLOZE_ITEM: ItemDTO = { export const FIGURE_DROP_ZONE_PLACEHOLDER_SIZE = "24px"; +// REMINDER: If you change this, you also have to change $parsons-step in questions.scss +export const PARSONS_MAX_INDENT = 3; +export const PARSONS_INDENT_STEP = 45; + // Legacy matches: [inline-question:questionId], [inline-question:questionId|w-50], [inline-question:questionId|h-50] or [inline-question:questionId|w-50h-200] // Matches: all legacy, [inline-question:questionId class="{classes}"] export const inlineQuestionRegex = /\[inline-question:(?[a-zA-Z0-9_-]+)(? *\| *(?w-\d+)?(?h-\d+)?| +class=(?:["']|'|&[rl]?quot;)(?[a-zA-Z0-9 _-]+?)(?:["']|'|&[rl]?quot;))?\]/g; diff --git a/src/scss/common/questions.scss b/src/scss/common/questions.scss index 4dec36e360..e83fffe120 100644 --- a/src/scss/common/questions.scss +++ b/src/scss/common/questions.scss @@ -352,7 +352,7 @@ figure .inline-container { .parsons-question { .parsons-items { - border: solid 1px #00000021; + border: solid 1px $gray-136; padding: 0 0.5em; &.empty { @@ -363,23 +363,6 @@ figure .inline-container { } } - .parsons-item > pre { - margin: 0.5rem 0; - padding: 0.5em 1em; - cursor: grab; // Doesn't work? - } - - .reorder-item { - > * { - overflow-x: auto; - margin: 0.5rem 0; - padding: 0.5em 1em; - cursor: grab; - background: var(--color-neutral-white, white); - border: solid 1px #00000021; - } - } - // REMINDER: If you change the PARSONS_MAX_INDENT and PARSONS_INDENT_STEP // constants, you also have to change these two in here. $parsons-max-indent: 3; @@ -392,50 +375,74 @@ figure .inline-container { } } - position: relative; + .indent-buttons { + display: flex; + align-items: center; - .controls { - display: none; - } + button { + display: grid; + position: relative; + justify-content: center; + align-content: center; - .show-controls { - .controls { - position: absolute; - right: 0.5rem; - top: 0.5rem; - display: block; - - button { - display: inline-block; - width: 20px; - height: 20px; - padding: 0; - background: $gray-120; - - &.show:hover { - background-color: $primary !important; - } - - &.hide { - opacity: 0.2; - cursor: default; - } - - &.show { - opacity: 1.0; - } + width: 20px; + height: 20px; + padding: 0; + background-color: $gray-120; + + &.show:hover { + background-color: $primary; + } + + &.hide { + opacity: 0.2; + cursor: default; } + + &.show { + opacity: 1.0; + } + } + + // Use negative z-index rather than `display: none` to hide buttons so they can still be selected using keyboard controls + &:not(:has(:focus)) button { + z-index: -999; + } + &:has(:focus) button { + z-index: 2; } } } - // The fixed position of a selected parsons-item causes the box-shadow to be drawn in the wrong place, - // so we need to apply it to the correctly positioned child instead .parsons-item, .reorder-item { - &:focus-visible:focus-visible { - box-shadow: none; - > * { - @include focus-outline; + display: flex; + align-items: center; + position: relative; + border: solid 1px $gray-136; + margin: 0.5rem 0; + background: var(--color-neutral-white, white); + + pre { + margin: 0.25em 0.5em; + padding: 0.25em 0; + border: none; + background: none; + } + + .reorder-buttons { + display: flex; + flex-direction: column; + align-items: center; + + button { + display: grid; + justify-content: center; + } + } + + &:hover, &:focus-within { + .indent-buttons button { + z-index: 2; } } } diff --git a/src/scss/cs/questions.scss b/src/scss/cs/questions.scss index e84e8d912c..164e9703f6 100644 --- a/src/scss/cs/questions.scss +++ b/src/scss/cs/questions.scss @@ -188,3 +188,11 @@ margin-top: 0.7rem; } } + +.parsons-question { + .parsons-item, .reorder-item { + .reorder-buttons button.disabled { + opacity: 0.5; + } + } +} diff --git a/src/scss/phy/typography.scss b/src/scss/phy/typography.scss index dc3efcc286..9f8d1ec634 100644 --- a/src/scss/phy/typography.scss +++ b/src/scss/phy/typography.scss @@ -39,7 +39,6 @@ pre { background: var(--color-neutral-white); padding: 1rem; font-weight: 400; - border: solid 1px #00000021; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.875rem; diff --git a/src/test/pages/__image_snapshots__/ada/Question type isaacParsonsQuestion should have no visual regression #0.png b/src/test/pages/__image_snapshots__/ada/Question type isaacParsonsQuestion should have no visual regression #0.png index b7e55b7277..530010a75b 100644 Binary files a/src/test/pages/__image_snapshots__/ada/Question type isaacParsonsQuestion should have no visual regression #0.png and b/src/test/pages/__image_snapshots__/ada/Question type isaacParsonsQuestion should have no visual regression #0.png differ diff --git a/src/test/pages/__image_snapshots__/sci/Question type isaacParsonsQuestion should have no visual regression #0.png b/src/test/pages/__image_snapshots__/sci/Question type isaacParsonsQuestion should have no visual regression #0.png index cc29399c65..1eee383ef7 100644 Binary files a/src/test/pages/__image_snapshots__/sci/Question type isaacParsonsQuestion should have no visual regression #0.png and b/src/test/pages/__image_snapshots__/sci/Question type isaacParsonsQuestion should have no visual regression #0.png differ