Move channel mentioning to use @mui/Autocomplete combobox without search functionality

This commit is contained in:
Rafael 2021-12-06 14:28:36 -03:00 committed by Thomas Zarebczan
parent a459e98cab
commit ea84d1af56
10 changed files with 213 additions and 263 deletions

View file

@ -84,7 +84,6 @@ export function CommentCreate(props: Props) {
settingsByChannelId,
shouldFetchComment,
supportDisabled,
uri,
createComment,
doFetchCreatorSettings,
doToast,
@ -156,7 +155,10 @@ export function CommentCreate(props: Props) {
}
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
if ((isLivestream || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
// $FlowFixMe
const isTyping = e.target.attributes['term'];
if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
e.preventDefault();
buttonRef.current.click();
}
@ -443,10 +445,11 @@ export function CommentCreate(props: Props) {
closeSelector={() => setShowEmotes(false)}
/>
)}
<FormField
autoFocus={isReply}
charCount={charCount}
className={isReply ? 'create__reply' : 'create___comment'}
className={isReply ? 'create__reply' : 'create__comment'}
disabled={isFetchingChannels}
isLivestream={isLivestream}
label={
@ -457,7 +460,7 @@ export function CommentCreate(props: Props) {
<SelectChannel tiny />
</div>
}
name={isReply ? 'create__reply' : 'create___comment'}
name={isReply ? 'create__reply' : 'create__comment'}
onBlur={() => window.removeEventListener('keydown', altEnterListener)}
onChange={(e) => setCommentValue(SIMPLE_SITE || !advancedEditor || isReply ? e.target.value : e)}
onFocus={() => window.addEventListener('keydown', altEnterListener)}
@ -470,7 +473,6 @@ export function CommentCreate(props: Props) {
ref={formFieldRef}
textAreaMaxLength={isLivestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
type={!SIMPLE_SITE && advancedEditor && !isReply ? 'markdown' : 'textarea'}
uri={uri}
value={commentValue}
/>
</>

View file

@ -39,7 +39,6 @@ type Props = {
stretch?: boolean,
textAreaMaxLength?: number,
type?: string,
uri?: string,
value?: string | number,
onChange?: (any) => any,
openEmoteMenu?: () => void,
@ -86,7 +85,6 @@ export class FormField extends React.PureComponent<Props> {
stretch,
textAreaMaxLength,
type,
uri,
openEmoteMenu,
quickActionHandler,
render,
@ -254,8 +252,6 @@ export class FormField extends React.PureComponent<Props> {
id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
inputRef={this.input}
hideSuggestions={hideSuggestions}
uri={uri}
isLivestream={isLivestream}
{...inputProps}
/>

View file

@ -1,10 +1,9 @@
import { connect } from 'react-redux';
import { selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
import { selectClaimForUri } from 'redux/selectors/claims';
import TextareaSuggestionsItem from './view';
const select = (state, props) => ({
claim: props.uri && selectClaimForUri(state, props.uri),
isResolvingUri: props.uri && selectIsUriResolving(state, props.uri),
});
export default connect(select)(TextareaSuggestionsItem);

View file

@ -1,43 +1,27 @@
// @flow
import { ComboboxOption, ComboboxOptionText } from '@reach/combobox';
import ChannelThumbnail from 'component/channelThumbnail';
import React from 'react';
type Props = {
claim?: Claim,
isResolvingUri: boolean,
uri?: string,
};
export default function TextareaSuggestionsItem(props: Props) {
const { claim, isResolvingUri, uri } = props;
const { claim, uri, ...autocompleteProps } = props;
if (!claim) return null;
if (isResolvingUri) {
return (
<div className="textareaSuggestion">
<div className="media__thumb media__thumb--resolving" />
</div>
);
}
const canonicalMention = claim.canonical_url.replace('lbry://', '').replace('#', ':');
const value = claim.canonical_url.replace('lbry://', '').replace('#', ':');
return (
<ComboboxOption value={canonicalMention}>
<div className="textareaSuggestion">
<div {...autocompleteProps}>
<ChannelThumbnail xsmall uri={uri} />
<div className="textareaSuggestion__label">
<span className="textareaSuggestion__title">
{(claim.value && claim.value.title) || <ComboboxOptionText />}
</span>
<span className="textareaSuggestion__value">
<ComboboxOptionText />
</span>
<span className="textareaSuggestion__title">{(claim.value && claim.value.title) || value}</span>
<span className="textareaSuggestion__value">{value}</span>
</div>
</div>
</ComboboxOption>
);
}

View file

@ -32,7 +32,7 @@ export default function TextareaTopSuggestion(props: Props) {
}
return filteredTop && filteredTop.length > 0 ? (
<div className="textareaSuggestions__row">
<div className="textareaSuggestions__group">
<div className="textareaSuggestions__label">
<LbcSymbol prefix={__('Most Supported')} />
</div>

View file

@ -1,7 +1,5 @@
import { connect } from 'react-redux';
import { doResolveUris } from 'redux/actions/claims';
import { getChannelFromClaim } from 'util/claim';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
import { selectChannelMentionData } from 'redux/selectors/comments';
import { selectShowMatureContent } from 'redux/selectors/settings';
@ -9,14 +7,16 @@ import { withRouter } from 'react-router';
import TextareaWithSuggestions from './view';
const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri)(state);
const { pathname } = props.location;
const maxComments = props.isLivestream ? MAX_LIVESTREAM_COMMENTS : -1;
const data = selectChannelMentionData(state, props.uri, maxComments);
const { canonicalCommentors, canonicalSubscriptions, commentorUris } = data;
const uri = `lbry:/${pathname.replaceAll(':', '#')}`;
const data = selectChannelMentionData(state, uri, maxComments);
const { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris } = data;
return {
canonicalCommentors,
canonicalCreatorUri: getChannelFromClaim(claim) && getChannelFromClaim(claim).canonical_url,
canonicalCreatorUri,
canonicalSubscriptions,
commentorUris,
showMature: selectShowMatureContent(state),

View file

@ -1,108 +1,100 @@
// @flow
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox';
import { matchSorter } from 'match-sorter';
import { SEARCH_OPTIONS } from 'constants/search';
import * as KEYCODES from 'constants/keycodes';
import { regexInvalidURI } from 'util/lbryURI';
import Autocomplete from '@mui/material/Autocomplete';
import React from 'react';
import Spinner from 'component/spinner';
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
import TextareaTopSuggestion from 'component/textareaTopSuggestion';
import type { ElementRef } from 'react';
import useLighthouse from 'effects/use-lighthouse';
import TextField from '@mui/material/TextField';
import useThrottle from 'effects/use-throttle';
const mentionRegex = /@[^\s"=?!@$%^&*;,{}<>/\\]*/gm;
const INPUT_DEBOUNCE_MS = 1000;
const LIGHTHOUSE_MIN_CHARACTERS = 4;
const SEARCH_SIZE = 10;
const mentionRegex = /@[^\s=&#:$@%?;/\\"<>%{}|^~[]*/gm;
type Props = {
canonicalCommentors: Array<string>,
canonicalCreatorUri: string,
canonicalSubscriptions: Array<string>,
canonicalCommentors?: Array<string>,
canonicalCreatorUri?: string,
canonicalSubscriptions?: Array<string>,
className?: string,
commentorUris: Array<string>,
doResolveUris: (Array<string>) => void,
hideSuggestions?: boolean,
commentorUris?: Array<string>,
disabled?: boolean,
id: string,
inputRef: any,
isLivestream?: boolean,
maxLength?: number,
name: string,
noTopSuggestion?: boolean,
placeholder?: string,
showMature: boolean,
type?: string,
uri?: string,
value: any,
doResolveUris: (Array<string>) => void,
onBlur: (any) => any,
onChange: (any) => any,
onFocus: (any) => any,
};
export default function TextareaWithSuggestions(props: Props) {
const {
canonicalCommentors,
canonicalCreatorUri,
canonicalSubscriptions,
canonicalSubscriptions: canonicalSubs,
className,
commentorUris,
doResolveUris,
hideSuggestions,
disabled,
id,
inputRef,
isLivestream,
maxLength,
name,
noTopSuggestion,
placeholder,
showMature,
type,
value: commentValue,
doResolveUris,
onBlur,
onChange,
onFocus,
} = props;
const inputProps = { className, placeholder };
const comboboxListRef: ElementRef<any> = React.useRef();
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
const [suggestionValue, setSuggestionValue] = React.useState(undefined);
const [debouncedTerm, setDebouncedTerm] = React.useState('');
const [topSuggestion, setTopSuggestion] = React.useState('');
const [canonicalSearchUris, setCanonicalSearchUris] = React.useState([]);
const [selectedValue, setSelectedValue] = React.useState(undefined);
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
const [shouldClose, setClose] = React.useState();
const suggestionTerm = suggestionValue && suggestionValue.term;
const isUriFromTermValid = suggestionTerm && !regexInvalidURI.test(suggestionTerm.substring(1));
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
const { results, loading } = useLighthouse(debouncedTerm, showMature, SEARCH_SIZE, additionalOptions, 0);
const stringifiedResults = JSON.stringify(results);
const shouldFilter = (uri, previous) => uri !== canonicalCreatorUri && (!previous || !previous.includes(uri));
const filteredCommentors = canonicalCommentors && canonicalCommentors.filter((uri) => shouldFilter(uri));
const filteredSubs = canonicalSubs && canonicalSubs.filter((uri) => shouldFilter(uri, filteredCommentors));
const shouldFilter = (uri, previousLists) =>
uri !== canonicalCreatorUri && (!previousLists || !previousLists.includes(uri));
const allOptions = [];
if (canonicalCreatorUri) allOptions.push(canonicalCreatorUri);
if (filteredSubs) allOptions.push(...filteredSubs);
if (filteredCommentors) allOptions.push(...filteredCommentors);
const filteredCommentors = canonicalCommentors.filter((uri) => shouldFilter(uri));
const filteredSubs = canonicalSubscriptions.filter((uri) => shouldFilter(uri, filteredCommentors));
const filteredTop = shouldFilter(topSuggestion, [...filteredCommentors, ...filteredSubs]) && topSuggestion;
const filteredSearch =
canonicalSearchUris &&
canonicalSearchUris.filter((uri) => shouldFilter(uri, [...filteredCommentors, ...filteredSubs, filteredTop || '']));
const allOptionsGrouped =
allOptions.length > 0
? allOptions.map((option) => {
const groupName =
(canonicalCreatorUri === option && __('Creator')) ||
(filteredSubs && filteredSubs.includes(option) && __('Following')) ||
(filteredCommentors && filteredCommentors.includes(option) && __('From comments'));
const creatorUriMatch = useSuggestionMatch(suggestionTerm || '', [canonicalCreatorUri]);
const subscriptionsMatch = useSuggestionMatch(suggestionTerm || '', filteredSubs);
const commentorsMatch = useSuggestionMatch(suggestionTerm || '', filteredCommentors);
return {
uri: option.replace('lbry://', '').replace('#', ':'),
group: groupName,
};
})
: [];
const hasMinSearchLength = suggestionTerm && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
const isTyping = suggestionValue && debouncedTerm !== suggestionValue.term;
const showPlaceholder = hasMinSearchLength && (isTyping || loading);
const allMatches = useSuggestionMatch(
suggestionTerm || '',
allOptionsGrouped.map(({ uri }) => uri)
);
/** --------- **/
/** Functions **/
/** --------- **/
function handleChange(e: SyntheticInputEvent<*>) {
onChange(e);
if (hideSuggestions) return;
const { value } = e.target;
function handleInputChange(value: string) {
onChange({ target: { value } });
const cursorIndex = inputRef && inputRef.current && inputRef.current.selectionStart;
const mentionMatches = value.match(mentionRegex);
@ -139,17 +131,24 @@ export default function TextareaWithSuggestions(props: Props) {
const handleSelect = React.useCallback(
(selectedValue: string) => {
setSelectedValue(selectedValue);
if (!suggestionValue) return;
const elem = inputRef && inputRef.current;
const newCursorPos = suggestionValue.index + selectedValue.length + 1;
const newValue =
commentValue.substring(0, suggestionValue.index) + // 1) From start of comment value until term start
`${selectedValue}` + // 2) Add the selected value
(commentValue.length > suggestionValue.lastIndex // 3) If there is more content until the the end of the comment value:
? commentValue.substring(suggestionValue.index + 1, commentValue.length) // 3.a) from term end, add the rest of comment value
? commentValue.substring(suggestionValue.lastIndex, commentValue.length) // 3.a) from term end, add the rest of comment value
: ' '); // 3.b) or else, add a space for new input after
onChange({ target: { value: newValue } });
inputRef.current.focus();
setSuggestionValue(undefined);
elem.focus();
elem.setSelectionRange(newCursorPos, newCursorPos);
},
[commentValue, inputRef, onChange, suggestionValue]
);
@ -158,132 +157,89 @@ export default function TextareaWithSuggestions(props: Props) {
/** Effects **/
/** ------- **/
// For disabling sending on Enter on Livestream chat
React.useEffect(() => {
const timer = setTimeout(() => {
if (isTyping && suggestionValue) setDebouncedTerm(!hasMinSearchLength ? '' : suggestionValue.term);
}, INPUT_DEBOUNCE_MS);
if (!isLivestream) return;
return () => clearTimeout(timer);
}, [hasMinSearchLength, isTyping, suggestionValue]);
React.useEffect(() => {
if (!stringifiedResults) return;
const arrayResults = JSON.parse(stringifiedResults);
if (doResolveUris && arrayResults && arrayResults.length > 0) {
// $FlowFixMe
doResolveUris(arrayResults)
.then((response) => {
try {
// $FlowFixMe
const canonical_urls = Object.values(response).map(({ canonical_url }) => canonical_url);
setCanonicalSearchUris(canonical_urls);
} catch (e) {}
})
.catch((e) => {});
if (suggestionTerm && inputRef) {
inputRef.current.setAttribute('term', suggestionTerm);
} else {
inputRef.current.removeAttribute('term');
}
}, [doResolveUris, stringifiedResults]);
}, [inputRef, isLivestream, suggestionTerm]);
// Only resolve commentors on Livestreams when actually mentioning/looking for it
// Only resolve commentors on Livestreams when first trying to mention/looking for it
React.useEffect(() => {
if (isLivestream && commentorUris && suggestionValue) doResolveUris(commentorUris);
}, [commentorUris, doResolveUris, isLivestream, suggestionValue]);
if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris);
}, [commentorUris, doResolveUris, isLivestream, suggestionTerm]);
// Allow selecting with TAB key
React.useEffect(() => {
if (!inputRef || !suggestionValue) return;
function handleKeyDown(e: SyntheticKeyboardEvent<*>) {
const { keyCode } = e;
const activeSelection = document.querySelector('[data-reach-combobox-option][data-highlighted]');
const firstValue = document.querySelectorAll('[data-reach-combobox-option] .textareaSuggestion__value')[0];
if (keyCode === KEYCODES.UP || keyCode === KEYCODES.DOWN) {
const selectedId = activeSelection && activeSelection.getAttribute('id');
const selectedItem = selectedId && document.querySelector(`li[id="${selectedId}"]`);
if (selectedItem) selectedItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
} else if (keyCode === KEYCODES.TAB) {
if (highlightedSuggestion && keyCode === KEYCODES.TAB) {
e.preventDefault();
const activeValue = document.querySelector(
'[data-reach-combobox-option][data-highlighted] .textareaSuggestion__value'
);
if (activeValue && activeValue.innerText) {
handleSelect(activeValue.innerText);
} else if (firstValue && firstValue.innerText) {
handleSelect(firstValue.innerText);
}
handleSelect(highlightedSuggestion.uri);
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleSelect, inputRef, suggestionValue]);
}, [handleSelect, highlightedSuggestion]);
/** ------ **/
/** Render **/
/** ------ **/
const suggestionsRow = (label: string, suggestions: any) => (
<div className="textareaSuggestions__row">
<div className="textareaSuggestions__label">{label}</div>
{suggestions.map((suggestion) => (
<TextareaSuggestionsItem key={suggestion} uri={suggestion} />
))}
const renderGroup = (group: string, children: any) => (
<div className="textareaSuggestions__group">
<label className="textareaSuggestions__label">{group}</label>
{children}
<hr className="textareaSuggestions__topSeparator" />
</div>
);
const renderInput = (params: any) => {
const { InputProps, disabled, fullWidth, id, inputProps: autocompleteInputProps } = params;
const inputProps = { ...autocompleteInputProps, ...inputDefaultProps };
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
return <TextField inputRef={inputRef} multiline select={false} {...autocompleteProps} />;
};
return (
<Combobox onSelect={handleSelect} aria-label={name}>
{/* Regular Textarea Field */}
<ComboboxInput
{...inputProps}
value={commentValue}
as="textarea"
id={name}
maxLength={maxLength}
onChange={(e) => handleChange(e)}
ref={inputRef}
selectOnClick
type={type}
autocomplete={false}
<Autocomplete
autoHighlight
disableClearable
filterOptions={(options) => options.filter(({ uri }) => allMatches && allMatches.includes(uri))}
freeSolo
fullWidth
getOptionLabel={(option) => option.uri}
groupBy={(option) => option.group}
id={id}
inputValue={commentValue}
loading={!allMatches || allMatches.length === 0}
loadingText={__('Nothing found')}
onBlur={() => onBlur()}
/* Different from onInputChange, onChange is only used for the selected value,
so here it is acting simply as a selection handler (see it as onSelect) */
onChange={(event, value) => handleSelect(value.uri)}
onClose={(event, reason) => reason !== 'selectOption' && setClose(true)}
onFocus={() => onFocus()}
onHighlightChange={(event, option) => setHighlightedSuggestion(option)}
onInputChange={(event, value, reason) => reason === 'input' && handleInputChange(value)}
onOpen={() => suggestionTerm && setClose(false)}
/* 'open' is for the popper box component, set to check for a valid term
or else it will be displayed all the time as empty */
open={!!suggestionTerm && !shouldClose}
options={allOptionsGrouped}
renderGroup={({ group, children }) => renderGroup(group, children)}
renderInput={(params) => renderInput(params)}
renderOption={(optionProps, option) => <TextareaSuggestionsItem uri={option.uri} {...optionProps} />}
value={selectedValue}
/>
{/* Possible Suggestions Box */}
{suggestionValue && isUriFromTermValid && (
<ComboboxPopover persistSelection className="textarea__suggestions">
<ComboboxList ref={comboboxListRef}>
{creatorUriMatch && creatorUriMatch.length > 0 && suggestionsRow(__('Creator'), creatorUriMatch)}
{subscriptionsMatch && subscriptionsMatch.length > 0 && suggestionsRow(__('Following'), subscriptionsMatch)}
{commentorsMatch && commentorsMatch.length > 0 && suggestionsRow(__('From comments'), commentorsMatch)}
{hasMinSearchLength &&
(showPlaceholder ? (
<Spinner type="small" />
) : (
results && (
<>
{!noTopSuggestion && (
<TextareaTopSuggestion
query={debouncedTerm}
filteredTop={filteredTop}
setTopSuggestion={setTopSuggestion}
/>
)}
{filteredSearch && filteredSearch.length > 0 && suggestionsRow(__('From search'), filteredSearch)}
</>
)
))}
</ComboboxList>
</ComboboxPopover>
)}
</Combobox>
);
}

View file

@ -11,7 +11,7 @@ import {
selectClaimIdForUri,
selectClaimIdsByUri,
} from 'redux/selectors/claims';
import { isClaimNsfw } from 'util/claim';
import { isClaimNsfw, getChannelFromClaim } from 'util/claim';
import { selectSubscriptionUris } from 'redux/selectors/subscriptions';
type State = { claims: any, comments: CommentsState };
@ -264,7 +264,7 @@ export const selectRepliesForParentId = createCachedSelector(
* @param filterInputs Values returned by filterCommentsDepOnList.
*/
const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs: any) => {
const filterProps = filterInputs.reduce(function (acc, cur, i) {
const filterProps = filterInputs.reduce((acc, cur, i) => {
acc[filterCommentsPropKeys[i]] = cur;
return acc;
}, {});
@ -392,18 +392,25 @@ export const selectSuperChatTotalAmountForUri = (state: State, uri: string) => {
};
export const selectChannelMentionData = createCachedSelector(
(state, uri) => uri,
selectClaimIdsByUri,
selectClaimsById,
selectTopLevelCommentsForUri,
selectSubscriptionUris,
(claimIdsByUri, claimsById, topLevelComments, subscriptionUris) => {
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris) => {
let canonicalCreatorUri;
const commentorUris = [];
const canonicalCommentors = [];
const canonicalSubscriptions = [];
topLevelComments.forEach((comment) => {
const uri = comment.channel_url;
if (uri) {
const claimId = claimIdsByUri[uri];
const claim = claimsById[claimId];
const channelFromClaim = claim && getChannelFromClaim(claim);
canonicalCreatorUri = channelFromClaim && channelFromClaim.canonical_url;
topLevelComments.forEach(({ channel_url: uri }) => {
// Check: if there are duplicate commentors
if (!commentorUris.includes(uri)) {
// Update: commentorUris
commentorUris.push(uri);
@ -416,6 +423,7 @@ export const selectChannelMentionData = createCachedSelector(
}
}
});
}
subscriptionUris.forEach((uri) => {
// Update: canonicalSubscriptions
@ -426,11 +434,6 @@ export const selectChannelMentionData = createCachedSelector(
}
});
return {
topLevelComments,
commentorUris,
canonicalCommentors,
canonicalSubscriptions,
};
return { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris };
}
)((state, uri, maxCount) => `${String(uri)}:${maxCount}`);

View file

@ -3,7 +3,7 @@
$thumbnailWidth: 1.5rem;
$thumbnailWidthSmall: 1rem;
.create___comment {
.create__comment {
position: relative;
}

View file

@ -1,16 +1,45 @@
.textarea__suggestions {
.MuiAutocomplete-inputRoot {
padding: 0 !important;
font-family: inherit !important;
font-weight: inherit !important;
font-size: inherit !important;
.MuiOutlinedInput-notchedOutline {
visibility: hidden;
}
.create__comment {
@extend textarea;
color: var(--color-text) !important;
min-height: calc(var(--height-input) * 1.5) !important;
}
}
.MuiAutocomplete-paper {
@extend .card;
background-color: var(--color-card-background);
max-height: 30vh;
overflow-y: scroll;
text-overflow: ellipsis;
box-shadow: var(--card-box-shadow);
color: var(--color-text) !important;
.textareaSuggestions__group {
&:last-child hr {
display: none;
}
.textareaSuggestions__label {
@extend .wunderbar__label;
}
.Mui-focused {
background-color: var(--color-menu-background--active);
}
}
> .icon {
top: 0;
left: var(--spacing-m);
height: 100%;
position: absolute;
z-index: 1;
stroke: var(--color-input-placeholder);
}
@ -18,61 +47,42 @@
@media (min-width: $breakpoint-small) {
padding: 0;
}
.textareaSuggestions__label:first-of-type {
margin-top: var(--spacing-xs);
}
}
.textareaSuggestion {
.MuiAutocomplete-option {
display: flex;
align-items: center;
padding: 0 var(--spacing-xxs);
margin-left: var(--spacing-xxs);
margin: 0 var(--spacing-xxs);
overflow: hidden;
text-overflow: ellipsis;
border-radius: var(--border-radius);
.channel-thumbnail {
@include handleChannelGif(2.1rem);
position: absolute;
margin-right: 0;
@media (min-width: $breakpoint-small) {
@include handleChannelGif(2.1rem);
}
}
}
.textareaSuggestions__row {
&:last-child hr {
display: none;
}
}
.textareaSuggestions__topSeparator {
@extend .wunderbar__top-separator;
}
.textareaSuggestion__label {
@extend .wunderbar__suggestion-label;
margin-left: var(--spacing-m);
display: block;
position: relative;
.textareaSuggestion__title {
@extend .wunderbar__suggestion-title;
}
.textareaSuggestions__label {
@extend .wunderbar__label;
.textareaSuggestion__value {
@extend .wunderbar__suggestion-name;
}
}
}
.textareaSuggestions__topSeparator {
@extend .wunderbar__top-separator;
}
.textareaSuggestion__value {
@extend .wunderbar__suggestion-name;
margin-left: calc(var(--spacing-l) - var(--spacing-xxs));
}
.textareaSuggestion__title {
@extend .wunderbar__suggestion-title;
margin-left: calc(var(--spacing-l) - var(--spacing-xxs));
}