Visual improvements to web autocomplete (#591)

* Visual improvements to web autocomplete

* Remove stray styling
zio/stable
Ollie H 2023-05-08 14:09:15 -07:00 committed by GitHub
parent 84046f42d5
commit cdfb1c7abf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 64 deletions

View File

@ -57,14 +57,6 @@
} }
}*/ }*/
/* OLLIE: TODO -- this is not accessible */
/* Remove focus state on inputs */
.ProseMirror-focused {
outline: 0;
}
input:focus {
outline: 0;
}
/* Remove default link styling */ /* Remove default link styling */
a { a {
color: inherit; color: inherit;
@ -106,28 +98,16 @@
color: #0085ff; color: #0085ff;
cursor: pointer; cursor: pointer;
} }
/* OLLIE: TODO -- this is not accessible */
/* Remove focus state on inputs */
.ProseMirror-focused {
outline: 0;
}
input:focus {
outline: 0;
}
.tippy-content .items { .tippy-content .items {
border-radius: 6px; width: fit-content;
background: #F3F3F8;
border: 1px solid #e0d9d9;
padding: 3px 3px;
}
.tippy-content .items .item {
display: block;
background: transparent;
color: #8a8c9a;
border: 0;
font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
padding: 7px 10px 8px;
width: 100%;
text-align: left;
box-sizing: border-box;
letter-spacing: 0.2px;
}
.tippy-content .items .item.is-selected {
background: #fff;
border-radius: 4px;
color: #333;
} }
</style> </style>
{% include "scripts.html" %} {% include "scripts.html" %}

View File

@ -1,9 +1,12 @@
import React, { import React, {
forwardRef, forwardRef,
useCallback,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo,
useState, useState,
} from 'react' } from 'react'
import {StyleSheet, View} from 'react-native'
import {ReactRenderer} from '@tiptap/react' import {ReactRenderer} from '@tiptap/react'
import tippy, {Instance as TippyInstance} from 'tippy.js' import tippy, {Instance as TippyInstance} from 'tippy.js'
import { import {
@ -12,6 +15,10 @@ import {
SuggestionKeyDownProps, SuggestionKeyDownProps,
} from '@tiptap/suggestion' } from '@tiptap/suggestion'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {usePalette} from 'lib/hooks/usePalette'
import Graphemer from 'graphemer'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
interface MentionListRef { interface MentionListRef {
onKeyDown: (props: SuggestionKeyDownProps) => boolean onKeyDown: (props: SuggestionKeyDownProps) => boolean
@ -26,7 +33,7 @@ export function createSuggestion({
async items({query}) { async items({query}) {
autocompleteView.setActive(true) autocompleteView.setActive(true)
await autocompleteView.setPrefix(query) await autocompleteView.setPrefix(query)
return autocompleteView.suggestions.slice(0, 8).map(s => s.handle) return autocompleteView.suggestions.slice(0, 8)
}, },
render: () => { render: () => {
@ -91,12 +98,14 @@ export function createSuggestion({
const MentionList = forwardRef<MentionListRef, SuggestionProps>( const MentionList = forwardRef<MentionListRef, SuggestionProps>(
(props: SuggestionProps, ref) => { (props: SuggestionProps, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(0)
const pal = usePalette('default')
const splitter = useMemo(() => new Graphemer(), [])
const selectItem = (index: number) => { const selectItem = (index: number) => {
const item = props.items[index] const item = props.items[index]
if (item) { if (item) {
props.command({id: item}) props.command({id: item.handle})
} }
} }
@ -137,21 +146,106 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
}, },
})) }))
const {items} = props
const getDisplayedName = useCallback(
(name: string) => {
// Heuristic value based on max display name and handle lengths
const DISPLAY_LIMIT = 30
if (name.length > DISPLAY_LIMIT) {
const graphemes = splitter.splitGraphemes(name)
if (graphemes.length > DISPLAY_LIMIT) {
return graphemes.length > DISPLAY_LIMIT
? `${graphemes.slice(0, DISPLAY_LIMIT).join('')}...`
: name.substring(0, DISPLAY_LIMIT)
}
}
return name
},
[splitter],
)
return ( return (
<div className="items"> <div className="items">
{props.items.length ? ( <View style={[pal.borderDark, pal.view, styles.container]}>
props.items.map((item, index) => ( {items.length > 0 ? (
<button items.map((item, index) => {
className={`item ${index === selectedIndex ? 'is-selected' : ''}`} const displayName = getDisplayedName(
key={index} item.displayName ?? item.handle,
onClick={() => selectItem(index)}> )
{item} const isSelected = selectedIndex === index
</button>
)) return (
) : ( <View
<div className="item">No result</div> key={item.handle}
)} style={[
isSelected ? pal.viewLight : undefined,
pal.borderDark,
styles.mentionContainer,
index === 0
? styles.firstMention
: index === items.length - 1
? styles.lastMention
: undefined,
]}>
<View style={styles.avatarAndDisplayName}>
<UserAvatar avatar={item.avatar ?? null} size={26} />
<Text style={pal.text} numberOfLines={1}>
{displayName}
</Text>
</View>
<Text type="xs" style={pal.textLight} numberOfLines={1}>
{item.handle}
</Text>
</View>
)
})
) : (
<Text type="sm" style={[pal.text, styles.noResult]}>
No result
</Text>
)}
</View>
</div> </div>
) )
}, },
) )
const styles = StyleSheet.create({
container: {
width: 500,
borderRadius: 6,
borderWidth: 1,
borderStyle: 'solid',
padding: 4,
},
mentionContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
paddingHorizontal: 12,
paddingVertical: 8,
gap: 4,
},
firstMention: {
borderTopLeftRadius: 2,
borderTopRightRadius: 2,
},
lastMention: {
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
},
avatarAndDisplayName: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
noResult: {
paddingHorizontal: 12,
paddingVertical: 8,
},
})

View File

@ -110,27 +110,7 @@
outline: 0; outline: 0;
} }
.tippy-content .items { .tippy-content .items {
border-radius: 6px; width: fit-content;
background: #F3F3F8;
border: 1px solid #e0d9d9;
padding: 3px 3px;
}
.tippy-content .items .item {
display: block;
background: transparent;
color: #8a8c9a;
border: 0;
font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
padding: 7px 10px 8px;
width: 100%;
text-align: left;
box-sizing: border-box;
letter-spacing: 0.2px;
}
.tippy-content .items .item.is-selected {
background: #fff;
border-radius: 4px;
color: #333;
} }
</style> </style>
</head> </head>