Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
dc26fe0
Base for content editable tags
Apr 22, 2024
f29e012
Fix contenteditable editting and errors and warnings
May 6, 2024
5a6d8a7
Fix selection from dropdown
May 6, 2024
04273de
Insert dropdown value into caret position
May 6, 2024
233c94c
Add field aria-label
May 7, 2024
e256010
Remove unused dependancy
May 7, 2024
71a4fdb
Work on picker item selection not yet done
May 7, 2024
9bab1c6
Proper selection when navigating in input
May 9, 2024
877c3fd
Refactor useCaretManipulation
May 9, 2024
f809361
Caret override and refactor
May 9, 2024
a3cf477
Still buggy
May 9, 2024
aaf8896
Merge branch 'master' into adamsamec/content-editable-tags
May 13, 2024
933164c
Some fixes
May 13, 2024
48d1e8c
wip
george-cz May 14, 2024
bc1980d
lexical
george-cz May 23, 2024
6cfc97e
fix
george-cz May 23, 2024
0cde4c1
split to 2 functions
george-cz May 24, 2024
210dd61
split to 2 files
george-cz May 24, 2024
fb6ed8f
improve a11y
george-cz May 24, 2024
39583da
move to activedescendant for autocomplete
george-cz May 27, 2024
4fe5ceb
allow activelement on rendered pills
george-cz May 27, 2024
5f9b8a7
fix onClick
george-cz May 27, 2024
57ee762
fix autoswitch mode
george-cz May 27, 2024
8c06377
repo cleanup
george-cz May 27, 2024
10d3f7d
delete/backspace
george-cz May 28, 2024
68ce95e
Merge pull request #1 from george-cz/jirivyhnalek/content-editable-tags
adamsamec May 28, 2024
e74faad
Merge remote-tracking branch 'upstream/master' into adamsamec/content…
May 28, 2024
1fe7add
Rename prototypes
May 28, 2024
9c2f442
Update yarn.lock
May 28, 2024
37b9a1c
Update yarn.lock
May 28, 2024
fdddee2
yarn.lock¨
May 28, 2024
128a4b6
Merge remote-tracking branch 'upstream/master' into adamsamec/content…
May 28, 2024
444d27d
Rename files and components
May 28, 2024
8e1f90c
Merge remote-tracking branch 'upstream/master' into adamsamec/content…
May 28, 2024
669a2cf
fix narration when going left/right
george-cz May 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{

@fabricteam fabricteam May 28, 2024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵 fluentuiv9 Open the Visual Regressions report to inspect the affected screenshots

Avatar Converged 1 screenshots
Image Name Diff(in Pixels) Image Type
Avatar Converged.badgeMask.normal.chromium.png 1 Changed
Card Converged - Selectable 4 screenshots
Image Name Diff(in Pixels) Image Type
Card Converged - Selectable.appearance selectable - Filled - Dark Mode.click.chromium.png 0 Removed
Card Converged - Selectable.appearance selectable - Filled - Dark Mode.hover.chromium.png 0 Removed
Card Converged - Selectable.appearance selectable - Filled - Dark Mode.normal.chromium.png 0 Removed
Card Converged - Selectable.appearance selectable - Filled - Dark Mode.selected.chromium.png 0 Removed
SwatchPicker Converged 2 screenshots
Image Name Diff(in Pixels) Image Type
SwatchPicker Converged.default - High Contrast.default.chromium.png 0 Added
SwatchPicker Converged.spacing.default.chromium.png 0 Added

"type": "patch",
"comment": "Picker prototype",
"packageName": "@fluentui/react-components",
"email": "asamec@microsoft.com",
"dependentChangeType": "patch"
}
4 changes: 3 additions & 1 deletion packages/react-components/react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@
"@fluentui/react-rating": "^9.0.10",

@fabricteam fabricteam May 28, 2024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵 fluentui-web-components-v3 Open the Visual Regressions report to inspect the affected screenshots

Accordion 3 screenshots
Image Name Diff(in Pixels) Image Type
Accordion.Size - Dark Mode.normal.chromium.png 2 Changed
Accordion.Size.normal.chromium.png 2 Changed
Accordion.Size - RTL.normal.chromium.png 2 Changed
Avatar 5 screenshots
Image Name Diff(in Pixels) Image Type
Avatar.Active Appearance - Dark Mode.normal.chromium.png 2 Changed
Avatar.Active Appearance.normal.chromium.png 10 Changed
Avatar.Active.normal.chromium.png 5 Changed
Avatar.Color.normal.chromium.png 1 Changed
Avatar.Color - RTL.normal.chromium.png 1 Changed
Badge 6 screenshots
Image Name Diff(in Pixels) Image Type
Badge.Appearance.normal.chromium.png 1 Changed
Badge.Default.normal.chromium.png 1 Changed
Badge.Shape.normal.chromium.png 1 Changed
Badge.Size.normal.chromium.png 6 Changed
Badge.Appearance - Dark Mode.normal.chromium.png 15 Changed
Badge.Color.normal.chromium.png 1 Changed
Button 15 screenshots
Image Name Diff(in Pixels) Image Type
Button.Large Icon Only.default.chromium.png 5 Changed
Button.Large Icon Only.hover.chromium.png 4 Changed
Button.Large With Icon - RTL.hover.chromium.png 4 Changed
Button.Large Icon Only - Dark Mode.pressed.chromium.png 5 Changed
Button.Large With Icon - Dark Mode.pressed.chromium.png 5 Changed
Button.Large With Icon - Dark Mode.default.chromium.png 4 Changed
Button.Large With Icon - RTL.pressed.chromium.png 3 Changed
Button.Large Icon Only - Dark Mode.hover.chromium.png 3 Changed
Button.Large With Icon - Dark Mode.hover.chromium.png 3 Changed
Button.Large With Icon.pressed.chromium.png 3 Changed
Button.Large Icon Only - Dark Mode.default.chromium.png 4 Changed
Button.Large With Icon.hover.chromium.png 4 Changed
Button.Large With Icon - RTL.default.chromium.png 5 Changed
Button.Large Icon Only.pressed.chromium.png 3 Changed
Button.Large With Icon.default.chromium.png 5 Changed
Divider 3 screenshots
Image Name Diff(in Pixels) Image Type
Divider.With Svg - RTL.normal.chromium.png 3 Changed
Divider.With Svg.normal.chromium.png 3 Changed
Divider.Vertical With Svg.normal.chromium.png 4 Changed
MenuList 16 screenshots
Image Name Diff(in Pixels) Image Type
MenuList.Checkbox With Icons - RTL.1st selected.chromium.png 4 Changed
MenuList.Checkbox - RTL.2nd selected.chromium.png 1 Changed
MenuList.Checkbox With Icons.normal.chromium.png 4 Changed
MenuList.Checkbox With Icons - RTL.2nd selected.chromium.png 5 Changed
MenuList.Checkbox With Icons - RTL.normal.chromium.png 4 Changed
MenuList.Radio With Icons.1st selected.chromium.png 4 Changed
MenuList.Checkbox With Icons.2nd selected.chromium.png 5 Changed
MenuList.With Icons.normal.chromium.png 4 Changed
MenuList.Radio With Icons.normal.chromium.png 4 Changed
MenuList.With Icons.hover menuitem.chromium.png 3 Changed
MenuList.Radio With Icons - RTL.1st selected.chromium.png 4 Changed
MenuList.Checkbox.2nd selected.chromium.png 1 Changed
MenuList.Radio With Icons.2nd selected.chromium.png 4 Changed
MenuList.Radio With Icons - RTL.normal.chromium.png 4 Changed
MenuList.Checkbox With Icons.1st selected.chromium.png 4 Changed
MenuList.Radio With Icons - RTL.2nd selected.chromium.png 4 Changed
Slider 2 screenshots
Image Name Diff(in Pixels) Image Type
Slider.Size Small.normal.chromium.png 1 Changed
Slider.Size Small.rightArrow.chromium.png 1 Changed
Switch 2 screenshots
Image Name Diff(in Pixels) Image Type
Switch.Checked - RTL.hover.chromium.png 1 Changed
Switch.Checked.hover.chromium.png 1 Changed
Text 1 screenshots
Image Name Diff(in Pixels) Image Type
Text.Block.normal.chromium.png 995 Changed
TextInput 1 screenshots
Image Name Diff(in Pixels) Image Type
TextInput.Size Large.normal.chromium.png 2 Changed

"@fluentui/react-search": "^9.0.6",
"@fluentui/react-teaching-popover": "^9.1.6",
"@fluentui/react-tag-picker": "^9.0.4"
"@fluentui/react-tag-picker": "^9.0.4",
"lexical": "^0.15.0",
"@lexical/react": "^0.15.0"
},
"peerDependencies": {
"@types/react": ">=16.14.0 <19.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { makeStyles, mergeClasses, shorthands } from '@fluentui/react-components';
import * as React from 'react';
import { people } from '../data';
import { AutocompletePluginCore } from './AutocompletePluginCore';

const useStyles = makeStyles({
root: {
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
...shorthands.gap('2px'),
maxWidth: '400px',
},
hidden: {
display: 'none',
},
item: {
cursor: 'pointer',
},
selected: {
fontWeight: 'bold',
},
});

const options = people;

const useFilteredList = (filter: string) => {
return React.useMemo(
() => (filter.length ? options.filter(option => option.toLowerCase().includes(filter.toLowerCase())) : []),
[filter],
);
};

export const AutocompletePlugin = ({ id }: { id: string }) => {
const [isOpen, setIsOpen] = React.useState(true);
const [query, setQuery] = React.useState('');
const [selectedIndex, setSelectedIndex] = React.useState(0);

const styles = useStyles();
const filtered = useFilteredList(query);

React.useEffect(() => {
setSelectedIndex(0);
setIsOpen(!!filtered.length);
}, [filtered]);

const selectedItem = React.useMemo(() => filtered[selectedIndex], [selectedIndex, filtered]);

React.useEffect(() => {}, [selectedItem]);

const onArrowKeyUp = React.useCallback(
event => {
if (isOpen) {
setSelectedIndex(currentIndex => {
return Math.max(0, currentIndex - 1);
});
event.preventDefault();
event.stopImmediatePropagation();
return true;
}
return false;
},
[isOpen, selectedIndex],
);

const onArrowKeyDown = React.useCallback(
event => {
if (isOpen) {
setSelectedIndex(currentIndex => {
return Math.min(currentIndex + 1, filtered.length - 1);
});
event.preventDefault();
event.stopImmediatePropagation();
return true;
}
return false;
},
[isOpen, selectedIndex],
);

return (
<AutocompletePluginCore
id={id}
isOpen={isOpen}
query={query}
onQueryChange={newQuery => setQuery(newQuery)}
autocompleteItem={isOpen ? selectedItem : undefined}
onArrowKeyUp={onArrowKeyUp}
onArrowKeyDown={onArrowKeyDown}
onEscape={() => {
if (isOpen) {
setIsOpen(false);
return true;
}
return false;
}}
>
{({ onClick, getItemId }) => {
return (
<div
className={mergeClasses(styles.root, !isOpen && styles.hidden)}
id={id}
role="listbox"
aria-expanded={isOpen}
>
{filtered.map((option, index) => (
<div
id={getItemId(id, option)}
aria-label={option}
role="option"
key={option}
className={mergeClasses(styles.item, index === selectedIndex && styles.selected)}
onMouseOver={() => {
setSelectedIndex(index);
}}
onClick={onClick}
>
{option}
</div>
))}
</div>
);
}}
</AutocompletePluginCore>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import * as React from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
$getSelection,
$isDecoratorNode,
$isNodeSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_EDITOR,
INSERT_PARAGRAPH_COMMAND,
KEY_ARROW_DOWN_COMMAND,
KEY_ARROW_LEFT_COMMAND,
KEY_ARROW_RIGHT_COMMAND,
KEY_ARROW_UP_COMMAND,
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
KEY_ENTER_COMMAND,
KEY_ESCAPE_COMMAND,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { $createNamePillNode } from './NamePillNode';

const getAutocompleteItemId = (autocompleteId: string, value: string) => {
return `${autocompleteId}_item-${value}`;
};

type AutocompletePluginCoreProps = {
id: string;
isOpen: boolean;
onQueryChange: (newQuery: string) => void;
children: (renderProps: { onClick: () => void; getItemId: typeof getAutocompleteItemId }) => React.ReactElement;
autocompleteItem?: string;
onArrowKeyUp?: (event: KeyboardEvent) => boolean;
onArrowKeyDown?: (event: KeyboardEvent) => boolean;
onEscape?: (event: KeyboardEvent) => boolean;
query?: string;
};

export const AutocompletePluginCore: React.FC<AutocompletePluginCoreProps> = ({
id,
isOpen,
onQueryChange,
autocompleteItem,
children,
onArrowKeyUp,
onArrowKeyDown,
onEscape,
query,
}) => {
const [editor] = useLexicalComposerContext();

const appendSelectedItem = React.useCallback(() => {
const sel = $getSelection();

// handle deletion of single selected node
if ($isNodeSelection(sel) && sel.getNodes().length === 1) {
const selectedNode = sel.getNodes()[0];
selectedNode.remove();
return true;
}

// handle adding of new node from selection
if ($isRangeSelection(sel) && sel.getNodes().length === 1) {
if (autocompleteItem) {
const node = sel.getNodes()[0];
const newNode = $createNamePillNode(autocompleteItem);
node.replace(newNode);
newNode.selectEnd();
return true;
}
}
return false;
}, [autocompleteItem]);

const onBackspace = React.useCallback(() => {
const sel = $getSelection();

if ($isNodeSelection(sel) && sel.getNodes().length === 1) {
const selectedNode = sel.getNodes()[0];
if ($isDecoratorNode(selectedNode)) {
// Have to move the selection at the end and let the native capslock handle the deletion
// otherwise 2 of the nodes will be deleted for some reason. This feels like a bug in lexical.
selectedNode.selectEnd();
}
}
return false;
}, []);

const onDelete = React.useCallback(() => {
const sel = $getSelection();

if ($isNodeSelection(sel) && sel.getNodes().length === 1) {
const selectedNode = sel.getNodes()[0];
if ($isDecoratorNode(selectedNode)) {
// Have to move the selection at the start and let the native delete handle the deletion
// otherwise 2 of the nodes will be deleted for some reason. This feels like a bug in lexical.
selectedNode.selectStart();
}
}

return false;
}, []);

React.useEffect(() => {
return editor.registerCommand(KEY_ENTER_COMMAND, appendSelectedItem, COMMAND_PRIORITY_CRITICAL);
});

React.useEffect(() => {
return editor.registerCommand(KEY_ESCAPE_COMMAND, onEscape, COMMAND_PRIORITY_CRITICAL);
});
React.useEffect(() => {
return editor.registerCommand(KEY_BACKSPACE_COMMAND, onBackspace, COMMAND_PRIORITY_CRITICAL);
});
React.useEffect(() => {
return editor.registerCommand(KEY_DELETE_COMMAND, onDelete, COMMAND_PRIORITY_CRITICAL);
});

// Update the activedescendant on the parent element to the currently selected item
React.useEffect(() => {
const editorElement = editor.getRootElement();
if (editorElement && autocompleteItem) {
editorElement.setAttribute('aria-activedescendant', getAutocompleteItemId(id, autocompleteItem));
}
}, [autocompleteItem, editor, id, query]);

React.useEffect(() => {
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
const sel = $getSelection();
if (!sel) {
return;
}

const editorElement = editor.getRootElement();

const nodes = sel.getNodes();
if ($isRangeSelection(sel) && nodes.length === 1) {
onQueryChange(nodes[0].getTextContent());
if (!isOpen) {
editorElement?.setAttribute('aria-activedescendant', '');
}
}

if ($isNodeSelection(sel)) {
const htmlElement = editor.getElementByKey(nodes[0].__key);

if (editorElement && htmlElement) {
editorElement.setAttribute('aria-activedescendant', htmlElement.id);
}
// when node is selected, clear query so that the autocomplete is not shown
onQueryChange('');
}
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor, isOpen, autocompleteItem, query, onQueryChange]);

React.useEffect(() => {
return editor.registerCommand(
INSERT_PARAGRAPH_COMMAND,
() => {
return true;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor]);

React.useEffect(() => {
return editor.registerCommand(
KEY_ARROW_LEFT_COMMAND,
() => {
editor.getRootElement()?.setAttribute('aria-activedescendant', '');
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor]);
React.useEffect(() => {
return editor.registerCommand(
KEY_ARROW_RIGHT_COMMAND,
() => {
editor.getRootElement()?.setAttribute('aria-activedescendant', '');
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor]);

React.useEffect(() => {
return editor.registerCommand(
KEY_ARROW_UP_COMMAND,
payload => {
return onArrowKeyUp?.(payload) ?? false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor, onArrowKeyUp]);

React.useEffect(() => {
return editor.registerCommand(
KEY_ARROW_DOWN_COMMAND,
payload => {
return onArrowKeyDown?.(payload) ?? false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor, onArrowKeyDown]);

const onClick = React.useCallback(() => {
editor.update(appendSelectedItem);
}, [appendSelectedItem, editor]);

return children({ onClick, getItemId: getAutocompleteItemId });
};
Loading