feature: add button to open cheatsheet in a new tab

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-03-27 14:56:50 +02:00
parent 438a5466e0
commit 9b9eafc948
7 changed files with 108 additions and 39 deletions

View file

@ -645,6 +645,7 @@
"cheatsheet": { "cheatsheet": {
"button": "Open Cheatsheet", "button": "Open Cheatsheet",
"modal":{ "modal":{
"popup": "Open in new tab",
"title": "Cheatsheet", "title": "Cheatsheet",
"headlines": { "headlines": {
"description": "Description", "description": "Description",

View file

@ -15,7 +15,6 @@ import type { Icon } from 'react-bootstrap-icons'
export interface IconButtonProps extends ButtonProps, PropsWithDataTestId { export interface IconButtonProps extends ButtonProps, PropsWithDataTestId {
icon: Icon icon: Icon
onClick?: () => void
border?: boolean border?: boolean
iconSize?: number | string iconSize?: number | string
} }

View file

@ -8,8 +8,8 @@ import { cypressId } from '../../../utils/cypress-attribute'
import { testId } from '../../../utils/test-id' import { testId } from '../../../utils/test-id'
import { UiIcon } from '../icons/ui-icon' import { UiIcon } from '../icons/ui-icon'
import { ShowIf } from '../show-if/show-if' import { ShowIf } from '../show-if/show-if'
import type { PropsWithChildren } from 'react' import type { PropsWithChildren, ReactElement } from 'react'
import React, { useMemo } from 'react' import React, { Fragment, useMemo } from 'react'
import { Modal } from 'react-bootstrap' import { Modal } from 'react-bootstrap'
import type { Icon } from 'react-bootstrap-icons' import type { Icon } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
@ -26,6 +26,7 @@ export interface ModalContentProps {
titleIcon?: Icon titleIcon?: Icon
modalSize?: 'lg' | 'sm' | 'xl' modalSize?: 'lg' | 'sm' | 'xl'
additionalClasses?: string additionalClasses?: string
additionalTitleElement?: ReactElement
} }
export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & ModalContentProps export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & ModalContentProps
@ -42,6 +43,7 @@ export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & M
* @param modalSize The modal size * @param modalSize The modal size
* @param children The children to render into the modal. * @param children The children to render into the modal.
* @param titleIsI18nKey If the title is a i18n key and should be used as such * @param titleIsI18nKey If the title is a i18n key and should be used as such
* @param additionalTitleElement additional optional element that should be shown in the header
* @param props Additional props directly given to the modal * @param props Additional props directly given to the modal
*/ */
export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({ export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
@ -54,6 +56,7 @@ export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
additionalClasses, additionalClasses,
modalSize, modalSize,
children, children,
additionalTitleElement,
...props ...props
}) => { }) => {
useTranslation() useTranslation()
@ -77,6 +80,7 @@ export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
<UiIcon icon={titleIcon} nbsp={true} /> <UiIcon icon={titleIcon} nbsp={true} />
{titleElement} {titleElement}
</Modal.Title> </Modal.Title>
{additionalTitleElement ?? <Fragment />}
</Modal.Header> </Modal.Header>
{children} {children}
</Modal> </Modal>

View file

@ -6,10 +6,10 @@
import { useBooleanState } from '../../../../hooks/common/use-boolean-state' import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
import { CommonModal } from '../../../common/modals/common-modal' import { CommonModal } from '../../../common/modals/common-modal'
import { CheatsheetModalBody } from './cheatsheet-modal-body' import { CheatsheetContent } from './cheatsheet-content'
import { CheatsheetInNewTabButton } from './cheatsheet-in-new-tab-button'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap' import { Button, Modal } from 'react-bootstrap'
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
/** /**
@ -32,12 +32,18 @@ export const CheatsheetButton: React.FC = () => {
</Button> </Button>
<CommonModal <CommonModal
modalSize={'xl'} modalSize={'xl'}
titleIcon={IconQuestionCircle}
show={modalVisibility} show={modalVisibility}
onHide={closeModal} onHide={closeModal}
showCloseButton={true} showCloseButton={true}
titleI18nKey={'cheatsheet.modal.title'}> titleI18nKey={'cheatsheet.modal.title'}
<CheatsheetModalBody /> additionalTitleElement={
<div className={'d-flex flex-row-reverse w-100 mx-2'}>
<CheatsheetInNewTabButton onClick={closeModal} />
</div>
}>
<Modal.Body>
<CheatsheetContent />
</Modal.Body>
</CommonModal> </CommonModal>
</Fragment> </Fragment>
) )

View file

@ -10,13 +10,13 @@ import { CategoryAccordion } from './category-accordion'
import { CheatsheetEntryPane } from './cheatsheet-entry-pane' import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
import { TopicSelection } from './topic-selection' import { TopicSelection } from './topic-selection'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { Col, ListGroup, Modal, Row } from 'react-bootstrap' import { Col, ListGroup, Row } from 'react-bootstrap'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
/** /**
* Renders the tab content for the cheatsheet. * Renders the tab content for the cheatsheet.
*/ */
export const CheatsheetModalBody: React.FC = () => { export const CheatsheetContent: React.FC = () => {
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>() const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>() const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
@ -31,14 +31,9 @@ export const CheatsheetModalBody: React.FC = () => {
) )
return ( return (
<Modal.Body>
<Row className={`mt-2`}> <Row className={`mt-2`}>
<Col xs={3}> <Col xs={3}>
<CategoryAccordion <CategoryAccordion extensions={extensions} selectedEntry={selectedExtension} onStateChange={changeExtension} />
extensions={extensions}
selectedEntry={selectedExtension}
onStateChange={changeExtension}
/>
</Col> </Col>
<Col xs={9}> <Col xs={9}>
<ListGroup> <ListGroup>
@ -60,6 +55,5 @@ export const CheatsheetModalBody: React.FC = () => {
</ListGroup> </ListGroup>
</Col> </Col>
</Row> </Row>
</Modal.Body>
) )
} }

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IconButton } from '../../../common/icon-button/icon-button'
import type { MouseEvent } from 'react'
import React, { useCallback } from 'react'
import { BoxArrowUpRight } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export interface CheatsheetInNewTabButtonProps {
onClick?: () => void
}
/**
* Renders a button that opens the cheatsheet in a new tab.
*/
export const CheatsheetInNewTabButton: React.FC<CheatsheetInNewTabButtonProps> = ({ onClick }) => {
const openPopUp = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
window.open('/cheatsheet', '_blank', 'menubar=no,width=1000,height=600,location=no,toolbar=no')
onClick?.()
},
[onClick]
)
const { t } = useTranslation()
return (
<IconButton
size={'sm'}
iconSize={1.2}
href={'/cheatsheet'}
onClick={openPopUp}
icon={BoxArrowUpRight}
className={'p-2 border-0'}
variant={'outline-dark'}
target={'_blank'}
title={t('cheatsheet.modal.popup') ?? ''}
/>
)
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CheatsheetContent } from '../components/editor-page/app-bar/cheatsheet/cheatsheet-content'
import { useApplyDarkMode } from '../hooks/common/use-apply-dark-mode'
import type { NextPage } from 'next'
import { Container } from 'react-bootstrap'
const CheatsheetPage: NextPage = () => {
useApplyDarkMode()
return (
<Container>
<CheatsheetContent></CheatsheetContent>
</Container>
)
}
export default CheatsheetPage