UNPKG

botframework-webchat-component

Version:
287 lines (256 loc) 9.05 kB
/* eslint react/no-array-index-key: "off" */ import { hooks } from 'botframework-webchat-api'; import BasicFilm, { createBasicStyleSet as createBasicStyleSetForReactFilm } from 'react-film'; import classNames from 'classnames'; import PropTypes from 'prop-types'; import React, { useMemo } from 'react'; import connectToWebChat from '../connectToWebChat'; import ScreenReaderText from '../ScreenReaderText'; import SuggestedAction from './SuggestedAction'; import useLocalizeAccessKey from '../hooks/internal/useLocalizeAccessKey'; import useNonce from '../hooks/internal/useNonce'; import useStyleSet from '../hooks/useStyleSet'; import useStyleToEmotionObject from '../hooks/internal/useStyleToEmotionObject'; import useSuggestedActionsAccessKey from '../hooks/internal/useSuggestedActionsAccessKey'; import useUniqueId from '../hooks/internal/useUniqueId'; const { useDirection, useLocalizer, useStyleOptions } = hooks; const ROOT_STYLE = { '&.webchat__suggested-actions': { '&.webchat__suggested-actions--flow-layout .webchat__suggested-actions__flow-box': { display: 'flex', flexWrap: 'wrap' }, '&.webchat__suggested-actions--stack-layout .webchat__suggested-actions__stack': { display: 'flex', flexDirection: 'column' } } }; function suggestedActionText({ displayText, title, type, value }) { if (type === 'messageBack') { return title || displayText; } else if (title) { return title; } else if (typeof value === 'string') { return value; } return JSON.stringify(value); } const connectSuggestedActions = (...selectors) => connectToWebChat( ({ language, suggestedActions }) => ({ language, suggestedActions }), ...selectors ); const SuggestedActionCarouselContainer = ({ children, className, screenReaderText }) => { const [ { suggestedActionsCarouselFlipperBoxWidth, suggestedActionsCarouselFlipperCursor, suggestedActionsCarouselFlipperSize } ] = useStyleOptions(); const [{ suggestedActions: suggestedActionsStyleSet }] = useStyleSet(); const [direction] = useDirection(); const [nonce] = useNonce(); const ariaLabelId = useUniqueId('webchat__suggested-actions'); const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; const filmStyleSet = useMemo( () => createBasicStyleSetForReactFilm({ autoHide: false, cursor: suggestedActionsCarouselFlipperCursor, flipperBoxWidth: suggestedActionsCarouselFlipperBoxWidth, flipperSize: suggestedActionsCarouselFlipperSize }), [ suggestedActionsCarouselFlipperBoxWidth, suggestedActionsCarouselFlipperCursor, suggestedActionsCarouselFlipperSize ] ); return ( // TODO: The content of suggested actions should be the labelled by the activity. // That means, when the user focus into the suggested actions, it should read similar to "Bot said, what's your preference of today? Suggested actions has items: apple button, orange button, banana button." <div aria-labelledby={ariaLabelId} aria-live="polite" className={classNames( 'webchat__suggested-actions', 'webchat__suggested-actions--carousel-layout', { 'webchat__suggested-actions--rtl': direction === 'rtl' }, rootClassName, suggestedActionsStyleSet + '', (className || '') + '' )} role="status" > <ScreenReaderText id={ariaLabelId} text={screenReaderText} /> {!!children && !!React.Children.count(children) && ( <BasicFilm autoCenter={false} className="webchat__suggested-actions__carousel" dir={direction} flipperBlurFocusOnClick={true} nonce={nonce} showDots={false} showScrollBar={false} styleSet={filmStyleSet} > {children} </BasicFilm> )} </div> ); }; SuggestedActionCarouselContainer.defaultProps = { children: undefined, className: undefined }; SuggestedActionCarouselContainer.propTypes = { children: PropTypes.any, className: PropTypes.string, screenReaderText: PropTypes.string.isRequired }; const SuggestedActionFlowContainer = ({ children, className, screenReaderText }) => { const [{ suggestedActions: suggestedActionsStyleSet }] = useStyleSet(); const ariaLabelId = useUniqueId('webchat__suggested-actions'); const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; return ( <div aria-labelledby={ariaLabelId} aria-live="polite" className={classNames( 'webchat__suggested-actions', 'webchat__suggested-actions--flow-layout', rootClassName, suggestedActionsStyleSet + '', (className || '') + '' )} role="status" > <ScreenReaderText id={ariaLabelId} text={screenReaderText} /> {!!children && !!React.Children.count(children) && ( <div className="webchat__suggested-actions__flow-box"> {React.Children.map(children, child => ( <div className="webchat__suggested-actions__item">{child}</div> ))} </div> )} </div> ); }; SuggestedActionFlowContainer.defaultProps = { children: undefined, className: undefined }; SuggestedActionFlowContainer.propTypes = { children: PropTypes.any, className: PropTypes.string, screenReaderText: PropTypes.string.isRequired }; const SuggestedActionStackedContainer = ({ children, className, screenReaderText }) => { const [{ suggestedActions: suggestedActionsStyleSet }] = useStyleSet(); const ariaLabelId = useUniqueId('webchat__suggested-actions'); const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; return ( <div aria-labelledby={ariaLabelId} aria-live="polite" className={classNames( 'webchat__suggested-actions', 'webchat__suggested-actions--stacked-layout', rootClassName, suggestedActionsStyleSet + '', (className || '') + '' )} role="status" > <ScreenReaderText id={ariaLabelId} text={screenReaderText} /> {!!children && !!React.Children.count(children) && ( <div className="webchat__suggested-actions__stack">{children}</div> )} </div> ); }; SuggestedActionStackedContainer.defaultProps = { children: undefined, className: undefined }; SuggestedActionStackedContainer.propTypes = { children: PropTypes.any, className: PropTypes.string, screenReaderText: PropTypes.string.isRequired }; const SuggestedActions = ({ className, suggestedActions = [] }) => { const [{ suggestedActionLayout, suggestedActionsStackedLayoutButtonTextWrap }] = useStyleOptions(); const [accessKey] = useSuggestedActionsAccessKey(); const localize = useLocalizer(); const localizeAccessKey = useLocalizeAccessKey(); const screenReaderText = localize( 'SUGGESTED_ACTIONS_ALT', suggestedActions.length ? accessKey ? localize('SUGGESTED_ACTIONS_ALT_HAS_CONTENT_AND_ACCESS_KEY', localizeAccessKey(accessKey)) : localize('SUGGESTED_ACTIONS_ALT_HAS_CONTENT') : localize('SUGGESTED_ACTIONS_ALT_NO_CONTENT') ); const children = suggestedActions.map(({ displayText, image, imageAltText, text, title, type, value }, index) => ( <SuggestedAction ariaHidden={true} buttonText={suggestedActionText({ displayText, title, type, value })} className="webchat__suggested-actions__button" displayText={displayText} image={image} imageAlt={imageAltText} key={index} text={text} textClassName={ suggestedActionLayout === 'stacked' && suggestedActionsStackedLayoutButtonTextWrap ? 'webchat__suggested-actions__button-text-stacked-text-wrap' : 'webchat__suggested-actions__button-text' } type={type} value={value} /> )); if (suggestedActionLayout === 'flow') { return ( <SuggestedActionFlowContainer className={className} screenReaderText={screenReaderText}> {children} </SuggestedActionFlowContainer> ); } else if (suggestedActionLayout === 'stacked') { return ( <SuggestedActionStackedContainer className={className} screenReaderText={screenReaderText}> {children} </SuggestedActionStackedContainer> ); } return ( <SuggestedActionCarouselContainer className={className} screenReaderText={screenReaderText}> {children} </SuggestedActionCarouselContainer> ); }; SuggestedActions.defaultProps = { className: '' }; SuggestedActions.propTypes = { className: PropTypes.string, suggestedActions: PropTypes.arrayOf( PropTypes.shape({ displayText: PropTypes.string, image: PropTypes.string, imageAltText: PropTypes.string, text: PropTypes.string, title: PropTypes.string, type: PropTypes.string.isRequired, value: PropTypes.any }) ).isRequired }; export default connectSuggestedActions()(SuggestedActions); export { connectSuggestedActions };