updateColor(color)}
+ onKeyDown={undefined}
+ />
+ ))}
+
+
+ );
+};
+
+TagEditor.propTypes = {
+ tag: (PropTypes.object as Requireable
).isRequired,
+ anchor: (PropTypes.oneOfType([
+ PropTypes.object as Requireable, PropTypes.string,
+ ])).isRequired,
+ closingFunction: PropTypes.func,
+};
+
+TagEditor.defaultProps = {
+ closingFunction: () => {},
+};
diff --git a/web/src/components/tagSelector/TagPicker.tsx b/web/src/components/tagSelector/TagPicker.tsx
new file mode 100644
index 00000000..8c089d49
--- /dev/null
+++ b/web/src/components/tagSelector/TagPicker.tsx
@@ -0,0 +1,118 @@
+import React, {
+ Requireable, useState,
+} from 'react';
+import PropTypes from 'prop-types';
+import {
+ Callout, DefaultButton, Stack, TextField,
+} from '@fluentui/react';
+import { useId } from '@fluentui/react-hooks';
+import { ICard } from '../../controllers/Card';
+import { useApp } from '../../AppContext';
+import { Tag } from '../Tag';
+import { ITag, ITagProperties } from '../../controllers/Tag';
+import { TagWrapper } from './TagWrapper';
+import { ITagEditorProps, TagEditor } from './TagEditor';
+
+export interface ITagPickerProps {
+ targetCard: ICard;
+ editing: boolean;
+}
+
+type ITagAnchor = Pick;
+
+export const TagPicker: React.VoidFunctionComponent = ({
+ targetCard, editing,
+}) => {
+ const user = useApp();
+ const [pickerActive, setPickerActive] = useState(false);
+ const [tagSearchString, setTagSearchString] = useState('');
+ const [focusedTag, setFocusedTag] = useState();
+ const pickerTargetId = useId('picker-target');
+
+ function getNewTag(): void {
+ const tagProps: Partial = {
+ label: tagSearchString || 'New Tag',
+ };
+ // newTag refreshes context and may lead to loss of data.
+ user.newTag(tagProps);
+ setTagSearchString('');
+ }
+
+ function removeTag(t: ITag): void {
+ targetCard.update({ tags: targetCard.tags.filter((that) => that.id !== t.id) });
+ }
+
+ // user variable is used to access the tags available to the user
+ return (
+
+
+ Tags
+
+
+ {/* could style to fit measurements */}
+
+ {targetCard?.tags.map((t) => ((editing)
+ ? (
+ removeTag(t)}
+ />
+ )
+ : ))}
+
+
+
+ {editing
+ && setPickerActive((old) => !old)} id={pickerTargetId} />}
+ {pickerActive
+ ? (
+ {
+ if (!focusedTag) setPickerActive(false);
+ }}
+ >
+
+
+ setTagSearchString(event.currentTarget.value)}
+ />
+
+
+ {/* Tags available to use are listed here */}
+ {user.user?.tags
+ .filter((t) => t.label.toUpperCase().startsWith(tagSearchString.toUpperCase()))
+ .map((t) => (
+
+ setFocusedTag({ anchor: id, tag: t })}
+ />
+
+ ))}
+
+
+ )
+ : null}
+ {focusedTag
+ && (
+ setFocusedTag(undefined)}
+ />
+ )}
+
+ );
+};
+
+TagPicker.propTypes = {
+ targetCard: (PropTypes.object as Requireable).isRequired,
+ editing: PropTypes.bool.isRequired,
+};
diff --git a/web/src/components/tagSelector/TagWrapper.tsx b/web/src/components/tagSelector/TagWrapper.tsx
new file mode 100644
index 00000000..5cb2d468
--- /dev/null
+++ b/web/src/components/tagSelector/TagWrapper.tsx
@@ -0,0 +1,49 @@
+import { DefaultButton } from '@fluentui/react';
+import { useId } from '@fluentui/react-hooks';
+import React, { Requireable } from 'react';
+import PropType from 'prop-types';
+import { ICard } from '../../controllers/Card';
+import { ITag } from '../../controllers/Tag';
+import { Tag } from '../Tag';
+
+export interface ITagWrapperProps {
+ tag: ITag;
+ card?: ICard;
+ setTagEdit?: (id: string) => void;
+}
+
+export const TagWrapper: React.VoidFunctionComponent = ({
+ tag, card, setTagEdit,
+}) => {
+ const targetElemId = useId();
+
+ const attachTag = () => {
+ if (card && !card.tags.includes(tag)) {
+ card.update({ tags: [...card.tags, tag] });
+ }
+ };
+
+ return (
+
+
+ {setTagEdit
+ && (
+ setTagEdit(targetElemId)}
+ />
+ )}
+
+ );
+};
+
+TagWrapper.propTypes = {
+ tag: (PropType.object as Requireable).isRequired,
+ card: (PropType.object as Requireable),
+ setTagEdit: PropType.func,
+};
+
+TagWrapper.defaultProps = {
+ card: undefined,
+ setTagEdit: undefined,
+};