feat: add fullscreen button to XBlock editor (#2892)

This commit is contained in:
Rômulo Penido
2026-02-27 12:05:14 -03:00
committed by GitHub
parent 060f7d4618
commit 2e6209314f
6 changed files with 104 additions and 28 deletions

View File

@@ -1,7 +1,16 @@
import React, { useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle } from '@openedx/paragon';
import {
ActionRow,
Icon,
IconButton,
ModalDialog,
ModalCloseButton,
Stack,
useToggle,
} from '@openedx/paragon';
import { Close, CloseFullscreen, OpenInFull } from '@openedx/paragon/icons';
import { LibraryBlock } from '../library-authoring/LibraryBlock';
import { EditorModalWrapper } from './containers/EditorContainer';
@@ -11,6 +20,8 @@ import messages from './messages';
import CancelConfirmModal from './containers/EditorContainer/components/CancelConfirmModal';
import { IframeProvider } from '../generic/hooks/context/iFrameContext';
import editorModalWrapperMessages from './containers/EditorContainer/messages';
interface AdvancedEditorProps {
usageKey: string,
onClose: (() => void) | null,
@@ -20,6 +31,7 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
const intl = useIntl();
const { showToast } = React.useContext(ToastContext);
const [isCancelConfirmOpen, openCancelConfirmModal, closeCancelConfirmModal] = useToggle(false);
const [isFullscreen, , , toggleFullscreen] = useToggle(false);
useEffect(() => {
const handleIframeMessage = (event) => {
@@ -49,7 +61,28 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
return (
<>
<EditorModalWrapper onClose={openCancelConfirmModal}>
<EditorModalWrapper onClose={openCancelConfirmModal} fullscreen={isFullscreen}>
<ModalDialog.Header>
<ActionRow>
<ModalDialog.Title>
{intl.formatMessage(editorModalWrapperMessages.modalTitle)}
</ModalDialog.Title>
<ActionRow.Spacer />
<Stack direction="horizontal" reversed gap={1}>
<ModalCloseButton
as={IconButton}
src={Close}
iconAs={Icon}
/>
<IconButton
src={isFullscreen ? CloseFullscreen : OpenInFull}
iconAs={Icon}
alt={intl.formatMessage(messages.advancedEditorFullscreenButtonAlt)}
onClick={toggleFullscreen}
/>
</Stack>
</ActionRow>
</ModalDialog.Header>
<IframeProvider>
<LibraryBlock
usageKey={usageKey}

View File

@@ -8,39 +8,57 @@ import {
IconButton,
ModalDialog,
Spinner,
Stack,
Toast,
useToggle,
} from '@openedx/paragon';
import { Close } from '@openedx/paragon/icons';
import { Close, CloseFullscreen, OpenInFull } from '@openedx/paragon/icons';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { parseErrorMsg } from '@src/library-authoring/add-content/AddContent';
import libraryMessages from '@src/library-authoring/add-content/messages';
import usePromptIfDirty from '@src/generic/promptIfDirty/usePromptIfDirty';
import { EditorComponent } from '../../EditorComponent';
import TitleHeader from './components/TitleHeader';
import * as hooks from './hooks';
import messages from './messages';
import { parseErrorMsg } from '../../../library-authoring/add-content/AddContent';
import libraryMessages from '../../../library-authoring/add-content/messages';
import './index.scss';
import usePromptIfDirty from '../../../generic/promptIfDirty/usePromptIfDirty';
import CancelConfirmModal from './components/CancelConfirmModal';
interface WrapperProps {
children: React.ReactNode;
}
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => {
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void, fullscreen?: boolean }> = (
{
children,
onClose,
fullscreen = false,
},
) => {
const intl = useIntl();
const title = intl.formatMessage(messages.modalTitle);
return (
<ModalDialog isOpen size="xl" isOverflowVisible={false} onClose={onClose} title={title}>{children}</ModalDialog>
<ModalDialog
isOpen
onClose={onClose}
title={title}
size={fullscreen ? 'fullscreen' : 'xl'}
isOverflowVisible={false}
hasCloseButton={false}
>
{children}
</ModalDialog>
);
};
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => <ModalDialog.Body className="pb-0">{ children }</ModalDialog.Body>;
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => <ModalDialog.Body className="pb-0">{children}</ModalDialog.Body>;
// eslint-disable-next-line react/jsx-no-useless-fragment
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => <>{ children }</>;
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => <>{children}</>;
interface Props extends EditorComponent {
children: React.ReactNode;
@@ -63,6 +81,7 @@ const EditorContainer: React.FC<Props> = ({
const [saved, setSaved] = React.useState(false);
const isInitialized = hooks.isInitialized();
const { isCancelConfirmOpen, openCancelConfirmModal, closeCancelConfirmModal } = hooks.cancelConfirmModalToggle();
const [isFullscreen, , , toggleFullscreen] = useToggle(false);
const handleCancel = hooks.handleCancel({ onClose, returnFunction });
const { createFailed, createFailedError } = hooks.createFailed();
const disableSave = !isInitialized;
@@ -97,8 +116,9 @@ const EditorContainer: React.FC<Props> = ({
handleCancel();
}
};
return (
<EditorModalWrapper onClose={confirmCancelIfDirty}>
<EditorModalWrapper onClose={confirmCancelIfDirty} fullscreen={isFullscreen}>
{createFailed && (
<Toast show onClose={clearCreateFailed}>
{parseErrorMsg(
@@ -127,15 +147,27 @@ const EditorContainer: React.FC<Props> = ({
/>
<ModalDialog.Header className="shadow-sm zindex-10">
<div className="d-flex flex-row justify-content-between">
<h2 className="h3 col pl-0">
<TitleHeader isInitialized={isInitialized} />
</h2>
<IconButton
src={Close}
iconAs={Icon}
onClick={confirmCancelIfDirty}
alt={intl.formatMessage(messages.exitButtonAlt)}
/>
<ActionRow>
<h2 className="h3 col pl-0">
<TitleHeader isInitialized={isInitialized} />
</h2>
<ActionRow.Spacer />
<Stack direction="horizontal" reversed gap={1}>
<IconButton
src={Close}
iconAs={Icon}
onClick={confirmCancelIfDirty}
alt={intl.formatMessage(messages.exitButtonAlt)}
autoFocus
/>
<IconButton
src={isFullscreen ? CloseFullscreen : OpenInFull}
iconAs={Icon}
alt={intl.formatMessage(messages.toggleFullscreenButtonLabel)}
onClick={toggleFullscreen}
/>
</Stack>
</ActionRow>
</div>
</ModalDialog.Header>
<EditorModalBody>

View File

@@ -1,7 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
cancelConfirmTitle: {
id: 'authoring.editorContainer.cancelConfirm.title',
defaultMessage: 'Exit the editor?',
@@ -57,6 +56,11 @@ const messages = defineMessages({
defaultMessage: 'Save',
description: 'Label for Save button',
},
toggleFullscreenButtonLabel: {
id: 'authoring.editorHeader.toggleFullscreen.label',
defaultMessage: 'Toggle Fullscreen',
description: 'Label for toggle fullscreen button',
},
});
export default messages;

View File

@@ -36,6 +36,11 @@ const messages = defineMessages({
defaultMessage: 'An unexpected error occurred in the editor',
description: 'Generic error message shown when an error occurs in the Advanced Editor.',
},
advancedEditorFullscreenButtonAlt: {
id: 'authoring.advancedEditor.fullscreenButton.alt',
defaultMessage: 'Toggle Fullscreen',
description: 'Alt text for the Fullscreen button',
},
});
export default messages;

View File

@@ -20,7 +20,11 @@ export const SANDBOX_OPTIONS = [
].join(' ');
const ModalIframe = forwardRef<HTMLIFrameElement, ModalIframeProps>(
({ title, className, ...props }, ref: ForwardedRef<HTMLIFrameElement>) => (
({
title,
className = '',
...props
}, ref: ForwardedRef<HTMLIFrameElement>) => (
<iframe
title={title}
className={classNames('modal-iframe', className)}
@@ -36,8 +40,4 @@ const ModalIframe = forwardRef<HTMLIFrameElement, ModalIframeProps>(
),
);
ModalIframe.defaultProps = {
className: '',
};
export default ModalIframe;

View File

@@ -2,8 +2,10 @@ import { getConfig } from '@edx/frontend-platform';
import React from 'react';
import { useQueryClient } from '@tanstack/react-query';
import EditorPage from '../../editors/EditorPage';
import { getBlockType } from '../../generic/key-utils';
import EditorPage from '@src/editors/EditorPage';
import { getBlockType } from '@src/generic/key-utils';
import { useLibraryContext } from '../common/context/LibraryContext';
import { invalidateComponentData } from '../data/apiHooks';