diff --git a/.dockerignore b/.dockerignore index 9a97864a16..527d2f98e2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,4 @@ !/packages/bundle/dist !/packages/playground/build !/packages/testharness/dist +!/serve-test.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ec6287b901..756ad90b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Fixes [#1340](https://github.com/microsoft/BotFramework-WebChat/issues/1340). Card container should not be focusable if they do not have `tapAction`, by [@compulim](https://github.compulim) in PR [#3193](https://github.com/microsoft/BotFramework-WebChat/issues/3193) +- Fixed [#3196](https://github.com/microsoft/BotFramework-WebChat/issues/3196). Cards with `tapAction` should be executable by ENTER or SPACEBAR key, by [@compulim](https://github.com/compulim) in PR [#3197](https://github.com/microsoft/BotFramework-WebChat/pull/3197) ### Changed diff --git a/Dockerfile-testharness2 b/Dockerfile-testharness2 index 5a5d6e8cbf..bbd9b780f9 100644 --- a/Dockerfile-testharness2 +++ b/Dockerfile-testharness2 @@ -8,8 +8,9 @@ ENV PORT=80 EXPOSE 80 RUN npm install serve@11.3.0 -g WORKDIR /web/ -ENTRYPOINT ["npx", "--no-install", "serve", "-p", "80", "/web/__tests__/html/"] +ENTRYPOINT ["npx", "--no-install", "serve", "-c", "serve-test.json", "-p", "80", "/web/"] +ADD serve-test.json /web/ ADD __tests__/html/ /web/__tests__/html/ ADD packages/bundle/dist/webchat-es5.js /web/packages/bundle/dist/ ADD packages/testharness/dist/testharness.js /web/packages/testharness/dist/ diff --git a/__tests__/__image_snapshots__/html/adaptive-cards-tap-action-js-adaptive-cards-with-tap-action-prop-should-react-to-click-enter-and-spacebar-1-snap.png b/__tests__/__image_snapshots__/html/adaptive-cards-tap-action-js-adaptive-cards-with-tap-action-prop-should-react-to-click-enter-and-spacebar-1-snap.png new file mode 100644 index 0000000000..7e9d3e015e Binary files /dev/null and b/__tests__/__image_snapshots__/html/adaptive-cards-tap-action-js-adaptive-cards-with-tap-action-prop-should-react-to-click-enter-and-spacebar-1-snap.png differ diff --git a/__tests__/html/__jest__/WebChatEnvironment.js b/__tests__/html/__jest__/WebChatEnvironment.js index 125546917b..4f753831ae 100644 --- a/__tests__/html/__jest__/WebChatEnvironment.js +++ b/__tests__/html/__jest__/WebChatEnvironment.js @@ -4,7 +4,7 @@ const NodeEnvironment = require('jest-environment-node'); const { browserName } = require('../../constants.json'); const hostServe = require('./hostServe'); -const serveJSON = require('../serve.json'); +const serveJSON = require('../../../serve-test.json'); class WebChatEnvironment extends NodeEnvironment { constructor(config, context) { diff --git a/__tests__/html/adaptiveCards.tapAction.html b/__tests__/html/adaptiveCards.tapAction.html new file mode 100644 index 0000000000..1ebe2ad67f --- /dev/null +++ b/__tests__/html/adaptiveCards.tapAction.html @@ -0,0 +1,113 @@ + + +
+ + + + + + + + + diff --git a/__tests__/html/adaptiveCards.tapAction.js b/__tests__/html/adaptiveCards.tapAction.js new file mode 100644 index 0000000000..5b07edc033 --- /dev/null +++ b/__tests__/html/adaptiveCards.tapAction.js @@ -0,0 +1,8 @@ +/** + * @jest-environment ./__tests__/html/__jest__/WebChatEnvironment.js + */ + +describe('Adaptive Cards', () => { + test('with "tapAction" prop should react to click, ENTER, and SPACEBAR', () => + runHTMLTest('adaptiveCards.tapAction.html')); +}); diff --git a/__tests__/setup/NUnitTestReporter.js b/__tests__/setup/NUnitTestReporter.js index b86d71f51f..2f34d0779d 100644 --- a/__tests__/setup/NUnitTestReporter.js +++ b/__tests__/setup/NUnitTestReporter.js @@ -185,9 +185,11 @@ class NUnitTestReporter { { minStartTime: Infinity, maxEndTime: -Infinity } ); - xml['test-run']['@start-time'] = new Date(minStartTime).toISOString(); - xml['test-run']['@end-time'] = new Date(maxEndTime).toISOString(); - xml['test-run']['@duration'] = Math.max(0, maxEndTime - minStartTime) / 1000; + if (isFinite(minStartTime) && isFinite(maxEndTime)) { + xml['test-run']['@start-time'] = new Date(minStartTime).toISOString(); + xml['test-run']['@end-time'] = new Date(maxEndTime).toISOString(); + xml['test-run']['@duration'] = Math.max(0, maxEndTime - minStartTime) / 1000; + } xml['test-run']['test-suite'] = xml['test-run']['test-suite'].map(testSuite => { const { 'test-case': testCases } = testSuite; diff --git a/package.json b/package.json index ae1f627c14..f8d31b46ef 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "eslint": "lerna run --parallel --stream eslint", "posteslint": "npm run prettier-readmes", "prettier-readmes": "prettier --write **/**/*.md --tab-width 3 --single-quote true", - "start": "concurrently --kill-others --raw \"serve\" \"serve -p 5001 -c __tests__/html/serve.json __tests__/html\" \"lerna run --ignore playground --parallel --stream start\"", + "start": "concurrently --kill-others --raw \"serve\" \"serve -p 5001 -c serve-test.json\" \"lerna run --ignore playground --parallel --stream start\"", "tableflip": "npx lerna clean --yes --concurrency 8 && npx rimraf node_modules && npm ci && npm run bootstrap -- --concurrency 8", "test": "jest --watch", "test:ci": "npm run test -- --ci --coverage true --no-watch", diff --git a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.js b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.js index 716a38b122..d6246d1828 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.js +++ b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.js @@ -222,7 +222,7 @@ const AdaptiveCardRenderer = ({ actionPerformedClassName, adaptiveCard, disabled const [actionsPerformed, setActionsPerformed] = useState([]); const [adaptiveCardsHostConfig] = useAdaptiveCardsHostConfig(); const [disabledFromComposer] = useDisabled(); - const [error, setError] = useState(); + const [errors, setErrors] = useState([]); const [lastRender, setLastRender] = useState(0); const activeElementIndexRef = useRef(-1); const adaptiveCardElementRef = useRef(); @@ -235,18 +235,46 @@ const AdaptiveCardRenderer = ({ actionPerformedClassName, adaptiveCard, disabled const disabled = disabledFromComposer || disabledFromProps; - const handleClick = useCallback( - ({ target }) => { + // TODO: [P2] We should consider using `adaptiveCard.selectAction` instead. + // This callback will not listen to "click" or "keypress" event if the component is disabled or does not have "tapAction" prop. + const handleClickAndKeyPress = useCallback( + event => { + const { key, target, type } = event; + // Some items, e.g. tappable text, cannot be disabled thru DOM attributes - if (!disabled) { + const { current } = contentRef; + const adaptiveCardRoot = current.querySelector('.ac-adaptiveCard[tabindex="0"]'); + + if (!adaptiveCardRoot) { + return console.warn( + 'botframework-webchat: No Adaptive Card root container can be found, probably on an unsupported Adaptive Card version.' + ); + } + + // For "keypress" event, we only listen to ENTER and SPACEBAR key. + if (type === 'keypress') { + if (key !== 'Enter' && key !== ' ') { + return; + } + + event.preventDefault(); + } + + // We will call performCardAction if either: + // 1. We are on the target, or + // 2. The event-dispatching element is not interactive + if (target !== adaptiveCardRoot) { const tabIndex = getTabIndex(target); // If the user is clicking on something that is already clickable, do not allow them to click the card. // E.g. a hero card can be tappable, and image and buttons inside the hero card can also be tappable. - if (typeof tabIndex !== 'number' || tabIndex < 0) { - tapAction && performCardAction(tapAction); + if (typeof tabIndex === 'number' && tabIndex >= 0) { + return; } } + + performCardAction(tapAction); + scrollToEnd(); }, [disabled, performCardAction, tapAction] ); @@ -331,10 +359,9 @@ const AdaptiveCardRenderer = ({ actionPerformedClassName, adaptiveCard, disabled const { failures } = adaptiveCard.validateProperties(); if (failures.length) { - // TODO: [P3] Since this can be called from `componentDidUpdate` and potentially error, we should fix a better way to propagate the error. - const errors = failures.reduce((items, { errors }) => [...items, ...errors], []); - - return setError(errors); + return setErrors( + failures.reduce((items, { errors }) => [...items, ...errors.map(({ message }) => new Error(message))], []) + ); } let element; @@ -342,14 +369,15 @@ const AdaptiveCardRenderer = ({ actionPerformedClassName, adaptiveCard, disabled try { element = adaptiveCard.render(); } catch (error) { - return setError(error); + return setErrors([error]); } if (!element) { - return setError('Adaptive Card rendered as empty element'); + return setErrors([new Error('Adaptive Card rendered as empty element')]); } - error && setError(null); + // Clear errors on next render + setErrors([]); restoreInputValues(element, inputValuesRef.current); @@ -369,7 +397,7 @@ const AdaptiveCardRenderer = ({ actionPerformedClassName, adaptiveCard, disabled adaptiveCardElementRef.current = undefined; }; - }, [adaptiveCard, adaptiveCardsHostConfig, contentRef, error, HostConfig, renderMarkdownAsHTML]); + }, [adaptiveCard, adaptiveCardsHostConfig, contentRef, HostConfig, renderMarkdownAsHTML, setErrors]); useEffect(() => { // Set onExecuteAction without causing unnecessary re-render. @@ -404,14 +432,19 @@ const AdaptiveCardRenderer = ({ actionPerformedClassName, adaptiveCard, disabled return () => undoStack.forEach(undo => undo && undo()); }, [actionsPerformed, actionPerformedClassName, lastRender]); - return error ? ( -{JSON.stringify(error, null, 2)}+ const handleClickAndKeyPressForTapAction = !disabled && tapAction ? handleClickAndKeyPress : undefined; + + return errors.length ? ( +
{JSON.stringify({ error, message }, null, 2)}+ ))}