From df2690679d1218c18078c26abf78641675adf00c Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:04:46 +0530 Subject: [PATCH] Spec update to support @mention in rich text components (#1594) --- ...-6c68cffe-b8b5-4e15-9c84-0d429391e1e3.json | 7 + .../src/rich-text/specs/README.md | 66 +- .../src/rich-text/specs/mention-hld.md | 733 ++++++++++++++++++ .../specs/spec-images/mention-state.png | Bin 0 -> 6908 bytes 4 files changed, 785 insertions(+), 21 deletions(-) create mode 100644 change/@ni-nimble-components-6c68cffe-b8b5-4e15-9c84-0d429391e1e3.json create mode 100644 packages/nimble-components/src/rich-text/specs/mention-hld.md create mode 100644 packages/nimble-components/src/rich-text/specs/spec-images/mention-state.png diff --git a/change/@ni-nimble-components-6c68cffe-b8b5-4e15-9c84-0d429391e1e3.json b/change/@ni-nimble-components-6c68cffe-b8b5-4e15-9c84-0d429391e1e3.json new file mode 100644 index 0000000000..e06eb90822 --- /dev/null +++ b/change/@ni-nimble-components-6c68cffe-b8b5-4e15-9c84-0d429391e1e3.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Spec update to support @mention in rich text components", + "packageName": "@ni/nimble-components", + "email": "123377523+vivinkrishna-ni@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/nimble-components/src/rich-text/specs/README.md b/packages/nimble-components/src/rich-text/specs/README.md index 6dc77beb0a..3bc7327e27 100644 --- a/packages/nimble-components/src/rich-text/specs/README.md +++ b/packages/nimble-components/src/rich-text/specs/README.md @@ -13,13 +13,17 @@ The Comment Feature, designed for adding and viewing comments with rich text con Therefore, it is essential to develop web components that allow users to view and create rich text content, enabling their use in various scenarios, including Comments and other instances that necessitate rich text capabilities. -[Nimble issue #1288](https://github.com/ni/nimble/issues/1288) +_Rich Text Components_: -Comments UI mockups - [Desktop view](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=6280-94045&mode=design&t=aC5VQw42BYcOesm2-0) and [Mobile view](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=7258-115224&mode=design&t=aC5VQw42BYcOesm2-0) +1. [Nimble issue #1288](https://github.com/ni/nimble/issues/1288) +2. [Rich Text Editor FE Library Decision](https://dev.azure.com/ni/DevCentral/_git/Skyline?path=/docs/design-documents/Platform/Comments/Comments-FE-Library-Decision.md&version=GBmaster&_a=preview) -[Comments Feature](https://dev.azure.com/ni/DevCentral/_backlogs/backlog/ASW%20SystemLink%20Platform/Initiatives/?workitem=2205215) +_Comments Feature in SLE_: -[Rich Text Editor FE Library Decision](https://dev.azure.com/ni/DevCentral/_git/Skyline?path=/docs/design-documents/Platform/Comments/Comments-FE-Library-Decision.md&version=GBmaster&_a=preview) +1. [Comments Feature](https://dev.azure.com/ni/DevCentral/_backlogs/backlog/ASW%20SystemLink%20Platform/Initiatives/?workitem=2205215) +2. Comments UI mockups - [Desktop view](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=6280-94045&mode=design&t=aC5VQw42BYcOesm2-0) and [Mobile view](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=7258-115224&mode=design&t=aC5VQw42BYcOesm2-0) +3. [Comments `@mention` mockup](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink-orig?type=design&node-id=7248-114254&mode=design&t=y3JtM3aT77Aw0xjK-0) +4. [Mention users in comments - Requirement doc](https://dev.azure.com/ni/DevCentral/_git/Skyline?path=/docs/design-documents/Platform/Requirements/Mention-users-in-comments.md&_a=preview) ### Non-goals @@ -36,12 +40,12 @@ Both components provide support for the following basic text formatting options: 4. Bulleted List 5. Absolute URL links 1. Supports only [absolute URI as per common mark spec](https://spec.commonmark.org/0.30/#absolute-uri) with valid [schemes](https://spec.commonmark.org/0.30/#scheme). - 2. In the initial release, we will provide support for `HTTP` and `HTTPS` schemes only. Depending on future requirements, we may extend support to include other schemes as well. To summarize: - 1. Supports `` as a valid markdown syntax for an absolute link - 2. Any other syntaxes like `https://www.ni.com` and `[NI Homepage](https://www.ni.com)` will not be supported for initial release + 2. In the initial release, we will provide support for `HTTP` and `HTTPS` schemes only but in markdown will preserve as an + [autolink](https://spec.commonmark.org/0.30/#autolink). Depending on future requirements, we may extend support to include other schemes as well to render as links. To summarize: 1. Supports `` as a valid markdown syntax for an absolute link 2. Any other syntaxes like `https://www.ni.com` and `[NI Homepage](https://www.ni.com)` will not be supported for initial release 3. [Tiptap's link extension](https://tiptap.dev/api/marks/link) provides various configurations to [add/remove HTML attributes](https://tiptap.dev/api/marks/link#removing-and-overriding-existing-html-attributes) for links, [validate](https://tiptap.dev/api/marks/link#validate) URLs entered or pasted into the editor and more. +6. Allow the user to tag or mention someone by entering **"@"** in the editor and selecting the user's name from the drop-down list. The `nimble-rich-text-editor` component will also offer APIs and interactive methods to format text in the following ways: @@ -54,7 +58,6 @@ The `nimble-rich-text-viewer` provides support for converting the input markdown #### _Additional features out of scope of this spec_ -- Allowing the user to tag or mention by entering `@` in the editor and selecting the user name from the drop-down list. - Support for adding images to the editor either by uploading or by pasting it. - Support for adding hyperlinks to the existing text in the editor. This allows users to add links to existing text in the editor. When the link button in the formatting options is clicked, a dialog opens, providing a space to enter the hyperlink for the selected text. @@ -107,6 +110,8 @@ Example usage of the `nimble-rich-text-editor` in the application layer is as fo ``` +[Design for `@mention` in Rich Text Editor](./mention-hld.md#nimble-rich-text-editor) + ### API _Props/Attrs_ @@ -200,6 +205,8 @@ _CSS Classes and CSS Custom Properties that affect the component_ _Note_: This initial component design serves as a starting point for implementation, and it may undergo changes once the visual design is completed. +[Other APIs for `@mention` in Rich Text Editor](./mention-hld.md#api) + ### Anatomy _Shadow DOM template_ @@ -215,6 +222,11 @@ _Shadow DOM template_ + + + + + ``` @@ -241,11 +253,15 @@ _CSS Parts_ - none +[Shadow DOM Template for API Components](./mention-hld.md#anatomy) + ### `nimble-rich-text-viewer` The `nimble-rich-text-viewer` is used for viewing rich text content when a markdown string is passed to it. It performs the post-processing tasks to convert the markdown string to corresponding HTML nodes for each text formatting. +[Design for `@mention` in Rich Text Viewer](./mention-hld.md#nimble-rich-text-viewer) + ### API _Props/Attrs_ @@ -254,10 +270,6 @@ _Props/Attrs_ [prosemirror-markdown parser](https://github.com/ProseMirror/prosemirror-markdown/blob/9049cd1ec20540d70352f8a3e8736fb0d1f9ce1b/src/from_markdown.ts#L199). The parsed node will then be rendered in the viewer component as rich text. -_Methods_ - -- none - _Events_ - none @@ -270,6 +282,8 @@ _CSS Classes and CSS Custom Properties that affect the component_ - The width of the component will be determined by the client. Reducing the width will cause the content to reflow, resulting in an increased height of the component or will enable the vertical scrollbar. +[APIs for `@mention` in Rich Text Viewer](./mention-hld.md#api-1) + ### Anatomy _Slot Names_ @@ -288,6 +302,8 @@ _CSS Parts_ - none +[Shadow DOM Template for API Components](./mention-hld.md#anatomy-1) + ### Angular integration An Angular directive will be created for both components. Input and accessor APIs will be created for the attributes and properties and output event @@ -323,7 +339,7 @@ document. Some of the highlighted points are mentioned below: 4. Includes support to render the content of the editor with markdown input. 5. Includes support to retrieve the content of the editor as HTML and Markdown output (using [prosemirror-markdown](https://github.com/ProseMirror/prosemirror-markdown)). -6. Includes extensions to support [@mention](https://tiptap.dev/api/nodes/mention) functionality (Future scope). +6. Includes extensions to support [@mention](https://tiptap.dev/api/nodes/mention) functionality. The `nimble-rich-text-editor` is initialized by creating an instance of the [Editor](https://tiptap.dev/api/editor#introduction) class from Tiptap's core library. With that, we have access to all the APIs exposed, by utilizing some of their extensions like @@ -345,26 +361,27 @@ markdown based on [CommonMark](http://commonmark.org/) flavor: - Bulleted list - `* Bulleted list` - Absolute URL links - `` (For more details on the markdown syntax for absolute URL links, see [Autolinks in CommonMark](https://spec.commonmark.org/0.30/#autolink)) - Hard line break - a backslash before the line ending `line1\\nline2` (For more details on the markdown syntax for Hard line breaks, see [Hard line breaks in CommanMark](https://spec.commonmark.org/0.30/#hard-line-breaks)) +- `@mention` - `` that follows [Autolinks in CommonMark](https://spec.commonmark.org/0.30/#autolink). For more details, see + [Markdown Format for `@mention`](./mention-hld.md#2-markdown-format) -_Configurations on Tiptap to support only absolute links_: +### _Implementation details for supporting absolute link:_ + +#### _Configurations on Tiptap to support only absolute links_: Install the [link extension](https://tiptap.dev/api/marks/link) mark from Tiptap and initialize the `Links` with the following configurations: -1. Set regular expression in [validate](https://tiptap.dev/api/marks/link#validate) field to support only `HTTP` and `HTTPS` absolute links in the editor. -2. Set [openOnClick](https://tiptap.dev/api/marks/link#open-on-click) to `false` for the editor, to restrict the user from opening a link from the editor by clicking. Users can open the link only by +1. Set [openOnClick](https://tiptap.dev/api/marks/link#open-on-click) to `false` for the editor, to restrict the user from opening a link from the editor by clicking. Users can open the link only by `Right-click >> Open link in new tab` from the editor. -3. Set [autoLink](https://tiptap.dev/api/marks/link#autolink) to `true`, to add the valid link automatically when typing. -4. Set [linkOnPaste](https://tiptap.dev/api/marks/link#link-on-paste) to `false` which will attach the URL to current selected text in the editor, converting it into a hyperlink. If it is `true`, +2. Set [autoLink](https://tiptap.dev/api/marks/link#autolink) to `true`, to add the valid link automatically when typing. +3. Set [linkOnPaste](https://tiptap.dev/api/marks/link#link-on-paste) to `false` which will attach the URL to current selected text in the editor, converting it into a hyperlink. If it is `true`, pasting a link to the selection will add the link behind the word which is not supported for the initial pass. The `nimble-rich-text-viewer` will be responsible for converting the input markdown string to HTML Fragments with the help of `prosemirror-markdown` parser, which is then converted to HTML string and rendered into the component to view all rich text content. -_Implementation details for supporting absolute link:_ - For the `nimble-rich-text-viewer` component, we will set up the `link` mark in the Prosemirror schema as below, allowing links in the component to open with default behavior (same tab). -Only links with schema HTTP and HTTPS will be treated as links within `nimble-rich-text-editor` and `nimble-rich-text-viewer` and rest of the links with any other schemas will be rendered as plain text. Note that APIs `setMarkdown()` of editor and `markdown` of viewer will have necessary validation mechanisms to ensure this schema restriction. +Only links with schema HTTP and HTTPS will be rendered as links within `nimble-rich-text-editor` and `nimble-rich-text-viewer` and rest of the links with any other schemas will be rendered as links but with no `href` attribute. Note that APIs `setMarkdown()` of editor and `markdown` of viewer will have necessary validation mechanisms to ensure this scheme restriction in rendering the link in editor and viewer. Here is the default [link configuration](https://github.com/ProseMirror/prosemirror-markdown/blob/b7c1fd2fb74c7564bfe5428c7c8141ded7ebdd9f/src/schema.ts#L138C5-L148C6) from the `prosemirror-markdown` package for comparison with the newly updated configuration. @@ -397,6 +414,8 @@ We have observed that issues such as [#1412](https://github.com/ni/nimble/issues opening anchor elements in a new tab. Once the visual design is finalized and integrated the implementation into the `nimble-anchor`, we will update the above `link` mark schema, in case we are required to update any attributes for the `nimble-anchor` component. +[Implementation Details For `@mention` Support](./mention-hld.md#implementation) + ### Prototype [`Tiptap prototype in Stackblitz`](https://stackblitz.com/edit/typescript-g3yfmo?file=index.ts) @@ -425,6 +444,8 @@ text in the editor. ![Bold and Numbered Button State](./spec-images/button-state.png) +[_`@mention` State_](./mention-hld.md#states) + ### Accessibility [Tiptap Accessibility](https://tiptap.dev/guide/accessibility) @@ -473,6 +494,8 @@ _Keyboard navigation with toolbar buttons focused_ _Note_: All other keyboard interaction determined by the slotted element will not be defined in this document. +[Accessibility For `@mention` Feature](./mention-hld.md#accessibility) + ### Globalization Localization: The `nimble-rich-text-editor` will use the `bold`, `italics`, `numberedList`, and `bulletedList` labels from `nimble-label-provider-rich-text` for the icons displayed in @@ -514,6 +537,7 @@ library. For the currently supported features, we will include the following lib - [prosemirror-markdown](https://www.npmjs.com/package/prosemirror-markdown) - [prosemirror-model](https://www.npmjs.com/package/prosemirror-model) - [@tiptap/extension-link](https://www.npmjs.com/package/@tiptap/extension-link) +- [@tiptap/extension-mention](https://www.npmjs.com/package/@tiptap/extension-mention) These packages will add up to a total space of approximately 900 KB in the components bundle. For more info see [this discussion on Teams](https://teams.microsoft.com/l/message/19:b6a61b8a7ffd451696e0cbbb8976c03b@thread.skype/1686833093592?tenantId=87ba1f9a-44cd-43a6-b008-6fdb45a5204e&groupId=41626d4a-3f1f-49e2-abdc-f590be4a329d&parentMessageId=1686833093592&teamName=ASW%20SystemLink&channelName=LIMS&createdTime=1686833093592). diff --git a/packages/nimble-components/src/rich-text/specs/mention-hld.md b/packages/nimble-components/src/rich-text/specs/mention-hld.md new file mode 100644 index 0000000000..ea2ce62a6e --- /dev/null +++ b/packages/nimble-components/src/rich-text/specs/mention-hld.md @@ -0,0 +1,733 @@ +# Rich Text Component Mention Support HLD + +## Problem Statement + +There is a requirement to `@mention` a user in `nimble-rich-text-editor` and notify them through an email in the comments feature. Once the user is +mentioned in the editor, it should be loaded back to the viewer with the special formatting text in the `nimble-rich-text-viewer`. + +### Out of Scope of this Spec + +1. Notifying the mentioned users through an email in the comments feature is part of the backend services +2. Adding a "Load More" or "Search Server" button to dynamically load more data in the `@mention` dropdown + in the editor. +3. Show a tooltip having more details of the user like email, and profile information on clicking the mentioned users in the viewer. + +## Links To Relevant Work Items and Reference Material + +- [Rich Text Editor and Viewer README](./README.md) +- [Comments `@mention` mockup](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink-orig?type=design&node-id=7248-114254&mode=design&t=y3JtM3aT77Aw0xjK-0) +- [Mention users in comments - Requirement doc](https://dev.azure.com/ni/DevCentral/_git/Skyline?path=/docs/design-documents/Platform/Requirements/Mention-users-in-comments.md&_a=preview) +- [Work Item for @mention in comments in AzDo](https://dev.azure.com/ni/DevCentral/_workitems/edit/2464517) + +## Design + +### `nimble-rich-text-editor` + +The rich text editor includes this feature for tagging or mentioning a user by inserting the **"@"** symbol within the editor. +This action triggers a dropdown list, allowing users to select a user. To provide the rich text editor component with the +necessary user information for populating this dropdown, clients can pass the user details through the configuration elements outlined below: + +```HTML + + + + + + + +``` + +The configuration element, `nimble-rich-text-mention-users`, consists of mapping elements that specify both the content to display +in the dropdown list (i.e., the `display-name`) and the data to store in the markdown when extracting content from the editor (i.e., the `mention-href`). +These details are subsequently transformed into a map or an object, which is used to populate the options within the shadow root +for the dropdown list of items. This component uses the [`pattern`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern) +attribute to acquire the regular expression pattern that corresponds to the URL of users within the specific domain of the client application. + +The `nimble-mapping-mention-user` element contains individual user information within its elements. Typically, the `mention-href` attribute +includes either the user ID or a value stored in the backend database. It's important that the `mention-href` adheres to a valid +[Autolink](https://spec.commonmark.org/0.30/#autolink) format, often used to link the user ID with the hosting domain. In cases where there's +no specific Href for each user within the domain, they can alternatively insert the user ID in a valid +[Autolink](https://spec.commonmark.org/0.30/#autolink) format in CommonMark flavor such as `user:user-id` and provide the appropriate +pattern for it. The Href provided in the `mention-href` is not restricted to `HTTPS/HTTP` links as they are intended to map a unique +value with the `display-name`. + +#### Future Scope + +1. The `nimble-mapping-mention-user` can be used to get other user details like email, profile information, etc., to display in the + dropdown list options. +2. If there is a requirement to mention an issue (using **"#"**) or a pull request (using **"!"**), + a new configuration component can be created and added as a child to the rich text editor. Below is an example of how + these elements would be used: + + ````HTML + + + + + + + + + + + + + + ``` + ```` + +3. As the Href are stored as absolute links within the markdown string, mentions of issues or systems, as described above, + initially appear as links. However, once we introduce support for mentioning issues or systems by matching the specified + pattern in the configuration element, even the absolute links will transform into `mention` nodes. + +#### Alternatives + +Slot-based approach: All user information will be communicated through `nimble-list-option` elements. These elements will +contain the user's ID in their `value` attribute and the user's name in their text content. When the **"@"** symbol is +inserted into the editor, these elements will be presented as a dropdown list. + +Pros: + +1. When the client does not specify a `value` attribute, the options are sent without a user's ID, and in such cases, + the value attribute is derived from the content of the option. + +Cons: + +1. It is impossible to have multiple types of mentions when `nimble-list-option` is used in the default slot. If we use + slot names to differentiate between various types of mentions, the rich text editor needs to be aware of all the type + of mentions like user slot name, issue slot name, etc, through an attribute value. +2. The slot-based approach is not configurable, meaning that if a user wishes to customize the entire user mention with + some arbitrary value, the only viable option is to include it within the options. In contrast, the configuration-based + approach readily allows for the inclusion of arbitrary values in the parent container element, making customization more + straightforward. For example, in the future if we are configuring the `character` to trigger the user mention from the client, + it is simple to add it like below, + ```HTML + + + + + + ``` + If it is a slot-based approach and without a configuration element, it is required to provide the information to every option as we don't know that it is a user-mention + or other mention + ```HTML + + + + ``` +3. The UI of the list option is completely dependent on the client application. +4. All the keyboard and mouse interactions within the dropdown list should be handled internally within the editor as `nimble-list-option` + does not support any. + +#### Client Usage Guidance on Filtered Users: + +Initially, the client application need not provide any user mapping elements within the children of `nimble-rich-text-mention-users`. +It can be empty. However, if the editor loads with an initial markdown string that contains user mentions, then the client should load those +user mapping elements. This is to map the user IDs in the markdown string converted to a readable user name in the editor. Therefore, +it is advisable to keep two lists, `filteredOptions` for dynamic filtering of users to populate in the `@mention` popup and `mentionedUsers` +for storing the already mentioned users which should be loaded initially by the client to represent the user names that are stored in markdown. +Later, the `mentionedUsers` will be filtered internally within the Nimble editor or by the client to show the names in the popup whenever texts +added after `@` in the editor. + +The `nimble-rich-text-mention-users` component will emit an event whenever the `@` character is entered into the editor. The client can listen to +the `mention-update` event and provide the other initial user lists that are not mentioned already. + +It is recommended to `sort` the usernames in alphabetical order and send at most 20 users list at a time to reduce the number of elements +in the DOM for a single page. This dynamic loading of the user's list will help specifically in the comments feature when there are a lot of `@mention` +happens on a single page and if the users list is huge, it may cause the page to load slower as the mapping mention element is overflowed +in the DOM when there are multiple editors in a single page or a huge number of users in the client organization. + +The `mention-update` event will also be triggered when the user types any character after `@`, containing that text along with the `@` character +in the event data. For example, if a user types `@` and then adds `a` the event will be emitted with data that includes the value `@a`. +The client can listen to this event, filter the list of users that includes the names containing the letter `a`, and then dynamically +update the `nimble-mapping-mention-user` element based on the filter data. Subsequently, a maximum of 20 filtered options should be +transmitted to the editor. + +The `mention-update` event will also be triggered when a user copies a mention node from the viewer/editor and pasting it in the editor, +client should read the `getMentionedHrefs()` method to identify the user URL(s) are being pasted into the editor so that the mapping +elements can be sent as a children. This is necessary because the corresponding name for the newly added user URL(s) is required to +render as username(s) in the components so the client should send the mapping elements for all mentioned users. + +Since the above event triggers for every key down event like adding/removing texts, move the text cursors after the `@` character which is quite an +expensive operation to perform for every keystroke so it is advisable to `debounce` the events if you're using network requests to perform the +filtering operations. Like, allow at most one request per second to filter the list for each second instead of for each keystrokes. + +_Note_: The editor will also perform the same filtering once again to ensure the filtered options are proper and update the dropdown list in the UI. +This helps to filter the list, regardless of whether the client is loading the list dynamically by listening to the event as mentioned above +or statically providing user details at the start via `nimble-mapping-mention`. + +### API + +To support the `@mention`, the creation of the following configuration elements and UI elements is required for rich text components: + +#### User mention element (Non-visible configuration element): + +This element serves as a container for the mapping elements and additionally functions as a configuration element. It allows the transmission of +arbitrary values that are specific to the `@mention` users within the rich text components. Currently, this component has the below +configuration properties/attributes for user mentions and plays the role of containing the user mapping elements. + +This component can be used as a feature flag to enable the support for `@mention` in the rich text editor and viewer. +If this component is not present at the children of the rich text editor or viewer, the component will function the same without the support +for `@mention` in any place like editor, viewer, markdown parser and serializer and the toolbar button to add **"@"** in the editor +will be hidden. + +_Component Name_ + +- `nimble-rich-text-mention-users` + +_Content_ + +- Zero or more `nimble-mapping-mention-user` elements + +_Props/Attrs_ + +- `pattern` - is a string attribute that gets the regex pattern to match the `mention-href` in the mapping element. Exactly similar to + HTML attribute: [pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern). +- `validity` - is a read-only object of boolean values that represents the validity state that the `@mention` configuration can be. The object type + is `RichTextMentionValidity`. The validation is especially for mapping the user details that are provided via the + `nimble-mapping-mention-user`. For example, if the client application provides the duplicate `mention-href` values that store the user ID, it will be an + issue in scenarios when parsing the mentioned user from just a user ID to a username. Similar to + [ValidityState](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) + +_Events_ + +- `mention-update` - This event fires when the `@` character is added to the editor and for every character input after `@`. + This fires with the `eventData` containing the current text that is added after the `@` character and before the current position of the + text cursor. + + This can be achieved through Tiptap's `onUpdate()` and `onStart()` methods in `render` function in + [suggestion](https://tiptap.dev/api/utilities/suggestion#render) configurations. + + This event will be triggered in the following scenarios to perform filtering in the client application. The below scenarios will also fire + when there is a configuration element `nimble-rich-text-mention-users` even without the mapping elements: + + 1. When a user inserts the character (e.g., `@`) into the editor, which activates the mention popup. + 2. When a user adds or removes text after inserting the mention character into the editor. + 3. When a user repositions the cursor between the text segments added after the mention character. + 4. When a user copies a rich text content containing mention nodes either from viewer or from editor and pastes it in editor. + + Refer the [accessibility](#accessibility) section to know more details about when it is required to emit the event for performing the filtering in the client application. + + Conversely, this event will not be fired when: + + 1. A user moves the cursor away either using the mouse or any key down events that move the cursor from the current position to outside of `@` + and characters after a minimum of two white spaces while the popup is open. + 2. A user selects an option from the dropdown list either using mouse click/pressing Enter or Tab. + 3. A user adds two or more spaces after triggering the mention popup as the dropdown will be dismissed, see [Accessibility](#accessibility). + 4. In the absence of `nimble-rich-text-mention-users` and no mapping elements. + +_Methods_ + +- `checkValidity()` - this returns `true` if the configuration of the `@mention` mapping data is valid and `false` otherwise. +- `getMentionedHrefs()` - this returns an array of strings representing the mentioned user URL (`mention-href` property value of + the user mapping element) in the current state of the editor. + +#### User mapping element (Non-visible configuration element): + +This mapping element is employed to establish a connection between the value displayed in the mapping view and the corresponding value stored within +the markdown string. For instance, the username for an `@mention` is contained in the `display-name` attribute, which is used for display in the +mention view, while the user Href contained in the `mention-href` attribute is used to store within the markdown string. + +_Component Name_ + +- `nimble-mapping-mention-user` + +_Props/Attrs_ + +- `mention-href`: string +- `display-name`: string + +#### User mention view (Visible UI element): + +This is a UI component used to render the `@mention` node in a rich text editor and rich text viewer when parsed or added into the components. +This holds the styling for the `@mention` nodes. + +Different copying and pasting behaviors of the user mention view node: + +1. Copying an `@mention` name from the viewer and pasting it into the editor should result in the appearance of the same mention node. +2. When copying an `@mention` name from the editor and pasting it into the editor should result in the appearance of the same mention node. +3. If a portion of an `@mention` name from the viewer is copied and pasted into the editor, it should render only part of the text which was copied from mention node. +4. Copying only a portion of an `@mention` name from the editor is not possible, as the entire name will always be selected. The copy-and-paste behavior will be the same as point 2. +5. When copying an `@mention` node from viewer which has a different `pattern` or no configuration element available in the child when pasting into the editor, the corresponding URL will + rendered as links in the editor. Later, when config elements were added or `pattern` updates, links matching with the pattern will render as mention node. + +_Component Name_ + +- `nimble-rich-text-mention-users-view` + +_Props/Attrs_ + +- `mention-href`: string - the unique user URL containing a user ID of the mentioned user +- `mention-label`: string - the user name of the mentioned user or the user ID extracted from the above Href if mapping element is not + present for the particular user + +_Content_ + +- `@` + mentioned user name or user ID (if mapping element is not present for the particular user) + +#### Mention popup (Visible UI element): + +This container element is responsible for holding the `nimble-list-box` and `nimble-list-option` elements generated from the map. These elements are used to exhibit the +mention options in a dropdown list when the mention character is introduced into the editor (e.g., **"@"**). Additionally, this container element +manages all the key down and mouse interactions within the dropdown, such as selecting options and navigating using the up and down arrow keys, that are associated with the list +options. + +_Component Name_ + +- `nimble-rich-text-mention-list-box` + +_Content_ + +- One or more `nimble-list-option` elements + +_Events_ + +- `change` - this event is fired whenever a change happens within the dropdown either by clicking the options via mouse or selecting the options through key down events. + This is used by the `nimble-rich-text-editor` to make the Tiptap editor select the option and render as a mention node using a command from the `Mention` extension in Tiptap. + +### Anatomy + +#### `nimble-rich-text-mention-users` + +```HTML + +``` + +#### `nimble-mapping-mention-user` + +```HTML + +``` + +#### `nimble-rich-text-mention-users-view` + +```HTML + +``` + +#### `nimble-rich-text-mention-list-box` + +```HTML + +``` + +### `nimble-rich-text-viewer` + +The rich text viewer also supports showing the mentioned users in the emphasized text with a prominent color like in the +interaction design linked in the [Background](./README.md#background) section above. So the markdown string that contains a `@mention` syntax should be +identified by the viewer for mapping the user URL with the user name to display within a `nimble-rich-text-mention-users-view`. + +Below is an example of how the client application can be used to provide the `nimble-rich-text-viewer` with necessary user information: + +```HTML + + + + + + + +``` + +#### Client Usage Guidance on Filtered Users: + +The client application should either parse the markdown string and get the user Href that match the [markdown string of `@mention`](#2-markdown-format) +to identify what are all the mentioned users or utilize the `getMentionedHrefs()` method from `nimble-rich-text-mention-users` that +stores the Href of the mentioned users in an array while creating a comment or adding a description in the editor to identify the mentioned users +in the markdown string. Once all the user IDs are identified, it is enough to provide the user details only for the identified users +through the `nimble-mapping-mention-user`. For example, if the markdown string contains only two `@mention` users namely 'Bob Jones' +and 'Alice Smith', it is enough to include only `nimble-mapping-mention-user` for 'Bob Jones' and 'Alice Smith' within +`nimble-rich-text-mention-users`. + +### API + +The following configuration and UI elements are employed to associate user URLs with names, and then present the corresponding mention nodes within +the `nimble-rich-text-viewer`. The viewer similarly makes use of the shared components, thus linking it to the above sections for the details. + +1. [User mention element (Non-visible configuration element)](#user-mention-element-non-visible-configuration-element) +1. [User mapping element (Non-visible configuration element)](#user-mapping-element-non-visible-configuration-element) +1. [User mention view (Visible UI element)](#user-mention-view-visible-ui-element) + +### Anatomy + +Shadow root template for the components that are used in the `nimble-rich-text-viewer`. + +1. [`nimble-rich-text-mention-users`](#nimble-rich-text-mention-users) +2. [`nimble-mapping-mention-user`](#nimble-mapping-mention-user) +3. [`nimble-rich-text-mention-users-view`](#nimble-rich-text-mention-users-view) + +## Implementation + +The Tiptap [mention extension](https://tiptap.dev/api/nodes/mention) will transform all the `@mention` nodes into `` +elements, each with custom attribute values. These attributes play a dual role: they dictate the content displayed in the user interface and +correspond to the information stored in markdown format. For instance, when `@mention` is primarily employed for user tagging, these attribute values +typically encompass user-related data, such as the username and user URL. + +1. `mention-href` - employed to store the value that is sent in the `mention-href` attribute of `nimble-mapping-mention-user`. +2. `mention-label` - used to store the actual `display-name` of the selected option. +3. `contentEditable` - defaults as `false`. The `@mention` node is only enabled in the editor after selecting from the list of options. It is not possible + to edit the names within the node; either can delete the entire name or select a new one from the list of options after deleting the entire name. + +#### 1. _Configurations on Tiptap_: + +Install the [mention extension](https://tiptap.dev/api/nodes/mention) from Tiptap and configure the extension's functionalities as +follows to enable the desired `@mention` interactions, + +1. [`renderLabel`](https://tiptap.dev/api/nodes/mention#render-label) - to define how the `@mention` appears in the editor. In this case + `@username`(typically `username` represents the text content of the option). If a label is not available, the user ID from the Href is displayed + instead of an absolute link. +2. [`suggestion`](https://tiptap.dev/api/utilities/suggestion) - to handle the interactions and implementation settings as below, + 1. `char` - a character that the user to trigger the dropdown list. The default value is **"@"**. + 2. `render` - a function responsible for handling all the interactions within the dropdown. It returns an object with the following methods + to achieve the desired outcome, + 1. `onStart` - to trigger the opening of the dropdown, displaying a list of names. It is also responsible for opening the dropdown + whenever the cursor is placed after the **"@"** symbol. This also adjusts the position of the anchored region by updating the + [`anchorElement`](https://github.com/microsoft/fast/blob/master/packages/web-components/fast-foundation/src/anchored-region/README.md#fields:~:text=to%20revaluate%20positioning-,anchorElement,-public) + from FAST with the `decorationNode` from the `SuggestionProps` in Tiptap. + 2. `onUpdate` - to trigger the opening of the dropdown whenever the text after the **"@"** is updated. + 3. `onKeydown` - to handle key-down events when the dropdown list is opened. The `props`, a parameter of this method, containing + the keyboard event passed to the `nimble-rich-text-mention-list-box` component which has the `keyDownHandler()` method. This + method handles the necessary key down handlers for the dropdown list and for the other unused keys for the dropdown it returns + `false` to retain the editor's default behaviors. + 4. `onExit` - to close the dropdown when focused away from the **"@"** character and their substring in the editor. + +#### 2. _Markdown Format_: + +`@mention` - ``. Since there is no built-in syntax for mentioning or tagging users or individuals in the +[CommonMark](https://spec.commonmark.org/0.30/) flavor, we have decided to utilize the [Autolinks](https://spec.commonmark.org/0.30/#autolink) +format in the `CommonMark` flavor with user Href containing a user ID as a unique value of `@mention` users. + +_Pros_ + +1. This markdown string offers the advantage of serving a dual purpose. Depending on the `pattern` specified in the configuration element, + it can be rendered as a `mention` node. In the absence of a specified `pattern`, it remains a valid markdown format and is displayed as an absolute link. +2. If we intend to enable interaction by clicking, passing a Href can be leveraged to navigate users to their intended destination. For instance, + when referencing an issue, clicking on it should direct the user to the issue page, and this functionality can be facilitated through the Href value. +3. Furthermore, this will not disrupt the operation of the `AutoLink` format since the `mention` node is evaluated before the `autolink` mark. + +_Alternatives_: + +1. `@mention` - a custom scheme (`user`) in the Autolink format of CommonMark flavor ``. + + Pros: + + 1. This markdown syntax does not interfere with the current `AutoLink` formats as the scheme we support for absolute links is `http` and `https`, + whereas this is a unique scheme and yet follows the standard `CommonMark` markdown flavor + 2. This syntax ensures the easy identification of a `mention` node using the `user` in place of `scheme` in Autolink when parsing the + entire markdown string + 3. The same syntax can be used when other mentions like issue mention by having a different `scheme` in the string like `` + + Cons: + + Creating a custom scheme allows the component to independently manage various scenarios. When parsing markdown, the tokenizer looks for the closing `>` tag, + recognizes the user ID, and saves it. However, if the user ID itself contains a `>`, it may mistakenly identify the wrong user ID. In valid absolute link URLs, + special characters like `>` are encoded and preserved, ensuring a unique absolute URL for retrieving data, storing it in markdown, and returning it. + +2. `@mention` - a custom markdown format `@`. + + Pros: + + 1. This syntax ensures the easy identification of a `mention` node using the **"@"** and **"<"** symbols when parsing the entire markdown string. + 2. The use of opening and closing **"<"** and **">"** symbols specifies the boundaries of the mention node's value within the markdown string, + allowing for the clear identification of where it starts and ends. This is especially important when the value contains whitespace, + as in the example `"@`, as without the **"<>"** symbols, it would be challenging to determine the precise end index of the value. + + Cons: + + 1. It does not follow [CommonMark](https://spec.commonmark.org/0.30/) flavor or any other standard markdown flavors. + +#### 3. _Defining schema and adding tokenizer rule in markdown parser_: + +As `@mention` is a custom markdown format uniquely created to support the nimble rich text components, it is necessary to +define the schema in `markdown-parser` to identify the markdown string in the format of `` as the `@mention` node. +The below schema is added to the end of other nodes using ProseMirror's +[addToEnd](https://prosemirror.net/docs/ref/#model.Fragment.addToEnd) method. + +`@mention` custom schema: + +```JS +mention: { + attrs: { + mentionHref: { default: '' }, + mentionLabel: { default: '' }, + }, + group: 'inline', + inline: true, + content: 'inline*', + toDOM(node) { + const { mentionHref, mentionLabel } = node.attrs; + return [ + 'nimble-rich-text-mention-users-view', + { + 'mention-href': mentionHref as string, + 'mention-label': mentionLabel as string, + }, + 0 + ]; + }, +} +``` + +Additionally, a custom tokenizer rule needs to be added to the `markdown-it` rules to handle the parser logic. +This can be achieved by loading the customized `mention` plugin into the supported tokenizer rules using the +[`use`](https://markdown-it.github.io/markdown-it/#MarkdownIt.use) method and identifying the value of the +`mention-href` that matches the `display-name`. The `mention-href` and `name` will then be generated as an object from the +`nimble-mapping-mention-user` elements. This custom node will be added `before` the `autolink` mark to give +the highest precedence to the `mention` node. + +If the user is no longer a valid user or the mapping elements were not yet updated but the markdown string stores a valid +user Href that matches the pattern, then the `@mention` node will parse as a user ID, instead of a user name as they are not +mapped with any mapping elements. + +#### 4. _Defining node in markdown serializer_: + +The rendered `@mention` node will be constructed into a markdown string by extracting the `mention-href` from +the `span` element in the editor when the `getMarkdown()` method is called. + +The example markdown string constructed for the below DOM element rendered in the editor is ``. + +```HTML +@Mary +``` + +The `getMentionedHrefs()` method, as described in the [API section of the editor](./README.md#api), will undergo an update within the +mentionNode. This update involves extracting the hrefs that matches the pattern specified in the configuration element and +then pushing them to an array. + +#### 5. _nimble-rich-text-mention-users_: + +An abstract base class, `RichTextMention`, is defined as the parent for all elements that contain mentions, and it will possess the following properties: + +1. `character`: string - is a specific symbol to trigger the mention popup. For user mention, it is **"@"**. +2. `icon`: string - element name of the icon for the corresponding toolbar button. + +The base class should also contain the `getMentionedHrefs()` method to get the mentioned users list in the current state of the editor. + +The `nimble-rich-text-mention-users` is a subclass derived from the base class and provides the essential values for the properties mentioned above, +specifically tailored for user mentions. These values are kept as part of the `MentionInternals` and will be utilized by various components +that require access to these specific values. + +Validation will be integrated into the internal workings through a `validConfiguration` flag. There is a base class called `RichTextMentionValidator` +responsible for handling validation, with methods for setting and retrieving the validation configuration. This approach ensures that the state of +valid and invalid values passed within the mapping element is effectively managed. The class obtains the name of the validity flag to communicate this +information via a public API called `validity`. + +By deriving from the base, the mention options can validate the following conditions for the `display-name` values in user mapping element: + +1. `validateMappingTypes(mappings)` +2. `validateNoMissingDisplayName(mappings)` + +A common validation, such as `mention-href` validation, can be carried out within a base class shared among all types of mentions +(like user mention, issue mention). Some of them includes: + +1. `validateUniqueHref(href, hrefType)` +2. `validateNoMissingHref(mappings)` +3. `validateHref(mappings)` +4. `validatePattern(pattern)` + +_Note_: These are subject to change based on the property changes in the `nimble-mapping-mention-user` element. + +If any of the mentioned options is invalid as per the above validation, that particular option will be rendered as an empty +option in the list. This indicates to the client that some of the options are wrongly configured and identify the +validation details using the public API `validity`. + +The public API to determine the validity of the mention options are `checkValidity()` and `validity`. +See the [API section](#user-mention-element-non-visible-configuration-element) for more details. + +#### 6. _nimble-rich-text-mention-users-view_: + +The foundation for configuring the rendering of mention nodes in the UI is provided by a base class known as `nimble-rich-text-mention-view`. This base class is an extension of +`FoundationElement`. + +The `nimble-rich-text-mention-users-view` is a view class that is derived from the above base class, and its template contains a `span` element with the required CSS styling. +They are equipped with attributes such as `mention-href` and `mention-label` to render the node as a `Mention` node in the Tiptap editor. +It uses the default slot to render the same text content from the editor to the view element's shadow root. + +The Mention node from Tiptap is extended, and the [`renderHTML`](https://tiptap.dev/guide/custom-extensions/#render-html) method is overridden to render +the element as a `nimble-rich-text-mention-users-view` instead of the default `span` element as shown in the +[Tiptap code](https://github.com/ueberdosis/tiptap/blob/42039c05f0894a2730a7b8f1b943ddb22d52a824/packages/extension-mention/src/mention.ts#L112). +The attributes will be retained and included in the rendered mention nodes. The [`parseHTML`](https://tiptap.dev/guide/custom-extensions/#parse-html) +will also hold the `nimble-rich-text-mention-users-view` tag and `span` with the type attribute mention to load the specified tags as mention node +in the editor. + +#### 7. _nimble-rich-text-mention-list-box_: + +This component has FAST's `list-box` in its template to inherit the essential mouse and keyboard interactions required within the dropdown list. +To use the list box, a component named `nimble-list-box` will be created with just the styling changed from +[FAST List Box](https://github.com/microsoft/fast/tree/master/packages/web-components/fast-foundation/src/listbox) to match the nimble theme. +Within its default slot, it gets `nimble-list-option` elements which are used to populate the list with the provided options. + +This component is responsible for updating the options property by filtering the list of options provided by the parent component, which in this context is the +`nimble-rich-text-editor`. When the options are updated, the component internally handles setting the selected index values within the list-box. + +To facilitate keyboard interactions, a public method called `keyDownHandler()` is provided in `nimble-rich-text-mention-list-box`. This method is invoked in the +`onKeyDown()` function in the Tiptap configuration, as described in the [configuration on Tiptap](#1-configurations-on-tiptap) section. The `keyDownHandler()` +method takes the keyboard event as input and performs necessary operations, such as responding to up and down arrow key presses to decrement or increment the +selected index value for each option, handling the `Enter` key presses for option selection, and so on. + +A click handler is attached to the `nimble-list-box`, and it emits the selected option data to the parent component through a `change` event. + +Both the click and key down handlers will emit a `change` event which is to be listened by the parent element to render the mention node based on selection in the editor. +This is done in the parent component by invoking the `mention.command` from Tiptap to render the mention node based on the selected option data. + +### Angular integration + +An Angular directive will be created for `nimble-rich-text-mention-users` and `nimble-mapping-mention-user` components. Input and accessor APIs will be created for the attributes +and properties and output event emitters will be created for the events, similar to how it's done in other directives. The components will not have form +association, so a `ControlValueAccessor` will not be created. + +### States + +When you add **"@"** into the editor, a dropdown will open with the first value as the selected option, indicated by a green overlay to the option +if `nimble-list-option` is passed into the default slot element. Upon selecting an option from the list, it will be rendered as prominent green text +with bold formatted by default in all themes. Below is a basic representation of `@mention` texts and dropdown lists in the default theme. + +![@mention State](./spec-images/mention-state.png) + +An **"@"** icon will be added to the formatting toolbar to insert the **"@"** character, and clicking the button will open a dropdown list of options. +This button will not be highlighted like other buttons when the cursor is placed on the @mentions within the text area of the editor. + +### Accessibility + +_Focus_ + +- Focus state of the list options for `@mention` will be the same as the `nimble-list-option`. +- Focusing out from the editor will close the `@mention` popup if it is already open. +- The `@mention` popup will open at the position where the **"@"** character is added into the editor. If the editor has the scrollbar enabled, + scrolling the editor up and down will not move the popup to the **"@"** position; it will remain at the same position where it was originally opened. + However, if an option is selected from the list, the focus will return to the position where the text cursor is located, next to the mentioned + user view node, in the editor. +- If the `@mention` popup is opened, it will move along with the editor as you scroll the entire webpage, and it will hide when the + editor hides. + +_Keyboard interactions for `@mention`_ + +| Key | Behavior | +| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `@` (At least single whitespace before `@`) | Open the list of options with the users list | +| `@` (At the start of the paragraph) | Open the list of options with the users list even without a preceding white space | +| | ----Below are the keyboard interactions when popup is opened---- | +| Enter, Tab | Select the currently focused option from the list | +| Up/Down Arrow keys | Move the focus upward/downward in the list of options | +| Left/Right Arrow keys | Move the cursor in the editor towards left/right and filters the list for the characters from `@` to the current cursor position | +| Left/Right Arrow keys (moves away from `@`) | Close the dropdown list popup | +| Escape | Close the dropdown if it is opened | +| Any character | Show the filtered list of users starts with the entered character | +| Space(One time between names) | Show the filtered list of users that contains a single whitespace in their name | +| Space(Two or more trailing white spaces) | Close the dropdown list popup | +| Group of characters | Show the filtered list of users starts with the entered group characters | +| Group of characters (Not in the list) | Close the dropdown list popup | +| | ----Below are the keyboard interactions when user is selected from the list---- | +| Backspace | Remove the entire selected name and cursor in the `@` position | +| Shift + Arrow keys | Select the mention node | + +_Note_: The `@mention` node is immutable. For instance, if the user is selected and the mentioned node(`nimble-rich-text-mention-users-view`) is added to the DOM, +the only option is to either remove the entire name or add a new name after removing the entire name. It is not possible to edit the name, like removing just +the last name and keeping the first name. + +_Mouse interactions for `@mention`_ + +1. Hovering over each option in the `@mention` dropdown lists will show an indication that the option is selectable. +2. Left-clicking the option will select the hovered option from the list of users. +3. Clicking outside the editor/Focusing out from the editor will close the popup if it is already open. + +_ARIA Roles/Properties for `@mention` components_ + +_`nimble-rich-text-mention-list-box`_ + +The `nimble-rich-text-mention-list-box` will have the role of `listbox` and the `nimble-list-option` will have the role of `option` as in the other +list option components like `nimble-select` and `nimble-combobox`. Both the components will respect the `aria-disabled` based on the parent element +`nimble-rich-text-editor`. + +The `nimble-list-option` (leverage the existing property functionalities) will have the following `aria-*` attributes value set based on the state of the option, + +1. `aria-selected` - set to `true` if the option is selected/focused from the list of options. Anyone from the list will have the `true` value. +2. `aria-posinset` - determines the current position of the list items. +3. `aria-setsize` - determines the actual length of the current list options. + +_`nimble-rich-text-mention-users-view`_ + +The `nimble-rich-text-mention-users-view` will not have any specific `roles` or `aria-*` attributes to differentiate the `@mention` node from other +texts in the editor or viewer. + +_Future Scope_: + +If the `nimble-rich-text-mention-users-view` has the ability to show more information about the user by showing a tooltip by clicking on the node, +then it should probably use a `button` or `link` role to specify its difference from other nodes. Also, having ARIA attributes like `aria-labelledby` +to mention the user detail which is not displayed in the UI when the feature to show a tooltip is enabled. + +## Open Issues + +- none diff --git a/packages/nimble-components/src/rich-text/specs/spec-images/mention-state.png b/packages/nimble-components/src/rich-text/specs/spec-images/mention-state.png new file mode 100644 index 0000000000000000000000000000000000000000..143accfc8147c7a64ceaa228f67c48af0c7ac47b GIT binary patch literal 6908 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 literal 0 HcmV?d00001