diff --git a/Composer/cypress/integration/Breadcrumb.spec.ts b/Composer/cypress/integration/Breadcrumb.spec.ts index 9f7d373374..a50f8967bd 100644 --- a/Composer/cypress/integration/Breadcrumb.spec.ts +++ b/Composer/cypress/integration/Breadcrumb.spec.ts @@ -15,10 +15,11 @@ context('breadcrumb', () => { function hasBreadcrumbItems(cy: Cypress.cy, items: (string | RegExp)[]) { cy.get('[data-testid="Breadcrumb"]') .last() - .get('li') - .should(($li) => { - items.forEach((item, idx) => { - expect($li.eq(idx)).to.contain(item); + .within(() => { + cy.get('li').should(($li) => { + items.forEach((item, idx) => { + expect($li.eq(idx)).to.contain(item); + }); }); }); } diff --git a/Composer/packages/client/__tests__/components/expandableNode.test.tsx b/Composer/packages/client/__tests__/components/expandableNode.test.tsx index 1fa7739519..6d9388b42d 100644 --- a/Composer/packages/client/__tests__/components/expandableNode.test.tsx +++ b/Composer/packages/client/__tests__/components/expandableNode.test.tsx @@ -14,16 +14,17 @@ describe('', () => { it('closes and opens on click', async () => { if (component == null) fail(); + const triangle = await component.findByTestId('summaryTag'); let details = await component.findByTestId('dialog'); - expect(details.attributes.getNamedItem('open')).not.toBeNull(); + expect(details.childNodes.length).toBe(2); // 1 for the summary itself, 1 for the details fireEvent.click(triangle); details = await component.findByTestId('dialog'); - expect(details.attributes.getNamedItem('open')).toBeNull(); + expect(details.childNodes.length).toBe(1); // when the node is closed, the details don't render at all fireEvent.click(triangle); details = await component.findByTestId('dialog'); - expect(details.attributes.getNamedItem('open')).not.toBeNull(); + expect(details.childNodes.length).toBe(2); }); }); diff --git a/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx b/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx index 2fd0b5d1b1..746b53a985 100644 --- a/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx @@ -6,8 +6,6 @@ import { jsx, css } from '@emotion/core'; import { useState, MouseEvent, KeyboardEvent } from 'react'; import { NeutralColors } from '@uifabric/fluent-theme'; -import { INDENT_PER_LEVEL } from './constants'; - type Props = { children: React.ReactNode; summary: React.ReactNode; @@ -18,29 +16,29 @@ type Props = { isActive?: boolean; }; -const summaryStyle = (depth: number, isActive: boolean, isOpen: boolean) => css` - label: summary; - padding-left: ${depth * INDENT_PER_LEVEL + 12}px; - padding-top: 6px; - display: list-item; +const listItemStyle = (isActive: boolean, isOpen: boolean) => css` + label: listItem; :hover { background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20}; } background: ${isActive ? NeutralColors.gray30 : NeutralColors.white}; ${isOpen - ? `list-style-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' height='10' width='10' viewBox='0 0 16 16'%3E%3Cpath style='fill:black;' d='M 0 8 H 16 L 8 16 L 0 8'/%3E%3C/svg%3E");` - : `list-style-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' height='10' width='10' viewBox='0 0 16 16'%3E%3Cpath style='fill:black;' d='M 8 0 V 16 L 16 8 L 8 0'/%3E%3C/svg%3E");`} + ? `list-style-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' height='16' width='16' viewBox='0 0 24 16'%3E%3Cpath style='fill:black%3B' d='M 8 8 h 16 l -8 8 l -8 -8'/%3E%3C/svg%3E");` + : `list-style-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' height='16' width='16' viewBox='0 0 24 16'%3E%3Cpath style='fill:black%3B' d='M 16 0 v 16 l 8 -8 l -8 -8'/%3E%3C/svg%3E");`} `; -const nodeStyle = css` - margin-top: 2px; +const listStyle = css` + label: list; + margin-top: 0; + margin-left: 16px; + padding-inline-start: 6px; + margin-block-end: 0px; `; export const ExpandableNode = ({ children, summary, detailsRef, - depth = 0, onToggle, defaultState = true, isActive = false, @@ -53,8 +51,10 @@ export const ExpandableNode = ({ } function handleClick(ev: MouseEvent) { - if ((ev.target as Element)?.tagName.toLowerCase() === 'summary') { + const target = ev.target as Element; + if (target.tagName.toLowerCase() === 'li') { setExpandedWithCallback(!isExpanded); + ev.stopPropagation(); } ev.preventDefault(); } @@ -64,25 +64,20 @@ export const ExpandableNode = ({ } return ( -
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-noninteractive-tabindex */} - +
  • {summary} -
  • -
    {children}
    -
    + + {isExpanded &&
    {children}
    } + ); }; diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx index 2f79907ee4..d49c58067a 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx @@ -19,8 +19,6 @@ import { TreeItem } from './treeItem'; import { ProjectTreeOptions, TreeLink } from './types'; const headerCSS = (label: string) => css` - margin-top: -6px; - width: 100%; label: ${label}; `; diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx index 774ce13829..704d8bc94e 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx @@ -30,7 +30,7 @@ import { getBaseName } from '../../utils/fileUtil'; import { TreeItem } from './treeItem'; import { ExpandableNode } from './ExpandableNode'; -import { INDENT_PER_LEVEL, LEVEL_PADDING, TREE_PADDING } from './constants'; +import { INDENT_PER_LEVEL, TREE_PADDING } from './constants'; import { ProjectTreeHeader, ProjectTreeHeaderMenuItem } from './ProjectTreeHeader'; import { isChildTriggerLinkSelected, doesLinkMatch } from './helpers'; import { ProjectHeader } from './ProjectHeader'; @@ -61,7 +61,6 @@ const tree = css` `; export const headerCSS = (label: string, isActive?: boolean) => css` - margin-top: -6px; width: 100%; label: ${label}; :hover { @@ -281,7 +280,6 @@ export const ProjectTree: React.FC = ({ link={dialogLink} menu={options.showMenu ? menu : options.showQnAMenu ? [QnAMenuItem] : []} menuOpenCallback={setMenuOpen} - padLeft={depth * LEVEL_PADDING} showErrors={false} textWidth={leftSplitWidth - TREE_PADDING} onSelect={handleOnSelect} @@ -292,7 +290,7 @@ export const ProjectTree: React.FC = ({ }; }; - const renderCommonDialogHeader = (skillId: string, depth: number) => { + const renderCommonDialogHeader = (skillId: string) => { const dialogLink: TreeLink = { dialogId: 'common', displayName: formatMessage('Common'), @@ -304,7 +302,7 @@ export const ProjectTree: React.FC = ({ }; return ( - + = ({ itemType={'dialog'} link={dialogLink} menuOpenCallback={setMenuOpen} - padLeft={depth * LEVEL_PADDING} showErrors={false} textWidth={leftSplitWidth - TREE_PADDING} onSelect={handleOnSelect} @@ -330,8 +327,7 @@ export const ProjectTree: React.FC = ({ }, dialog: DialogInfo, projectId: string, - dialogLink: TreeLink, - depth: number + dialogLink: TreeLink ): React.ReactNode => { const link: TreeLink = { projectId: rootProjectId, @@ -349,12 +345,11 @@ export const ProjectTree: React.FC = ({ = ({ return scope.toLowerCase().includes(filter.toLowerCase()); }; - const renderTriggerList = ( - triggers: ITrigger[], - dialog: DialogInfo, - projectId: string, - dialogLink: TreeLink, - depth: number - ) => { + const renderTriggerList = (triggers: ITrigger[], dialog: DialogInfo, projectId: string, dialogLink: TreeLink) => { return triggers .filter((tr) => filterMatch(dialog.displayName) || filterMatch(getTriggerName(tr))) .map((tr) => { @@ -406,8 +395,7 @@ export const ProjectTree: React.FC = ({ { ...tr, index, displayName: getTriggerName(tr), warningContent, errorContent }, dialog, projectId, - dialogLink, - depth + dialogLink ); }); }; @@ -464,7 +452,7 @@ export const ProjectTree: React.FC = ({ summary={renderTriggerGroupHeader(groupDisplayName, dialog, projectId)} onToggle={(newState) => setPageElement(key, newState)} > -
    {renderTriggerList(triggers, dialog, projectId, link, 1)}
    +
    {renderTriggerList(triggers, dialog, projectId, link)}
    ); }; @@ -485,7 +473,7 @@ export const ProjectTree: React.FC = ({ const renderDialogTriggers = (dialog: DialogInfo, projectId: string, startDepth: number, dialogLink: TreeLink) => { return dialogIsFormDialog(dialog) ? renderDialogTriggersByProperty(dialog, projectId, startDepth + 1) - : renderTriggerList(dialog.triggers, dialog, projectId, dialogLink, 1); + : renderTriggerList(dialog.triggers, dialog, projectId, dialogLink); }; // flatten lg imports url is same to dialog, to match correct link need render it as dialog @@ -634,7 +622,7 @@ export const ProjectTree: React.FC = ({ // eventually we will filter on topic trigger phrases const filteredTopics = filter == null || filter.length === 0 ? topics : topics.filter((topic) => filterMatch(topic.displayName)); - const commonLink = options.showCommonLinks ? [renderCommonDialogHeader(projectId, 1)] : []; + const commonLink = options.showCommonLinks ? [renderCommonDialogHeader(projectId)] : []; const importedLgLinks = options.showLgImports ? lgImportsList.map((file) => renderLgImportAsDialog(file, projectId)) diff --git a/Composer/packages/client/src/components/ProjectTree/TopicsList.tsx b/Composer/packages/client/src/components/ProjectTree/TopicsList.tsx index c2c4b26236..f050de5634 100644 --- a/Composer/packages/client/src/components/ProjectTree/TopicsList.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TopicsList.tsx @@ -10,7 +10,7 @@ import get from 'lodash/get'; import { ExpandableNode } from './ExpandableNode'; import { TreeItem } from './treeItem'; -import { LEVEL_PADDING, INDENT_PER_LEVEL } from './constants'; +import { INDENT_PER_LEVEL } from './constants'; import { headerCSS } from './ProjectTree'; type TopicsListProps = { @@ -69,8 +69,10 @@ export const TopicsList: React.FC = ({ topics, onToggle, textWi link={{ displayName: formatMessage('Power Virtual Agents Topics ({count})', { count: topics.length }), projectId, + isRoot: false, + isRemote: false, + diagnostics: [], }} - padLeft={0 * LEVEL_PADDING} showErrors={false} textWidth={textWidth} /> diff --git a/Composer/packages/client/src/components/ProjectTree/constants.ts b/Composer/packages/client/src/components/ProjectTree/constants.ts index 64133fa6c2..77028671af 100644 --- a/Composer/packages/client/src/components/ProjectTree/constants.ts +++ b/Composer/packages/client/src/components/ProjectTree/constants.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export const SUMMARY_ARROW_SPACE = 28; // the rough pixel size of the dropdown arrow to the left of a Details/Summary element export const INDENT_PER_LEVEL = 16; export const ACTION_ICON_WIDTH = 28; export const THREE_DOTS_ICON_WIDTH = 28; diff --git a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx index 2a2d7051fd..d4a575a8c2 100644 --- a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx +++ b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx @@ -21,13 +21,13 @@ import { DiagnosticSeverity, Diagnostic, Icons } from '@bfc/shared'; import isEmpty from 'lodash/isEmpty'; import uniqueId from 'lodash/uniqueId'; -import { SUMMARY_ARROW_SPACE, THREE_DOTS_ICON_WIDTH } from './constants'; +import { THREE_DOTS_ICON_WIDTH } from './constants'; import { TreeLink, TreeMenuItem } from './types'; import { TreeItemContent } from './TreeItemContent'; // -------------------- Styles -------------------- // -const projectTreeItemContainer = css` +const projectTreeItemContainer = (extraSpace: number) => css` outline: none; :focus { outline: rgb(102, 102, 102) solid 1px; @@ -39,6 +39,8 @@ const projectTreeItemContainer = css` text-align: left; cursor: pointer; + padding-left: ${extraSpace}px; + label: ProjectTreeItemContainer; `; @@ -94,16 +96,13 @@ const navContainer = ( isActive: boolean, menuOpenHere: boolean, textWidth: number, - isBroken: boolean, - padLeft: number, - marginLeft: number + isBroken: boolean ) => css` ${isAnyMenuOpen ? '' : ` &:hover { - background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20}; - + background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20}; .dialog-more-btn { visibility: visible; } @@ -125,17 +124,11 @@ const navContainer = ( label: navItem; - height: 24px; font-size: 12px; - padding-left: ${padLeft}px; - margin-left: ${marginLeft}px; - min-width: calc(100% - ${padLeft + 24}px); + min-width: 100%; opacity: ${isBroken ? 0.5 : 1}; align-items: center; - position: relative; - top: -4px; - :hover { background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20}; } @@ -285,7 +278,6 @@ type ITreeItemProps = { dialogName?: string; textWidth?: number; extraSpace?: number; - padLeft?: number; marginLeft?: number; hasChildren?: boolean; menu?: TreeMenuItem[]; @@ -293,6 +285,8 @@ type ITreeItemProps = { isMenuOpen?: boolean; showErrors?: boolean; role?: string; + href?: string; + tooltip?: string; }; const renderTreeMenuItem = (link: TreeLink) => (item: TreeMenuItem) => { @@ -424,11 +418,7 @@ export const TreeItem: React.FC = ({ dialogName, onSelect, textWidth = 100, - hasChildren = false, menu = [], - extraSpace = 0, - padLeft = 0, - marginLeft = 0, menuOpenCallback = () => {}, isMenuOpen = false, showErrors = true, @@ -444,7 +434,6 @@ export const TreeItem: React.FC = ({ const linkString = `${link.projectId}_DialogTreeItem${link.dialogId}_${link.trigger ?? ''}`; const isBroken = !!link.botError; - const spacerWidth = hasChildren && !isBroken ? 0 : SUMMARY_ARROW_SPACE + extraSpace; const overflowIconWidthOnHover = overflowMenu.length > 0 ? THREE_DOTS_ICON_WIDTH : 0; @@ -517,7 +506,7 @@ export const TreeItem: React.FC = ({ ); }, - [textWidth, spacerWidth, extraSpace, overflowIconWidthActiveOrChildSelected, showErrors] + [textWidth, overflowIconWidthActiveOrChildSelected, showErrors] ); const onRenderOverflowButton = useCallback( @@ -574,15 +563,7 @@ export const TreeItem: React.FC = ({ return (
    = ({ : undefined } > -
    = ({ ]} overflowItems={overflowMenu} styles={{ item: { flex: 1 } }} - onRenderItem={onRenderItem( - textWidth - spacerWidth + extraSpace - overflowIconWidthActiveOrChildSelected, - showErrors - )} + onRenderItem={onRenderItem(textWidth, showErrors)} onRenderOverflowButton={onRenderOverflowButton( !!isActive, isChildSelected,