diff --git a/wax-prosemirror-services/src/MatchingService/MatchingOptionNodeView.js b/wax-prosemirror-services/src/MatchingService/MatchingOptionNodeView.js index 9d5d78d1d4afcad7cd453fc37870ffc42f0bdcd7..92b16cd5e93ea06309a40959f329f3e9543e7d6e 100644 --- a/wax-prosemirror-services/src/MatchingService/MatchingOptionNodeView.js +++ b/wax-prosemirror-services/src/MatchingService/MatchingOptionNodeView.js @@ -23,7 +23,7 @@ export default class MatchingOptionNodeView extends QuestionsNodeView { } stopEvent(event) { - if (event.target.type === 'text') { + if (event.target.type === 'text' || event.target.type === 'button') { return true; } const innerView = this.context.pmViews[this.node.attrs.id]; diff --git a/wax-prosemirror-services/src/MatchingService/components/MatchingOptionComponent.js b/wax-prosemirror-services/src/MatchingService/components/MatchingOptionComponent.js index b40c8f9305aac39a2f37a5f669c63a6345b285da..1695544ffb568bb4ed8bce1c968a4f4ea2944b45 100644 --- a/wax-prosemirror-services/src/MatchingService/components/MatchingOptionComponent.js +++ b/wax-prosemirror-services/src/MatchingService/components/MatchingOptionComponent.js @@ -150,7 +150,12 @@ export default ({ node, view, getPos }) => { )} {readOnly && testMode && !showFeedBack && ( - <TestModeDropDownComponent getPos={getPos} node={node} view={view} /> + <TestModeDropDownComponent + getPos={getPos} + node={node} + uniqueId={uuidv4()} + view={view} + /> )} {readOnly && showFeedBack && ( diff --git a/wax-prosemirror-services/src/MatchingService/components/TestModeDropDownComponent.js b/wax-prosemirror-services/src/MatchingService/components/TestModeDropDownComponent.js index 3d471faaf68d83a88861562d5e8aa32d6f7e90f0..c78edf4a3742e92bf06e5f172d5aaac31dc87c7b 100644 --- a/wax-prosemirror-services/src/MatchingService/components/TestModeDropDownComponent.js +++ b/wax-prosemirror-services/src/MatchingService/components/TestModeDropDownComponent.js @@ -1,57 +1,88 @@ /* eslint-disable no-unused-vars */ /* eslint-disable no-underscore-dangle */ -import React, { useContext, useMemo, useEffect, useState } from 'react'; +import React, { + useContext, + useMemo, + useEffect, + useState, + useRef, + createRef, +} from 'react'; import styled from 'styled-components'; import { find } from 'lodash'; import { WaxContext, DocumentHelpers, - ReactDropDownStyles, + useOnClickOutside, + Icon, } from 'wax-prosemirror-core'; -import Dropdown from 'react-dropdown'; import { v4 as uuidv4 } from 'uuid'; -const Wrapper = styled.div` - ${ReactDropDownStyles}; -`; -const DropdownStyled = styled(Dropdown)` - display: inline-flex; - cursor: not-allowed; - margin-left: auto; - opacity: ${props => (props.select ? 1 : 0.4)}; - pointer-events: ${props => (props.select ? 'default' : 'none')}; - .Dropdown-control { - border: none; - padding: 8px 30px 8px 10px; - - &:hover { - box-shadow: none; - } +const Wrapper = styled.div``; + +const DropDownButton = styled.button` + background: #fff; + border: none; + color: #000; + cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; + opacity: ${props => (props.disabled ? `0.4` : `1`)}; + display: flex; + position: relative; + 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: 10px auto auto; + position: absolute; + width: 170px; + max-height: 150px; + overflow-y: auto; + z-index: 2; + + span { + cursor: pointer; + padding: 8px 10px; } - .Dropdown-menu { - width: 102%; - display: flex; - flex-direction: column; - align-items: flex-start; - .Dropdown-option { - width: 100%; - } + span:focus { + background: #f2f9fc; + outline: 2px solid #f2f9fc; } `; -const TestModeDropDownComponent = ({ getPos, node, view }) => { +const StyledIcon = styled(Icon)` + height: 18px; + width: 18px; + margin-left: auto; +`; + +const TestModeDropDownComponent = ({ getPos, node, view, uniqueId }) => { const [selectedOption, setSelectedOption] = useState(undefined); + const itemRefs = useRef([]); + const wrapperRef = useRef(); + const [isOpen, setIsOpen] = useState(false); const context = useContext(WaxContext); const { pmViews: { main }, + activeView, } = context; + let isDisabled = false; + if (node.attrs.options.length === 0) isDisabled = true; + const onChange = option => { setSelectedOption(option); @@ -68,8 +99,12 @@ const TestModeDropDownComponent = ({ getPos, node, view }) => { ); } }); + openCloseMenu(); + setSelectedOption(option.value); }; + useOnClickOutside(wrapperRef, () => setIsOpen(false)); + useEffect(() => { const value = selectedOption ? selectedOption.value : ''; const found = find(node.attrs.options, { value }); @@ -79,23 +114,109 @@ const TestModeDropDownComponent = ({ getPos, node, view }) => { } }, [node.attrs.options]); - const ReadOnlyMultipleDropDown = useMemo( - () => ( - <Wrapper key={uuidv4()}> - <DropdownStyled - key={uuidv4()} - onChange={option => onChange(option)} - options={node.attrs.options} - placeholder="Select option" - select - value={ - selectedOption === 'undedfined' ? 'Select Option' : selectedOption - } - /> + const onKeyDown = (e, index) => { + e.preventDefault(); + if (e.keyCode === 40) { + // arrow down + 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 + ) { + 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); + } + }; + + useEffect(() => { + if (isDisabled) setIsOpen(false); + }, [isDisabled]); + + const openCloseMenu = () => { + if (!isDisabled) setIsOpen(!isOpen); + }; + + const ReadOnlyMultipleDropDown = useMemo(() => { + let selectedValue; + if (selectedOption) { + selectedValue = node.attrs.options.filter(option => { + return option.value === selectedOption; + }); + } + return ( + <Wrapper disabled={isDisabled} ref={wrapperRef}> + <DropDownButton + aria-controls={uniqueId} + aria-expanded={isOpen} + aria-haspopup + disabled={isDisabled} + onKeyDown={e => { + if (e.keyCode === 40) { + if (!itemRefs.current[0].current) return; + itemRefs.current[0].current.focus(); + } + if (e.keyCode === 27) { + setIsOpen(false); + } + if (e.keyCode === 13 || e.keyCode === 32) { + setIsOpen(true); + } + }} + onMouseDown={openCloseMenu} + role="combobox" + type="button" + > + {selectedOption === null || !selectedOption + ? 'Select Option' + : selectedValue[0].label} + <StyledIcon name="expand" /> + </DropDownButton> + <DropDownMenu + aria-label="Choose an option" + id={uniqueId} + isOpen={isOpen} + role="listbox" + > + {node.attrs.options.map((option, index) => { + itemRefs.current[index] = itemRefs.current[index] || createRef(); + return ( + <span + aria-selected={option.value === selectedOption} + key={option.value} + onClick={() => onChange(option)} + onKeyDown={e => onKeyDown(e, index)} + ref={itemRefs.current[index]} + role="option" + tabIndex="-1" + > + {option.label} + </span> + ); + })} + </DropDownMenu> </Wrapper> - ), - [node.attrs.options, selectedOption], - ); + ); + }, [node.attrs.options, selectedOption, isOpen]); return ReadOnlyMultipleDropDown; };