Skip to content

Commit d9de21f

Browse files
rllyy97Copilot
andauthored
fix(DesignerV2): show workflow note controls on hover (#9661)
* fix(designer): show workflow note controls on hover Reveal note color and delete controls on hover or keyboard focus, with a continuous hover target and browser regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(designer): keep note controls visible while color picker is open Track the popover open state so the portalled color picker keeps the note toolbar visible and interactive. Cover popup hover, focus, selection, and dismissal in the browser regression. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 328a254 commit d9de21f

4 files changed

Lines changed: 85 additions & 8 deletions

File tree

‎e2e/designer/notes.spec.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,60 @@ test.describe(
77
tag: '@mock',
88
},
99
async () => {
10+
test('Note controls appear on hover and keyboard focus', async ({ page }) => {
11+
await page.goto('/v2');
12+
await page.getByText('Local', { exact: true }).waitFor({ state: 'visible', timeout: 3 * 60 * 1000 });
13+
await GoToMockWorkflow(page, 'Notes In Metadata');
14+
15+
const note = page.locator('[data-testid="rf__node-validNote"]');
16+
const buttons = note.getByRole('button');
17+
const toolbar = buttons.first().locator('..');
18+
19+
await page.mouse.move(0, 0);
20+
await expect(buttons).toHaveCount(2);
21+
await expect(toolbar).toHaveCSS('opacity', '0');
22+
await expect(toolbar).toHaveCSS('pointer-events', 'none');
23+
24+
await note.locator('.note-drag-handle').hover();
25+
await expect(toolbar).toHaveCSS('opacity', '1');
26+
await expect(toolbar).toHaveCSS('pointer-events', 'auto');
27+
28+
const noteBounds = await note.boundingBox();
29+
const toolbarBounds = await toolbar.boundingBox();
30+
expect(noteBounds).not.toBeNull();
31+
expect(toolbarBounds).not.toBeNull();
32+
if (!noteBounds || !toolbarBounds) {
33+
throw new Error('Note and toolbar must have bounds.');
34+
}
35+
await page.mouse.move(toolbarBounds.x + toolbarBounds.width / 2, noteBounds.y + noteBounds.height - 1);
36+
await page.mouse.move(toolbarBounds.x + toolbarBounds.width / 2, toolbarBounds.y + 6, { steps: 5 });
37+
await expect(toolbar).toHaveCSS('opacity', '1');
38+
39+
await buttons.first().click();
40+
const swatches = page.locator('.fui-SwatchPicker').getByRole('radio');
41+
await expect(swatches).toHaveCount(6);
42+
await swatches.first().hover();
43+
await swatches.first().focus();
44+
await expect(swatches.first()).toBeFocused();
45+
await expect(toolbar).toHaveCSS('opacity', '1');
46+
await expect(toolbar).toHaveCSS('pointer-events', 'auto');
47+
await swatches.nth(1).click();
48+
await expect(note.locator('.note-drag-handle').locator('..')).toHaveCSS('background-color', 'rgb(204, 229, 255)');
49+
await expect(toolbar).toHaveCSS('opacity', '1');
50+
await page.keyboard.press('Escape');
51+
await buttons.first().blur();
52+
await page.mouse.move(0, 0);
53+
await expect(toolbar).toHaveCSS('opacity', '0');
54+
55+
await buttons.first().focus();
56+
await expect(toolbar).toHaveCSS('opacity', '1');
57+
await page.keyboard.press('Tab');
58+
await expect(buttons.last()).toBeFocused();
59+
await expect(toolbar).toHaveCSS('opacity', '1');
60+
await page.keyboard.press('Enter');
61+
await expect(note).toHaveCount(0);
62+
});
63+
1064
// Regression test for https://github.com/Azure/LogicAppsUX/issues/9466
1165
test('Malformed entries in definition.metadata.notes do not crash the designer', async ({ page }) => {
1266
const pageErrors: string[] = [];

‎libs/designer-v2/src/lib/ui/CustomNodes/NoteNode/NoteColorButton.tsx‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Button, Popover, PopoverTrigger, PopoverSurface, SwatchPicker, ColorSwatch, tokens } from '@fluentui/react-components';
2+
import type { PopoverProps } from '@fluentui/react-components';
23
import type { AppDispatch } from '../../../core';
34
import { useReadOnly } from '../../../core/state/designerOptions/designerOptionsSelectors';
45
import { updateNote } from '../../../core/state/notes/notesSlice';
@@ -9,7 +10,13 @@ import { bundleIcon, ColorFilled, ColorRegular } from '@fluentui/react-icons';
910

1011
const ColorIcon = bundleIcon(ColorFilled, ColorRegular);
1112

12-
const ColorButton = ({ id, selectedColor }: any) => {
13+
interface ColorButtonProps {
14+
id: string;
15+
selectedColor?: string;
16+
onOpenChange?: PopoverProps['onOpenChange'];
17+
}
18+
19+
const ColorButton = ({ id, selectedColor, onOpenChange }: ColorButtonProps) => {
1320
const dispatch = useDispatch<AppDispatch>();
1421
const isReadOnly = useReadOnly();
1522
const colorSelectCallback = useCallback(
@@ -32,7 +39,7 @@ const ColorButton = ({ id, selectedColor }: any) => {
3239
];
3340

3441
return (
35-
<Popover withArrow size="small" positioning="after">
42+
<Popover withArrow size="small" positioning="after" onOpenChange={onOpenChange}>
3643
<PopoverTrigger>
3744
<Button icon={<ColorIcon />} appearance="transparent" size="small" />
3845
</PopoverTrigger>

‎libs/designer-v2/src/lib/ui/CustomNodes/NoteNode/NoteNode.styles.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { makeStyles } from '@fluentui/react-components';
22

33
export const useNoteNodeStyles = makeStyles({
4+
noteNode: {
5+
position: 'relative',
6+
'&:hover .msla-note-toolbar, &:focus-within .msla-note-toolbar': {
7+
opacity: 1,
8+
pointerEvents: 'auto',
9+
},
10+
},
411
noteCard: {
512
position: 'relative',
613
boxShadow: '0 0 2px rgba(0,0,0,0.24), 0 2px 4px rgba(0,0,0,0.28)',
@@ -73,11 +80,19 @@ export const useNoteNodeStyles = makeStyles({
7380
},
7481
noteToolbar: {
7582
position: 'absolute',
76-
bottom: '-32px',
83+
top: '100%',
7784
left: '0',
85+
width: '100%',
7886
margin: '-2px',
87+
paddingTop: '12px',
7988
display: 'flex',
80-
justifyContent: 'flex-end',
89+
justifyContent: 'flex-start',
8190
gap: '4px',
91+
opacity: 0,
92+
pointerEvents: 'none',
93+
},
94+
noteToolbarVisible: {
95+
opacity: 1,
96+
pointerEvents: 'auto',
8297
},
8398
});

‎libs/designer-v2/src/lib/ui/CustomNodes/NoteNode/index.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const NoteNode = ({ id, dragging }: NodeProps) => {
2727
const noteData = useNote(id);
2828

2929
const [isEditing, setIsEditing] = useState(false);
30+
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
3031

3132
const deleteCallback = useCallback(() => {
3233
if (isReadOnly) {
@@ -63,7 +64,7 @@ const NoteNode = ({ id, dragging }: NodeProps) => {
6364
);
6465

6566
return (
66-
<>
67+
<div className={styles.noteNode}>
6768
<Card
6869
className={mergeClasses(styles.noteCard, dragging && styles.draggingNote)}
6970
style={{
@@ -95,12 +96,12 @@ const NoteNode = ({ id, dragging }: NodeProps) => {
9596
</Card>
9697
{/* Note Action Buttons */}
9798
{!isReadOnly && (
98-
<div className={styles.noteToolbar}>
99-
<ColorButton id={id} selectedColor={noteData?.color} />
99+
<div className={mergeClasses(styles.noteToolbar, isColorPickerOpen && styles.noteToolbarVisible, 'msla-note-toolbar')}>
100+
<ColorButton id={id} selectedColor={noteData?.color} onOpenChange={(_, data) => setIsColorPickerOpen(data.open)} />
100101
<Button icon={<DeleteIcon />} size="small" appearance="transparent" onClick={deleteCallback} />
101102
</div>
102103
)}
103-
</>
104+
</div>
104105
);
105106
};
106107

0 commit comments

Comments
 (0)