diff --git a/Composer/packages/adaptive-form/src/components/fields/IntellisenseFields.tsx b/Composer/packages/adaptive-form/src/components/fields/IntellisenseFields.tsx index a05617ebd2..0bd58e4a33 100644 --- a/Composer/packages/adaptive-form/src/components/fields/IntellisenseFields.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/IntellisenseFields.tsx @@ -47,9 +47,11 @@ export const IntellisenseTextField: React.FC> = (props) => { onKeyDownTextField, onKeyUpTextField, onClickTextField, + aria, }) => ( > = (props) onKeyDownTextField, onKeyUpTextField, onClickTextField, + aria, }) => (
> = (props) => onBlur={props.onBlur} onChange={onChange} > - {({ textFieldValue, focused, onValueChanged, onKeyDownTextField, onKeyUpTextField, onClickTextField }) => ( + {({ textFieldValue, focused, onValueChanged, onKeyDownTextField, onKeyUpTextField, onClickTextField, aria }) => ( > = function StringField(pr focused, cursorPosition, hasIcon, + aria, } = props; const textFieldRef = React.createRef(); @@ -64,6 +65,7 @@ export const StringField: React.FC> = function StringField(pr <> = (newValue?: T) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface FieldProps { + aria?: Record; className?: string; definitions: SchemaDefinitions | undefined; description?: string; diff --git a/Composer/packages/intellisense/src/components/CompletionElement.tsx b/Composer/packages/intellisense/src/components/CompletionElement.tsx index 83330ba61b..91d4cef055 100644 --- a/Composer/packages/intellisense/src/components/CompletionElement.tsx +++ b/Composer/packages/intellisense/src/components/CompletionElement.tsx @@ -6,7 +6,7 @@ import { css, jsx } from '@emotion/core'; import { CompletionItemKind } from 'monaco-languageclient'; import { FontIcon } from '@fluentui/react/lib/Icon'; import { TooltipHost } from '@fluentui/react/lib/Tooltip'; -import React from 'react'; +import React, { useLayoutEffect, useRef } from 'react'; import { CompletionItem, MarkupContent } from 'vscode-languageserver-types'; import { DirectionalHint } from '@fluentui/react/lib/common/DirectionalHint'; @@ -78,17 +78,31 @@ const renderDocumentation = (documentation: string | MarkupContent | undefined) }; export const CompletionElement = (props: { + id: string; completionItem: CompletionItem; isSelected: boolean; onClickCompletionItem: () => void; }) => { - const { completionItem, isSelected, onClickCompletionItem } = props; - + const { completionItem, isSelected, onClickCompletionItem, id } = props; + const rootRef = useRef(null); const additionalStyles = isSelected ? styles.selectedElement : {}; + useLayoutEffect(() => { + if (isSelected) { + rootRef.current?.scrollIntoView?.(); + } + }, [isSelected]); + const renderItem = () => { return ( -
+
{completionItem.data?.matches diff --git a/Composer/packages/intellisense/src/components/CompletionList.tsx b/Composer/packages/intellisense/src/components/CompletionList.tsx index e2491f2145..4b18e0555e 100644 --- a/Composer/packages/intellisense/src/components/CompletionList.tsx +++ b/Composer/packages/intellisense/src/components/CompletionList.tsx @@ -27,18 +27,22 @@ export const CompletionList = React.forwardRef< HTMLDivElement, { completionItems: CompletionItem[]; + getItemId: (index: number) => string; selectedItem: number; completionListOverride?: JSX.Element | null; onClickCompletionItem: (index: number) => void; + 'aria-label': string; + id: string; } >((props, ref) => { - const { completionItems, selectedItem, completionListOverride, onClickCompletionItem } = props; + const { completionItems, selectedItem, completionListOverride, onClickCompletionItem, getItemId, ...rest } = props; return ( -
+
{completionListOverride ?? completionItems.map((completionItem, index) => ( & { + role: string; + 'aria-description': string; +}; export const Intellisense = React.memo( (props: { @@ -28,6 +36,7 @@ export const Intellisense = React.memo( onKeyDownTextField: (event: React.KeyboardEvent) => void; onKeyUpTextField: (event: React.KeyboardEvent) => void; onClickTextField: (event: React.MouseEvent) => void; + aria: AriaState; }) => JSX.Element; }) => { const { @@ -53,6 +62,34 @@ export const Intellisense = React.memo( const mainContainerRef = React.useRef(null); const completionListRef = React.useRef(null); + const itemIdPrefix = useId('suggestion-item'); + const suggestionsContainerId = useId('suggestion-items'); + const getItemId = useCallback((index: number) => `${itemIdPrefix}-${index}`, []); + const [aria, setAria] = useState(() => ({ + role: 'combobox', + 'aria-owns': suggestionsContainerId, + 'aria-autocomplete': 'list', + 'aria-expanded': 'false', + 'aria-activedescendant': '', + 'aria-description': formatMessage("Start typing to get suggestions or type '=' to write an expression"), + })); + + useEffect(() => { + if (showCompletionList && selectedCompletionItem >= 0) { + setAria({ + ...aria, + 'aria-expanded': 'true', + 'aria-activedescendant': getItemId(selectedCompletionItem), + }); + } else { + setAria({ + ...aria, + 'aria-expanded': 'false', + 'aria-activedescendant': '', + }); + } + }, [selectedCompletionItem, showCompletionList]); + const completionItems = useLanguageServer(url, scopes, id, textFieldValue, cursorPosition, projectId); const completionListOverride = completionListOverrideResolver !== undefined && focused ? completionListOverrideResolver(textFieldValue) : null; @@ -194,6 +231,18 @@ export const Intellisense = React.memo( setCursorPosition((event.target as HTMLInputElement).selectionStart || 0); }; + const resultsMessage = showCompletionList + ? formatMessage( + `{ + suggestionsCount, plural, + =1 {One suggestion found} + =0 {No suggestions found} + other {# suggestions found} + }`, + { suggestionsCount: completionListOverride ? 0 : completionItems.length } + ) + : ''; + return (
{children({ @@ -204,10 +253,14 @@ export const Intellisense = React.memo( onKeyDownTextField, onKeyUpTextField, onClickTextField, + aria, })} - + {completionListOverride || showCompletionList ? (