Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EASI-4658] Integrate discussions tagging #2917

2 changes: 1 addition & 1 deletion pkg/sanitization/tagged_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ func createTaggedHTMLPolicy() *bluemonday.Policy {
policy := bluemonday.NewPolicy()
// rules for tags
policy.AllowElements("span", "p")
policy.AllowAttrs("data-type", "class", "tag-type", "data-id-db").OnElements("span")
policy.AllowAttrs("data-type", "class", "tag-type", "data-id-db", "data-label").OnElements("span")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tiptap needs a data-label attribute to render the mention labels. Without this attribute, the mentions just rendered as an empty span tag.

Copy link
Contributor

Choose a reason for hiding this comment

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

nice!! 🔥

return policy
}
164 changes: 88 additions & 76 deletions src/components/MentionTextArea/MentionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import React, {
useState
} from 'react';
import { useTranslation } from 'react-i18next';
import { TagType } from 'gql/gen/graphql';

import Spinner from 'components/Spinner';
import {
MentionListOnKeyDown,
MentionSuggestionProps
} from 'types/discussions';

import './index.scss';

Expand All @@ -21,91 +26,98 @@ export const SuggestionLoading = () => {
);
};

// Handler dropdown scroll event on keypress
/** Handler dropdown scroll event on keypress */
samoddball marked this conversation as resolved.
Show resolved Hide resolved
const scrollIntoView = () => {
const selectedElm = document.querySelector('.is-selected');
selectedElm?.scrollIntoView({ block: 'nearest' });
};

const MentionList = forwardRef((props: any, ref) => {
const { t } = useTranslation('discussionsMisc');
/** Renders the list of suggestions within `MentionTextArea` */
const MentionList = forwardRef<MentionListOnKeyDown, MentionSuggestionProps>(
(props, ref) => {
const { t } = useTranslation('general');

const [selectedIndex, setSelectedIndex] = useState(0);
const [selectedIndex, setSelectedIndex] = useState<number>(0);

// Sets the selected mention within the editor props
const selectItem = (index: any) => {
const item = props.items[index];
/** Sets the selected mention within the editor props */
const selectItem = (index: number) => {
const item = props.items[index];

if (item) {
props.command({
id: item.username,
label: item.displayName,
'tag-type': item.tagType
});
}
};

const upHandler = () => {
setSelectedIndex(
(selectedIndex + props.items?.length - 1) % props.items?.length
);
scrollIntoView();
};

const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items?.length);
scrollIntoView();
};

const enterHandler = () => {
selectItem(selectedIndex);
};

useEffect(() => setSelectedIndex(0), [props.items]);

useImperativeHandle(ref, () => ({
onKeyDown: ({ event }: { event: any }) => {
if (event.key === 'ArrowUp' || (event.shiftKey && event.key === 'Tab')) {
upHandler();
return true;
if (item) {
props.command({
'tag-type': item.tagType,
label: item.displayName,
'data-label': item.displayName,
'data-id-db': item.tagType === TagType.USER_ACCOUNT ? item.id : ''
});
}

if (
event.key === 'ArrowDown' ||
(!event.shiftKey && event.key === 'Tab')
) {
downHandler();
return true;
};

const upHandler = () => {
setSelectedIndex(
(selectedIndex + props.items?.length - 1) % props.items?.length
);
scrollIntoView();
};

const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items?.length);
scrollIntoView();
};

const enterHandler = () => {
selectItem(selectedIndex);
};

useEffect(() => setSelectedIndex(0), [props.items]);

useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (
event.key === 'ArrowUp' ||
(event.shiftKey && event.key === 'Tab')
) {
upHandler();
return true;
}

if (
event.key === 'ArrowDown' ||
(!event.shiftKey && event.key === 'Tab')
) {
downHandler();
return true;
}

if (event.key === 'Enter') {
enterHandler();
return true;
}

return false;
}

if (event.key === 'Enter') {
enterHandler();
return true;
}

return false;
}
}));

return (
<div className="items">
{props.items?.length ? (
props.items?.map((item: any, index: any) => (
<button
className={`item ${index === selectedIndex ? 'is-selected' : ''}`}
key={item.username}
id={item.username}
type="button"
onClick={() => selectItem(index)}
>
{item.displayName}
</button>
))
) : (
<div className="item">{t('noResults')}</div>
)}
</div>
);
});
}));

return (
<div className="items">
{props.items?.length ? (
props.items?.map((item, index) => (
<button
className={`item ${index === selectedIndex ? 'is-selected' : ''}`}
key={item.displayName}
id={item.displayName}
type="button"
onClick={() => selectItem(index)}
>
{item.displayName}
</button>
))
) : (
<span className="item padding-x-1">{t('noResults')}</span>
)}
</div>
);
}
);

export default MentionList;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`MentionTextArea component > renders the component to view text 1`] = `
<DocumentFragment>
<div
class="tiptap__readonly font-body-sm line-height-body-5"
id="mentionTextArea-editorContent"
>
<div
aria-label="Rich text area"
Expand All @@ -25,6 +26,7 @@ exports[`MentionTextArea component > renders the editable text area component 1`
<DocumentFragment>
<div
class="tiptap__editable usa-textarea"
id="mentionTextArea-editorContent"
tabindex="-1"
>
<div
Expand Down
74 changes: 31 additions & 43 deletions src/components/MentionTextArea/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Document from '@tiptap/extension-document';
import Mention from '@tiptap/extension-mention';
import Mention, { MentionOptions } from '@tiptap/extension-mention';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import {
EditorContent,
EditorEvents,
Node,
NodeViewProps,
NodeViewWrapper,
ReactNodeViewRenderer,
useEditor
Expand All @@ -15,23 +18,20 @@ import classNames from 'classnames';

import Alert from 'components/shared/Alert';
import IconButton from 'components/shared/IconButton';
import { MentionAttributes, MentionSuggestion } from 'types/discussions';

import suggestion from './suggestion';
import { getMentions } from './util';

import './index.scss';

/* The rendered Mention after selected from MentionList
This component can be any react jsx component, but must be wrapped in <NodeViewWrapper />
Attrs of selected mention are accessed through node prop */
const MentionComponent = ({ node }: { node: any }) => {
/** The rendered Mention after selected from MentionList */
// This component can be any react jsx component, but must be wrapped in <NodeViewWrapper />
const MentionComponent = ({ node }: NodeViewProps) => {
// Get attributes of selected mention
const { label } = node.attrs;

// Label may return null if the text was truncated by <TruncatedText />
// In this case don't render the mention, and shift the line up by the height of the non-rendered label
if (!label) {
return <div className="margin-top-neg-4" />;
}
if (!label) return null;

return (
<NodeViewWrapper className="react-component display-inline">
Expand All @@ -40,9 +40,14 @@ const MentionComponent = ({ node }: { node: any }) => {
);
};

/* Extended TipTap Mention class with additional attributes
Additionally sets a addNodeView to render custo JSX as mention */
const CustomMention = Mention.extend({
/**
* Extended TipTap Mention class with additional attributes
*
* Additionally sets a addNodeView to render custo JSX as mention
*/
const CustomMention: Node<
MentionOptions<MentionSuggestion, MentionAttributes>
> = Mention.extend({
atom: true,
selectable: true,
addAttributes() {
Expand All @@ -64,6 +69,7 @@ const CustomMention = Mention.extend({
type MentionTextAreaProps = {
id: string;
setFieldValue?: (value: string) => void;
mentionSuggestions?: MentionSuggestion[];
editable?: boolean;
disabled?: boolean;
initialContent?: string;
Expand All @@ -80,6 +86,7 @@ const MentionTextArea = React.forwardRef<HTMLDivElement, MentionTextAreaProps>(
{
id,
setFieldValue,
mentionSuggestions,
editable = false,
disabled,
initialContent,
Expand All @@ -96,35 +103,13 @@ const MentionTextArea = React.forwardRef<HTMLDivElement, MentionTextAreaProps>(
const [tagAlert, setTagAlert] = useState<boolean>(false);

/** Mock users array for testing until tagging functionality is implemented */
const fetchUsers = ({ query }: { query: string }) => {
return [
{ username: 'a', displayName: 'Admin lead', tagType: 'other' },
{
username: 'b',
displayName: 'Governance Admin Team',
tagType: 'other'
},
{
username: 'c',
displayName: 'Governance Review Board (GRB)',
tagType: 'other'
},
{
username: 'OSYC',
displayName: 'Grant Eliezer',
tagType: 'user'
},
{
username: 'MKCK',
displayName: 'Forest Brown',
tagType: 'user'
},
{
username: 'PJEA',
displayName: 'Janae Stokes',
tagType: 'user'
}
];
const fetchUsers = ({ query }: { query: string }): MentionSuggestion[] => {
if (!mentionSuggestions) return [];

return mentionSuggestions.filter(val =>
// Convert both strings to lowercase so filter is not case-sensitive
val.displayName.toLowerCase().includes(query.toLowerCase())
);
};

/** Character limit when truncating text in non-editable text area */
Expand Down Expand Up @@ -188,7 +173,9 @@ const MentionTextArea = React.forwardRef<HTMLDivElement, MentionTextAreaProps>(
}
},
// Sets an alert if a mention is selected, and users/teams will be emailed
onSelectionUpdate: ({ editor: input }: any) => {
onSelectionUpdate: ({
editor: input
}: EditorEvents['selectionUpdate']) => {
setTagAlert(!!getMentions(input?.getJSON()).length);
},
content
Expand All @@ -212,6 +199,7 @@ const MentionTextArea = React.forwardRef<HTMLDivElement, MentionTextAreaProps>(
return (
<>
<EditorContent
id={`${id}-editorContent`}
ref={ref}
tabIndex={editable ? -1 : undefined}
editor={editor}
Expand Down
Loading
Loading