diff --git a/Composer/packages/client/src/components/EditableField.tsx b/Composer/packages/client/src/components/EditableField.tsx index 35cea51a6b..502674b21b 100644 --- a/Composer/packages/client/src/components/EditableField.tsx +++ b/Composer/packages/client/src/components/EditableField.tsx @@ -9,6 +9,7 @@ import { mergeStyleSets } from '@fluentui/style-utilities'; import { IconButton } from '@fluentui/react/lib/Button'; import { IIconProps } from '@fluentui/react/lib/Icon'; import { Announced } from '@fluentui/react/lib/Announced'; +import formatMessage from 'format-message'; import { FieldConfig, useForm } from '../hooks/useForm'; import { useAfterRender } from '../hooks/useAfterRender'; @@ -296,6 +297,7 @@ const EditableField: React.FC = (props) => { /> {enableIcon && ( = (props) => { return ( = ({ onClick={onCloseWebChat} /> - { const isPanelExpanded = useRecoilValue(debugPanelExpansionState); const activeTab = useRecoilValue(debugPanelActiveTabState); const pivotRef = useRef(null); + const contentId = useId('debug-panel-content'); useEffect(() => { if (isPanelExpanded) { @@ -170,15 +172,20 @@ export const DebugPanel: React.FC = () => { {headerPivot}
{ />
-
+
{debugExtensions.map((debugTabs) => { const { ContentWidget } = debugTabs; return ( diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticFilters.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticFilters.tsx index 9ac7829015..27298c3123 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticFilters.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticFilters.tsx @@ -149,6 +149,7 @@ export const DiagnosticsFilters: React.FC = (props) => }} > = (props) => { const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { return ( = ({ onRender: (item: BotStatus) => { return ( = ({ }, { key: 'SkillManifest', - name: '', + name: formatMessage('Manifest'), className: 'skillManifest', fieldName: 'skillManifestUrl', minWidth: 134, @@ -313,7 +314,7 @@ export const BotStatusList: React.FC = ({ }, { key: 'ShowPublishHistory', - name: '', + name: formatMessage('History'), className: 'showHistory', fieldName: 'showHistory', minWidth: 150, @@ -325,6 +326,8 @@ export const BotStatusList: React.FC = ({ onRender: (item: BotStatus) => { return ( onChangeShowHistoryBots(item)} diff --git a/Composer/packages/form-dialogs/src/components/common/FieldLabel.tsx b/Composer/packages/form-dialogs/src/components/common/FieldLabel.tsx index 7c0a36bd61..79261dcff9 100644 --- a/Composer/packages/form-dialogs/src/components/common/FieldLabel.tsx +++ b/Composer/packages/form-dialogs/src/components/common/FieldLabel.tsx @@ -20,7 +20,7 @@ export const FieldLabel = React.memo((props: Props) => { {props.defaultRender} {props.optional ? ({formatMessage('optional')}) : null} - + ); }); diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx index 1e7c50d18c..9db7e05d27 100644 --- a/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx +++ b/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx @@ -64,6 +64,12 @@ const InternalPropertyList = React.memo((props: Props) => ( export const PropertyList = (props: Props) => { const { kind } = props; + const helpText = + kind === 'required' + ? formatMessage( + 'Required properties are properties that your bot will ask the user to provide. The user must provide values for all required properties.' + ) + : formatMessage('Optional properties are properties the bot accepts if given but does not ask for.'); return ( @@ -71,16 +77,7 @@ export const PropertyList = (props: Props) => { {kind === 'required' ? formatMessage('Required properties') : formatMessage('Optional properties')} - + {(provided, { isDraggingOver }) => ( diff --git a/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx b/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx index 407b01676d..3c01e46b09 100644 --- a/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx +++ b/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx @@ -51,6 +51,7 @@ export const RequiredPriorityIndicator = React.memo((props: Props) => { help: ({ children }) => ( diff --git a/Composer/packages/lib/code-editor/src/components/HelpIconTooltip.tsx b/Composer/packages/lib/code-editor/src/components/HelpIconTooltip.tsx index ddab298b8e..16670e3b83 100644 --- a/Composer/packages/lib/code-editor/src/components/HelpIconTooltip.tsx +++ b/Composer/packages/lib/code-editor/src/components/HelpIconTooltip.tsx @@ -24,7 +24,7 @@ export const HelpIconTooltip = React.memo( }: { tooltipId: string; helpMessage: string | JSX.Element | JSX.Element[]; - tooltipProps?: Partial; + tooltipProps: HelpTooltipProps; }) => { return ( {text}; -export const ItemWithTooltip = React.memo(({ tooltipId, itemText, tooltipText: helpMessage, tooltipProps }: Props) => ( - - {typeof itemText === 'string' ? defaultRender(itemText) : itemText} - - -)); +export const ItemWithTooltip = React.memo( + ({ tooltipId, itemText, tooltipText: helpMessage, tooltipProps, ...props }: Props) => ( + + {typeof itemText === 'string' ? defaultRender(itemText) : itemText} + + + ) +); diff --git a/Composer/packages/lib/code-editor/src/lg/LgSpeechModalityToolbar.tsx b/Composer/packages/lib/code-editor/src/lg/LgSpeechModalityToolbar.tsx index 3d8621807d..1fb54961a3 100644 --- a/Composer/packages/lib/code-editor/src/lg/LgSpeechModalityToolbar.tsx +++ b/Composer/packages/lib/code-editor/src/lg/LgSpeechModalityToolbar.tsx @@ -36,6 +36,7 @@ export const LgSpeechModalityToolbar = React.memo((props: Props) => { const renderHeaderContent = React.useCallback( (itemProps: IContextualMenuItemProps, defaultRenders: IContextualMenuItemRenderFunctions) => ( { (itemProps: IContextualMenuItemProps, defaultRenders: IContextualMenuItemRenderFunctions) => itemProps.item.itemType === ContextualMenuItemType.Header ? ( visit this document.', { @@ -217,6 +218,7 @@ export const ModalityPivot = React.memo((props: Props) => { /> ) : ( { ))} {menuItems.filter((item) => item.itemType !== ContextualMenuItemType.Header).length && ( - null} /> + null} + /> )} diff --git a/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorContainer.tsx b/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorContainer.tsx index 4ea518e5f6..2e483ba461 100644 --- a/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorContainer.tsx +++ b/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorContainer.tsx @@ -159,6 +159,7 @@ export const ModalityEditorContainer: React.FC = ({ ): JSX.Element | null => itemProps?.itemType === DropdownMenuItemType.Header ? ( = ({ null} onRenderOverflowButton={onRenderOverflowButton} /> diff --git a/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorTitle.tsx b/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorTitle.tsx index a6c38e6a0b..ea4cca7549 100644 --- a/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorTitle.tsx +++ b/Composer/packages/lib/code-editor/src/lg/modalityEditors/ModalityEditorTitle.tsx @@ -11,13 +11,14 @@ import { ModalityType } from '../types'; const labelStyles = { root: { fontSize: FluentTheme.fonts.small.fontSize } }; type Props = { - title: string | JSX.Element | JSX.Element[]; + title: string; modalityType: ModalityType; helpMessage: string | JSX.Element | JSX.Element[]; }; export const ModalityEditorTitle = React.memo(({ title, modalityType, helpMessage }: Props) => ( {title}} tooltipId={`${modalityType}ModalityTitle`} tooltipText={helpMessage} diff --git a/Composer/packages/lib/code-editor/src/lg/modalityEditors/StringArrayItem.tsx b/Composer/packages/lib/code-editor/src/lg/modalityEditors/StringArrayItem.tsx index f2b63de15b..f698bf9b7a 100644 --- a/Composer/packages/lib/code-editor/src/lg/modalityEditors/StringArrayItem.tsx +++ b/Composer/packages/lib/code-editor/src/lg/modalityEditors/StringArrayItem.tsx @@ -165,7 +165,13 @@ const TextViewItem = React.memo( {onRenderDisplayText?.() ?? value.replace(/\r?\n/g, '↵')} - + ); } diff --git a/Composer/packages/lib/code-editor/src/lu/DefineEntityButton.tsx b/Composer/packages/lib/code-editor/src/lu/DefineEntityButton.tsx index ecffc8075a..57d57b6cfa 100644 --- a/Composer/packages/lib/code-editor/src/lu/DefineEntityButton.tsx +++ b/Composer/packages/lib/code-editor/src/lu/DefineEntityButton.tsx @@ -121,6 +121,7 @@ export const DefineEntityButton = React.memo((props: Props) => { const renderMenuItemHeader = React.useCallback( (itemProps: IContextualMenuItemProps, defaultRenders: IContextualMenuItemRenderFunctions) => ( this page to learn more about entity definition.', { diff --git a/Composer/packages/lib/ui-shared/src/components/HelpTooltip.tsx b/Composer/packages/lib/ui-shared/src/components/HelpTooltip.tsx index bd802de516..61b5f0cf49 100644 --- a/Composer/packages/lib/ui-shared/src/components/HelpTooltip.tsx +++ b/Composer/packages/lib/ui-shared/src/components/HelpTooltip.tsx @@ -46,6 +46,7 @@ export type HelpTooltipStyles = IStyleFunctionOrObject< >; export type HelpTooltipProps = Omit & { + 'aria-label': string; iconProps?: IIconProps & { 'data-testid'?: string }; styles?: HelpTooltipStyles; }; @@ -57,6 +58,7 @@ export const HelpTooltip: React.FC = ({ iconProps, ...props }) return ( { + splitButtonAriaLabel: string; +} + +const SplitButtonContainer = styled.div` + position: relative; + display: inline-flex; + + label: split-button-container; +`; + +const SplitButtonDivider = styled.span( + ({ primary, disabled, styles }: { primary?: boolean; disabled?: boolean; styles?: Record }) => [ + ` + position: absolute; + width: 1px; + right: 33px; + top: 8px; + bottom: 8px; + background-color: ${ + disabled + ? FluentTheme.semanticColors.disabledText + : primary + ? FluentTheme.palette.white + : FluentTheme.palette.neutralPrimary + }; + + label: split-button-divider; + `, + styles, + ] +); + +const menuButtonStyles = { + root: { + minWidth: '0', + width: '32px', + marginLeft: '-2px', + padding: '6px', + }, + label: { + display: 'none', + }, +}; + +/** + * Accessible analog of the Fluent split button component. + * + * Fluent team won't fix a11y issues of the split button in the current version + * as it introduces breaking changes. + * For more details see: https://github.com/microsoft/fluentui/issues/21904. + * + * Unlike Fluent split button this component consists of two buttons. This allows to operate with buttons + * independently and addresses issues with nested button controls not being accessible using screen readers. + */ +export const SplitButton: React.FC = ({ + primary, + menuAs, + menuIconProps, + menuProps, + menuTriggerKeyCode, + contextMenu, + onMenuClick, + onAfterMenuDismiss, + onContextMenu, + onRenderMenuIcon, + persistMenu, + onContextMenuCapture, + splitButtonAriaLabel, + disabled, + ...props +}) => { + const splitButtonStyles = useMemo( + () => + mergeStyleSets(menuButtonStyles, { + root: props.styles?.splitButtonMenuButton, + icon: props.styles?.splitButtonMenuIcon, + }), + [props.styles] + ); + const dividerStyles = props.styles?.splitButtonDivider as IRawStyle; + return ( + + + + + + ); +}; + +SplitButton.displayName = 'SplitButton'; diff --git a/Composer/packages/lib/ui-shared/src/components/index.ts b/Composer/packages/lib/ui-shared/src/components/index.ts index 5aab5a1079..5f793f61d2 100644 --- a/Composer/packages/lib/ui-shared/src/components/index.ts +++ b/Composer/packages/lib/ui-shared/src/components/index.ts @@ -15,3 +15,4 @@ export * from './CopyableText'; export * from './SectionTitle'; export * from './HelpTooltip'; export * from './Field'; +export * from './SplitButton'; diff --git a/extensions/packageManager/src/components/FeedModal.tsx b/extensions/packageManager/src/components/FeedModal.tsx index bed398ebe4..4282675f55 100644 --- a/extensions/packageManager/src/components/FeedModal.tsx +++ b/extensions/packageManager/src/components/FeedModal.tsx @@ -112,7 +112,7 @@ export const FeedModal: React.FC = (props) => { }, }, { - key: 'column1', + key: 'column2', name: 'Type', fieldName: 'text', minWidth: 75, @@ -123,6 +123,7 @@ export const FeedModal: React.FC = (props) => { if (!selectedItem || item.key !== selectedItem.key || !editRow) return ; return ( = (props) => { }, }, { - key: 'column2', + key: 'column3', name: 'URL', fieldName: 'url', minWidth: 200, @@ -154,7 +155,7 @@ export const FeedModal: React.FC = (props) => { }, }, { - key: 'column3', + key: 'column4', name: 'Filter', fieldName: 'defaultQuery.query', minWidth: 200, @@ -174,7 +175,7 @@ export const FeedModal: React.FC = (props) => { }, }, { - key: 'column4', + key: 'column5', name: 'Prerelease', fieldName: 'defaultQuery.prerelease', minWidth: 90, @@ -192,16 +193,20 @@ export const FeedModal: React.FC = (props) => { }, }, { - key: 'column5', - minWidth: 40, - maxWidth: 40, + key: 'column6', + name: 'Delete', + minWidth: 50, isResizable: false, - name: '', onRender: (item: PackageSourceFeed) => { if (item.key === selectedItem?.key) return ( - + ); }, diff --git a/extensions/packageManager/src/pages/Library.tsx b/extensions/packageManager/src/pages/Library.tsx index d507d1040a..c9150f6db0 100644 --- a/extensions/packageManager/src/pages/Library.tsx +++ b/extensions/packageManager/src/pages/Library.tsx @@ -3,11 +3,11 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; +import styled from '@emotion/styled'; import React, { useState, Fragment, useEffect } from 'react'; import formatMessage from 'format-message'; import { Link, - PrimaryButton, DefaultButton, Pivot, PivotItem, @@ -32,7 +32,7 @@ import { useTelemetryClient, TelemetryClient, } from '@bfc/extension-client'; -import { Toolbar, IToolbarItem, LoadingSpinner, DisplayMarkdownDialog } from '@bfc/ui-shared'; +import { Toolbar, IToolbarItem, LoadingSpinner, DisplayMarkdownDialog, SplitButton } from '@bfc/ui-shared'; import ReactMarkdown from 'react-markdown'; import { @@ -62,6 +62,23 @@ export interface PackageSourceFeed extends IDropdownOption { readonly?: boolean; } +const InstallButtonText = styled.span` + overflow: hidden; + display: inline-block; + + label: install-button-text; +`; + +const InstallButtonVersion = styled.span` + max-width: 80px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + + label: install-button-version; +`; + const Library: React.FC = () => { const [items, setItems] = useState([]); const { projectId, reloadProject, projectCollection: allProjectCollection, stopBot } = useProjectApi(); @@ -576,6 +593,8 @@ const Library: React.FC = () => { updateFeeds(response.data); }; + const InstallButton = versionOptions != undefined ? SplitButton : DefaultButton; + return ( { {selectedItem.authors} - {/* display "v1.0 installed" if installed, or "install v1.1" if not" */} {isInstalled(selectedItem) && selectedVersion === installedVersion(selectedItem) ? ( - - {selectedVersion} - -   - {strings.installed} + {selectedVersion}  + {strings.installed} ) : isUpdate ? ( - {strings.updateButton} - - {selectedVersion} - + {strings.updateButton}  + {selectedVersion} ) : ( - {strings.installButton}  - - {selectedVersion} - + {strings.installButton}  + {selectedVersion} )} - +