diff --git a/editors/demo/src/locale/en.js b/editors/demo/src/locale/en.js index a06aebce256b70daf1fbd7033ff9e65baf5e376d..44286df53fb7d0929cfccb8cea6df5b611396a13 100644 --- a/editors/demo/src/locale/en.js +++ b/editors/demo/src/locale/en.js @@ -46,7 +46,7 @@ const en = { 'Change to Subtitle': 'Switch to Subtitle', Subtitle: 'Subtitle', 'Change to Title': 'Switch to Title', - 'Title (H1)': 'Title (H1)', + 'Title (H1)': 'Title', 'Change to Block Quote': 'Switch to Block Quote', 'Block Quote': 'Block Quote', 'Change to Extract Poetry': 'Switch to Extract Poetry', diff --git a/wax-prosemirror-services/src/DisplayBlockLevel/TitleService/Title.js b/wax-prosemirror-services/src/DisplayBlockLevel/TitleService/Title.js index 1b95b3348b7917080f9884e17781b6471da81210..aed89db4c86984bf3ddc8f44b13cb5fa4b554b9d 100644 --- a/wax-prosemirror-services/src/DisplayBlockLevel/TitleService/Title.js +++ b/wax-prosemirror-services/src/DisplayBlockLevel/TitleService/Title.js @@ -8,7 +8,7 @@ import checkLevelFromConfig from '../HeadingService/checkLevelFromConfig'; @injectable() export default class Title extends Tools { title = 'Change to Title'; - label = 'Title (H1)'; + label = 'Title'; name = 'Title'; get run() { diff --git a/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDownComponent.js b/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDownComponent.js index 4f8e91ebe097420062acb16bda0d30a554c1c139..6c147847ecf9d30815b4fc4869e4888bb1bcc1e7 100644 --- a/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDownComponent.js +++ b/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDownComponent.js @@ -1,57 +1,87 @@ /* eslint-disable no-underscore-dangle */ -import React, { useContext, useMemo, useState, useEffect } from 'react'; +import React, { + useContext, + useMemo, + useState, + useEffect, + useRef, + createRef, +} from 'react'; import { isEmpty } from 'lodash'; import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; -import { WaxContext, ReactDropDownStyles } from 'wax-prosemirror-core'; -import Dropdown from 'react-dropdown'; -import { v4 as uuidv4 } from 'uuid'; +import { WaxContext, Icon, useOnClickOutside } from 'wax-prosemirror-core'; const Wrapper = styled.div` - ${ReactDropDownStyles}; + opacity: ${props => (props.disabled ? '0.4' : '1')}; + display: flex; `; -const DropdownStyled = styled(Dropdown)` - display: inline-flex; - cursor: not-allowed; - opacity: ${props => (props.select ? 1 : 0.4)}; - pointer-events: ${props => (props.select ? 'default' : 'none')}; - .Dropdown-control { - width: 170px; - border: none; - padding: 12px 26px 8px 10px; - &:hover { - box-shadow: none; - } + +const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +const DropDownButton = styled.button` + background: #fff; + border: none; + color: #000; + cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; + display: flex; + width: 160px; + + span { + position: relative; + top: 2px; } +`; - .Dropdown-arrow { - top: 17px; +const DropDownMenu = styled.div` + visibility: ${props => (props.isOpen ? 'visible' : 'hidden')}; + background: #fff; + display: flex; + flex-direction: column; + border: 1px solid #ddd; + border-radius: 0.25rem; + box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%); + margin: 38px auto auto; + position: absolute; + width: 160px; + max-height: 180px; + overflow-y: auto; + z-index: 2; + + option { + cursor: pointer; + padding: 8px 10px; } - .Dropdown-menu { - width: 102%; - display: flex; - flex-direction: column; - align-items: flex-start; - .Dropdown-option { - width: 100%; - } + option:focus, + option:hover { + background: #f2f9fc; + outline: 2px solid #f2f9fc; + } + option:disabled { + opacity: 0.3; + cursor: not-allowed; } `; -// eslint-disable-next-line react/prop-types +const StyledIcon = styled(Icon)` + height: 18px; + width: 18px; + margin-left: auto; +`; + const BlockDropDownComponent = ({ view, tools }) => { const { t, i18n } = useTranslation(); const context = useContext(WaxContext); const { + activeView, activeViewId, pmViews: { main }, } = context; - const [label, setLabel] = useState(null); - - const isEditable = main.props.editable(editable => { - return editable; - }); const translatedLabel = (translation, defaultLabel) => { return !isEmpty(i18n) && i18n.exists(translation) @@ -59,7 +89,14 @@ const BlockDropDownComponent = ({ view, tools }) => { : defaultLabel; }; + const [label, setLabel] = useState(null); + const dropDownOptions = [ + { + label: translatedLabel(`Wax.BlockLevel.Title (H1)`, 'Title'), + value: '0', + item: tools[0], + }, { label: translatedLabel(`Wax.BlockLevel.Heading 2`, 'Heading 2'), value: '5', @@ -82,6 +119,21 @@ const BlockDropDownComponent = ({ view, tools }) => { }, ]; + const itemRefs = useRef([]); + const wrapperRef = useRef(); + const [isOpen, setIsOpen] = useState(false); + const isEditable = main.props.editable(editable => { + return editable; + }); + + const isDisabled = !isEditable; + + useOnClickOutside(wrapperRef, () => setIsOpen(false)); + + useEffect(() => { + if (isDisabled) setIsOpen(false); + }, [isDisabled]); + useEffect(() => { setLabel(translatedLabel('Wax.BlockLevel.Block Level', 'Heading styles')); dropDownOptions.forEach(option => { @@ -101,25 +153,108 @@ const BlockDropDownComponent = ({ view, tools }) => { t('Wax.BlockLevel.Paragraph'), ]); + const openCloseMenu = () => { + if (!isDisabled) setIsOpen(!isOpen); + if (isOpen) + setTimeout(() => { + activeView.focus(); + }); + }; + + const onKeyDown = (e, index) => { + e.preventDefault(); + // arrow down + if (e.keyCode === 40) { + if (index === itemRefs.current.length - 1) { + itemRefs.current[0].current.focus(); + } else { + itemRefs.current[index + 1].current.focus(); + } + } + + // arrow up + if (e.keyCode === 38) { + if (index === 0) { + itemRefs.current[itemRefs.current.length - 1].current.focus(); + } else { + itemRefs.current[index - 1].current.focus(); + } + } + + // enter + if (e.keyCode === 13) { + itemRefs.current[index].current.click(); + } + + // ESC + if (e.keyCode === 27) { + setIsOpen(false); + } + }; + const MultipleDropDown = useMemo( () => ( - <Wrapper key={uuidv4()}> - <DropdownStyled - key={uuidv4()} - onChange={option => { - tools[option.value].run(main.state, main.dispatch); - }} - options={dropDownOptions} - placeholder={translatedLabel( - 'Wax.BlockLevel.Block Level', - 'Heading styles', - )} - select={isEditable} - value={label} - /> + <Wrapper disabled={isDisabled} ref={wrapperRef}> + <ButtonWrapper> + <DropDownButton + aria-controls="block-level-options" + aria-expanded={isOpen} + aria-haspopup + disabled={isDisabled} + onKeyDown={e => { + if (e.keyCode === 40) { + itemRefs.current[0].current.focus(); + } + if (e.keyCode === 27) { + setIsOpen(false); + } + if (e.keyCode === 13 || e.keyCode === 32) { + setIsOpen(true); + } + }} + onMouseDown={openCloseMenu} + type="button" + > + <span>{label}</span> + <StyledIcon name="expand" /> + </DropDownButton> + </ButtonWrapper> + <DropDownMenu + aria-label="Choose a block level action" + id="block-level-options" + isOpen={isOpen} + role="menu" + > + {dropDownOptions.map((option, index) => { + itemRefs.current[index] = itemRefs.current[index] || createRef(); + return ( + <option + disabled={ + !tools[option.value].select( + main.state, + activeViewId, + activeView, + ) + } + key={option.value} + onClick={() => { + tools[option.value].run(main.state, main.dispatch); + + openCloseMenu(); + }} + onKeyDown={e => onKeyDown(e, index)} + ref={itemRefs.current[index]} + role="menuitem" + tabIndex="-1" + > + {option.label} + </option> + ); + })} + </DropDownMenu> </Wrapper> ), - [label, isEditable, t('Wax.BlockLevel.Paragraph')], + [isDisabled, isOpen, label, t('Wax.BlockLevel.Paragraph')], ); return MultipleDropDown; diff --git a/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerContainerComponent.js b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerContainerComponent.js index 336863d49c60b6add2592ca84f8157ff86ae2839..87b3582d72ae1d95562bcdc52c5c48f34562d6e0 100644 --- a/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerContainerComponent.js +++ b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerContainerComponent.js @@ -1,6 +1,7 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { WaxContext, DocumentHelpers, Icon } from 'wax-prosemirror-core'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; +import { th } from '@pubsweet/ui-toolkit'; +import { WaxContext, DocumentHelpers, Icon } from 'wax-prosemirror-core'; import EditorComponent from '../../MultipleChoiceQuestionService/components/EditorComponent'; import FeedbackComponent from '../../MultipleChoiceQuestionService/components/FeedbackComponent'; import NumericalAnswerDropDownCompontent from './NumericalAnswerDropDownCompontent'; @@ -24,10 +25,6 @@ const NumericalAnswerContainerTool = styled.div` height: 33px; display: flex; flex-direction: row; - span:first-of-type { - position: relative; - top: 3px; - } `; const NumericalAnswerOption = styled.div` @@ -37,11 +34,41 @@ const NumericalAnswerOption = styled.div` const ActionButton = styled.button` background: transparent; cursor: pointer; - margin-top: 16px; border: none; - position: relative; margin-left: auto; - bottom: 13px; + z-index: 999; +`; + +const StyledIconContainer = styled.span` + float: right; + position: relative; + top: 3px; +`; + +const StyledIconAction = styled(Icon)` + position: relative; + right: 4px; + cursor: pointer; + height: 24px; + width: 24px; + z-index: 999; +`; + +const InfoMsg = styled.div` + color: #fff; + display: none; + user-select: none; + position: absolute; + width: 100%; + span { + background: ${th('colorPrimary')}; + bottom: 35px; + border-radius: 4px; + float: right; + right: 162px; + padding: 4px; + position: relative; + } `; const StyledIconActionRemove = styled(Icon)` @@ -60,6 +87,8 @@ export default ({ node, view, getPos }) => { const customProps = main.props.customValues; const { testMode, showFeedBack } = customProps; + const infoMsgRef = useRef(); + const [infoMsgIsOpen, setInfoMsgIsOpen] = useState(false); const isEditable = main.props.editable(editable => { return editable; @@ -88,6 +117,16 @@ export default ({ node, view, getPos }) => { }); }, []); + const displayInfoMsg = () => { + if (infoMsgRef.current && !infoMsgIsOpen) + infoMsgRef.current.style.display = 'block'; + + if (infoMsgRef.current && infoMsgIsOpen) + infoMsgRef.current.style.display = 'none'; + + setInfoMsgIsOpen(!infoMsgIsOpen); + }; + return ( <NumericalAnswerWrapper> <div> @@ -101,6 +140,20 @@ export default ({ node, view, getPos }) => { > <StyledIconActionRemove name="deleteOutlinedQuestion" /> </ActionButton> + {options[node.attrs.id]?.numericalAnswer === 'preciseAnswer' && ( + <StyledIconContainer + onClick={displayInfoMsg} + onKeyPress={() => {}} + role="button" + tabIndex={0} + > + <StyledIconAction name="help" /> + </StyledIconContainer> + )} + + <InfoMsg ref={infoMsgRef}> + <span>Separate answer variants with a semi colon</span> + </InfoMsg> </NumericalAnswerContainerTool> )} </div> diff --git a/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerDropDownCompontent.js b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerDropDownCompontent.js index 87c21f7f92515382c4c00f91096d5ac56cbcaddd..c80ea7c78ab649822ac4b7a6d9f3d48f7fefdbe3 100644 --- a/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerDropDownCompontent.js +++ b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerDropDownCompontent.js @@ -17,6 +17,7 @@ import { const Wrapper = styled.div` opacity: ${props => (props.disabled ? '0.4' : '1')}; + z-index: 999; `; const DropDownButton = styled.button` @@ -33,7 +34,7 @@ const DropDownButton = styled.button` span { position: relative; - top: 12px; + top: 4px; } `; diff --git a/wax-questions-service/src/NumericalAnswerService/components/PreciseAnswerComponent.js b/wax-questions-service/src/NumericalAnswerService/components/PreciseAnswerComponent.js index 3ebc0a4ab314d3f509e723d46bb31e19b774a80c..eca3b188a15ef2e6c45869807940d5ae167b50f5 100644 --- a/wax-questions-service/src/NumericalAnswerService/components/PreciseAnswerComponent.js +++ b/wax-questions-service/src/NumericalAnswerService/components/PreciseAnswerComponent.js @@ -144,10 +144,7 @@ const PreciseAnswerComponent = ({ node, readOnly, testMode, showFeedBack }) => { )} {readOnly && showFeedBack && ( <ResultContainer> - <span> - Accepted Answer Range:{' '} - {`(Accepted Answers : ${precise.replaceAll(';', ' -')})`} - </span> + <span>{`(Accepted Answers : ${precise.replaceAll(';', ' -')})`}</span> <span> Answer:{' '} <FinalResult isCorrect={isCorrect}>{preciseStudent}</FinalResult> diff --git a/wax-table-service/src/components/TableDropDown.js b/wax-table-service/src/components/TableDropDown.js index 9625d2373cc4b64b4242b157e3b86017584f2620..562bc75aabf0ac95e363cfc6d27349570480596b 100644 --- a/wax-table-service/src/components/TableDropDown.js +++ b/wax-table-service/src/components/TableDropDown.js @@ -15,6 +15,13 @@ import * as tablesFn from '../tableSrc'; const Wrapper = styled.div` opacity: ${props => (props.disabled ? '0.4' : '1')}; + display: flex; +`; + +const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; `; const DropDownButton = styled.button` @@ -23,7 +30,6 @@ const DropDownButton = styled.button` color: #000; cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; display: flex; - position: relative; width: 160px; span { @@ -40,7 +46,7 @@ const DropDownMenu = styled.div` border: 1px solid #ddd; border-radius: 0.25rem; box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%); - margin: 10px auto auto; + margin: 29px auto auto; position: absolute; width: 170px; max-height: 180px; @@ -250,32 +256,34 @@ const TableDropDown = ({ item }) => { const TableDropDownComponent = useMemo( () => ( <Wrapper disabled={isDisabled} ref={wrapperRef}> - <DropDownButton - aria-controls="table-options" - aria-expanded={isOpen} - aria-haspopup - disabled={isDisabled} - onKeyDown={e => { - if (e.keyCode === 40) { - itemRefs.current[0].current.focus(); - } - if (e.keyCode === 27) { - setIsOpen(false); - } - if (e.keyCode === 13 || e.keyCode === 32) { - setIsOpen(true); - } - }} - onMouseDown={openCloseMenu} - type="button" - > - <span> - {!isEmpty(i18n) && i18n.exists('Wax.Tables.Table Options') - ? t('Wax.Tables.Table Options') - : 'Table Options'} - </span> - <StyledIcon name="expand" /> - </DropDownButton> + <ButtonWrapper> + <DropDownButton + aria-controls="table-options" + aria-expanded={isOpen} + aria-haspopup + disabled={isDisabled} + onKeyDown={e => { + if (e.keyCode === 40) { + itemRefs.current[0].current.focus(); + } + if (e.keyCode === 27) { + setIsOpen(false); + } + if (e.keyCode === 13 || e.keyCode === 32) { + setIsOpen(true); + } + }} + onMouseDown={openCloseMenu} + type="button" + > + <span> + {!isEmpty(i18n) && i18n.exists('Wax.Tables.Table Options') + ? t('Wax.Tables.Table Options') + : 'Table Options'} + </span> + <StyledIcon name="expand" /> + </DropDownButton> + </ButtonWrapper> <DropDownMenu aria-label="Choose a table action" id="table-options"