-
Notifications
You must be signed in to change notification settings - Fork 0
feat(game-editor): add Image Settings modal for flashcard images #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,105 @@ | ||||
| import React, { useEffect } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { Button, Image } from '@openedx/paragon'; | ||||
| import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; | ||||
| import BaseModal from '../../sharedComponents/BaseModal'; | ||||
| import AltTextControls from '../../sharedComponents/ImageUploadModal/ImageSettingsModal/AltTextControls'; | ||||
| import ErrorAlert from '../../sharedComponents/ErrorAlerts/ErrorAlert'; | ||||
| import * as hooks from '../../sharedComponents/ImageUploadModal/ImageSettingsModal/hooks'; | ||||
| import messages from './messages'; | ||||
| import './GameImageSettingsModal.scss'; | ||||
|
|
||||
| /** | ||||
| * Simplified image settings modal for game editor flashcards | ||||
| * Only includes alt-text controls, excludes width/height dimensions | ||||
| * @param {bool} isOpen - is the modal open? | ||||
| * @param {func} close - close the modal | ||||
| * @param {obj} imageData - current image data object with url and altText | ||||
| * @param {func} onSave - save callback with updated alt text | ||||
| */ | ||||
| const GameImageSettingsModal = ({ | ||||
| close, | ||||
| isOpen, | ||||
| imageData, | ||||
| onSave, | ||||
| }) => { | ||||
| const intl = useIntl(); | ||||
| const altText = hooks.altTextHooks(imageData?.altText || ''); | ||||
|
|
||||
| useEffect(() => { | ||||
| if (imageData && isOpen) { | ||||
| altText.setValue(imageData.altText || ''); | ||||
| altText.setIsDecorative(!imageData.altText || imageData.altText === ''); | ||||
| } | ||||
| }, [imageData?.altText, imageData?.url, isOpen]); | ||||
|
|
||||
| const handleSave = () => { | ||||
| if (!altText.isDecorative && !altText.value?.trim()) { | ||||
| altText.error.set(); | ||||
| return; | ||||
| } | ||||
|
|
||||
| onSave({ | ||||
| altText: altText.isDecorative ? '' : altText.value, | ||||
| isDecorative: altText.isDecorative, | ||||
| }); | ||||
| close(); | ||||
|
||||
| close(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| .game-img-settings-container { | ||
| .game-img-thumbnail-container { | ||
| width: 250px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
|
|
||
| .game-img-thumbnail { | ||
| max-height: 250px; | ||
| max-width: 250px; | ||
| object-fit: contain; | ||
| } | ||
| } | ||
|
|
||
| hr { | ||
| width: 1px; | ||
| } | ||
|
|
||
| .game-img-settings-controls { | ||
| flex: 1; | ||
| min-width: 375px; | ||
|
|
||
| .img-settings-control-label { | ||
| font-size: 1rem; | ||
| } | ||
|
|
||
| .decorative-control-label label { | ||
| font-size: .75rem; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,6 +43,7 @@ import Button from '../../sharedComponents/Button'; | |||||||||||||||||||||||||||||
| import DraggableList, { SortableItem } from '../../../generic/DraggableList'; | ||||||||||||||||||||||||||||||
| import messages from './messages'; | ||||||||||||||||||||||||||||||
| import PictureIcon from './PictureIcon'; | ||||||||||||||||||||||||||||||
| import GameImageSettingsModal from './GameImageSettingsModal'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export const hooks = { | ||||||||||||||||||||||||||||||
| getContent: ({ type, settings, list }) => { | ||||||||||||||||||||||||||||||
|
|
@@ -89,6 +90,8 @@ export const GameEditor = ({ | |||||||||||||||||||||||||||||
| list, | ||||||||||||||||||||||||||||||
| updateTerm, | ||||||||||||||||||||||||||||||
| updateDefinition, | ||||||||||||||||||||||||||||||
| updateTermImageAlt, | ||||||||||||||||||||||||||||||
| updateDefinitionImageAlt, | ||||||||||||||||||||||||||||||
| toggleOpen, | ||||||||||||||||||||||||||||||
| setList, | ||||||||||||||||||||||||||||||
| addCard, | ||||||||||||||||||||||||||||||
|
|
@@ -106,6 +109,12 @@ export const GameEditor = ({ | |||||||||||||||||||||||||||||
| const [validationErrors, setValidationErrors] = useState({}); | ||||||||||||||||||||||||||||||
| const [localInputValues, setLocalInputValues] = useState({}); | ||||||||||||||||||||||||||||||
| const [isAlertVisible, setIsAlertVisible] = useState(true); | ||||||||||||||||||||||||||||||
| const [imageSettingsModal, setImageSettingsModal] = useState({ | ||||||||||||||||||||||||||||||
| isOpen: false, | ||||||||||||||||||||||||||||||
| imageData: null, | ||||||||||||||||||||||||||||||
| cardIndex: null, | ||||||||||||||||||||||||||||||
| imageType: null, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const MAX_TERM_LENGTH = 120; | ||||||||||||||||||||||||||||||
| const MAX_DEFINITION_LENGTH = 120; | ||||||||||||||||||||||||||||||
|
|
@@ -322,9 +331,56 @@ export const GameEditor = ({ | |||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const renderImageDisplay = useCallback((imageUrl, filePath, index, imageType) => ( | ||||||||||||||||||||||||||||||
| const renderImageDisplay = useCallback((imageUrl, filePath, index, imageType, altText) => ( | ||||||||||||||||||||||||||||||
| <div className="card-image-area d-flex align-items-center align-self-stretch"> | ||||||||||||||||||||||||||||||
| <img className="card-image" src={imageUrl} alt={`${imageType.toUpperCase()}_IMG`} /> | ||||||||||||||||||||||||||||||
| <OverlayTrigger | ||||||||||||||||||||||||||||||
| placement="bottom" | ||||||||||||||||||||||||||||||
| overlay={( | ||||||||||||||||||||||||||||||
| <Tooltip id={`image-settings-tooltip-${imageType}-${index}`}> | ||||||||||||||||||||||||||||||
| <IconButton | ||||||||||||||||||||||||||||||
| src={PictureIcon} | ||||||||||||||||||||||||||||||
| iconAs={Icon} | ||||||||||||||||||||||||||||||
| alt="IMG" | ||||||||||||||||||||||||||||||
| variant="plain" | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
|
Comment on lines
+340
to
+345
|
||||||||||||||||||||||||||||||
| <IconButton | |
| src={PictureIcon} | |
| iconAs={Icon} | |
| alt="IMG" | |
| variant="plain" | |
| /> | |
| {intl.formatMessage(messages.imageSettingsTooltip)} |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OverlayTrigger is rendering a Tooltip that contains an IconButton instead of tooltip text. This is likely not the intended UX (the tooltip will show an icon, not a label) and places an interactive control inside a tooltip, which is problematic for accessibility. Use the tooltip to render the localized label (e.g., messages.imageSettingsTooltip) and keep any icon/button outside the tooltip (or switch to IconButtonWithTooltip if you want an icon with a tooltip).
| <IconButton | |
| src={PictureIcon} | |
| iconAs={Icon} | |
| alt="IMG" | |
| variant="plain" | |
| /> | |
| {intl.formatMessage(messages.imageSettingsTooltip)} |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The card image alt attribute is still a placeholder (e.g., TERM_IMG) and does not use the saved alt text. This means the new alt-text setting won’t improve screen reader output. Use the stored altText value for alt (and an empty string when decorative) so the persisted alt text is actually applied.
| alt={`${imageType.toUpperCase()}_IMG`} | |
| alt={altText || ''} |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New Image Settings behavior (opening the modal from the image and saving alt text back to Redux via updateTermImageAlt / updateDefinitionImageAlt) isn’t covered by tests. Since this file already has index.test.jsx covering image upload/delete, add tests that click the image to open the modal and verify that saving calls the correct update action with the entered alt text.
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -156,6 +156,26 @@ const messages = defineMessages({ | |||||||||||
| defaultMessage: 'On', | ||||||||||||
| description: 'Label for on toggle button.', | ||||||||||||
| }, | ||||||||||||
| imageSettingsTooltip: { | ||||||||||||
| id: 'GameEditor.imageSettingsTooltip', | ||||||||||||
| defaultMessage: 'Image settings', | ||||||||||||
| description: 'Tooltip for image settings button.', | ||||||||||||
| }, | ||||||||||||
|
Comment on lines
+159
to
+163
|
||||||||||||
| imageSettingsTooltip: { | |
| id: 'GameEditor.imageSettingsTooltip', | |
| defaultMessage: 'Image settings', | |
| description: 'Tooltip for image settings button.', | |
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useEffect has dependencies on
imageData?.altTextandimageData?.url, but it also callsaltText.setValueandaltText.setIsDecorative. ThealtTextobject should be included in the dependency array to avoid stale closures, or the effect should be restructured to avoid this issue.