1
- import { UserIcon } from '@sanity/icons'
2
1
import {
2
+ AvatarStack ,
3
3
Box ,
4
+ // eslint-disable-next-line no-restricted-imports
5
+ Button as UIButton ,
4
6
Checkbox ,
5
- Container ,
6
7
Flex ,
7
8
Menu ,
8
- MenuDivider ,
9
9
// eslint-disable-next-line no-restricted-imports
10
10
MenuItem ,
11
11
Text ,
12
- TextInput ,
13
12
} from '@sanity/ui'
14
- import {
15
- type ChangeEvent ,
16
- type KeyboardEvent ,
17
- type MouseEvent ,
18
- useCallback ,
19
- useMemo ,
20
- useRef ,
21
- useState ,
22
- } from 'react'
23
- import { LoadingBlock , type UserWithPermission , useTranslation } from 'sanity'
13
+ import { AnimatePresence , motion } from 'framer-motion'
14
+ import { type MouseEvent , useCallback , useMemo , useState } from 'react'
15
+ import { type UserWithPermission , useTranslation } from 'sanity'
24
16
import { styled } from 'styled-components'
25
17
26
18
import { MenuButton } from '../../../../../ui-components'
27
19
import { tasksLocaleNamespace } from '../../../../i18n'
28
20
import { useMentionUser } from '../../context'
29
- import { useFilteredOptions } from '../form/fields/assignee/useFilteredOptions '
21
+ import { SearchUsersMenu } from '../searchUsersMenu/SearchUsersMenu '
30
22
import { TasksUserAvatar } from '../TasksUserAvatar'
31
23
32
24
type SelectItemHandler = ( id : string ) => void
33
25
34
- function MentionUserMenuItem ( props : {
26
+ function SubscriberUserMenuItem ( props : {
35
27
user : UserWithPermission
36
28
onSelect : SelectItemHandler
37
29
selected : boolean
@@ -41,6 +33,7 @@ function MentionUserMenuItem(props: {
41
33
42
34
const handleCheckboxClick = useCallback (
43
35
( e : MouseEvent < HTMLDivElement > ) => {
36
+ // Stops propagation to avoid closing the menu. When clicking the checkbox we want to keep the menu open.
44
37
e . stopPropagation ( )
45
38
handleSelect ( )
46
39
} ,
@@ -68,53 +61,40 @@ const StyledMenu = styled(Menu)`
68
61
width: 308px;
69
62
border-radius: 3px;
70
63
`
64
+ interface TasksSubscriberMenuProps {
65
+ value ?: string [ ]
66
+ handleUserSubscriptionChange : ( userId : string ) => void
67
+ }
71
68
72
- const IGNORED_KEYS = [
73
- 'Control' ,
74
- 'Shift' ,
75
- 'Alt' ,
76
- 'Enter' ,
77
- 'Home' ,
78
- 'End' ,
79
- 'PageUp' ,
80
- 'PageDown' ,
81
- 'Meta' ,
82
- 'Tab' ,
83
- 'CapsLock' ,
84
- ]
69
+ export function TasksSubscribersMenu ( props : TasksSubscriberMenuProps ) {
70
+ const { value = [ ] , handleUserSubscriptionChange} = props
85
71
86
- function TasksSubscribers ( { onSelect, value = [ ] } : { onSelect : SelectItemHandler ; value ?: string [ ] } ) {
87
- const [ searchTerm , setSearchTerm ] = useState < string > ( '' )
72
+ const onSelect = useCallback (
73
+ ( userId : string ) => handleUserSubscriptionChange ( userId ) ,
74
+ [ handleUserSubscriptionChange ] ,
75
+ )
76
+
77
+ const { t} = useTranslation ( tasksLocaleNamespace )
88
78
const { mentionOptions} = useMentionUser ( )
89
- const inputRef = useRef < HTMLInputElement | null > ( null )
90
79
// This list will keep a local state of users who are initially subscribed and later added or removed.
91
- // To always render them at the top
80
+ // rendering them always at the top.
92
81
const [ subscribersList , setSubscribersList ] = useState ( value )
93
82
94
- const handleSearchChange = useCallback ( ( event : ChangeEvent < HTMLInputElement > ) => {
95
- setSearchTerm ( event . currentTarget . value )
96
- } , [ ] )
97
-
98
- const filteredOptions = useFilteredOptions ( { options : mentionOptions . data || [ ] , searchTerm} )
99
-
100
- const selectedUsers = useMemo (
101
- ( ) => filteredOptions . filter ( ( user ) => subscribersList . includes ( user . id ) ) ,
102
- [ filteredOptions , subscribersList ] ,
103
- )
104
-
105
83
const handleSelect = useCallback (
106
84
( id : string ) => {
107
85
if ( ! subscribersList . includes ( id ) ) {
86
+ // Persist user id in local subscribers list state.
108
87
setSubscribersList ( [ ...subscribersList , id ] )
109
88
}
110
89
onSelect ( id )
111
90
} ,
112
91
[ subscribersList , onSelect ] ,
113
92
)
93
+
114
94
const renderItem = useCallback (
115
95
( user : UserWithPermission ) => {
116
96
return (
117
- < MentionUserMenuItem
97
+ < SubscriberUserMenuItem
118
98
user = { user }
119
99
onSelect = { handleSelect }
120
100
key = { user . id }
@@ -124,79 +104,52 @@ function TasksSubscribers({onSelect, value = []}: {onSelect: SelectItemHandler;
124
104
} ,
125
105
[ handleSelect , value ] ,
126
106
)
127
- const handleKeyDown = useCallback ( ( event : KeyboardEvent < HTMLElement > ) => {
128
- // If target is input don't do anything
129
- if ( event . target === inputRef . current ) {
130
- return
131
- }
132
-
133
- if ( ! IGNORED_KEYS . includes ( event . key ) ) {
134
- inputRef . current ?. focus ( )
135
- }
136
- } , [ ] )
137
-
138
- const { t} = useTranslation ( tasksLocaleNamespace )
139
-
140
- if ( mentionOptions . loading ) {
141
- return (
142
- < Container width = { 0 } >
143
- < LoadingBlock showText />
144
- </ Container >
145
- )
146
- }
147
-
148
- return (
149
- < div onKeyDown = { handleKeyDown } style = { { maxHeight : '360px' , width : '100%' } } >
150
- < Box paddingBottom = { 2 } >
151
- < TextInput
152
- placeholder = { t ( 'form.subscribers.menu.input.placeholder' ) }
153
- autoFocus
154
- border = { false }
155
- onChange = { handleSearchChange }
156
- value = { searchTerm }
157
- fontSize = { 1 }
158
- icon = { UserIcon }
159
- ref = { inputRef }
160
- />
161
- </ Box >
162
107
163
- { filteredOptions . length === 0 ? (
164
- < Box padding = { 3 } >
165
- < Text align = "center" size = { 1 } muted >
166
- { t ( 'form.input.assignee.search.no-users.text' ) }
167
- </ Text >
168
- </ Box >
169
- ) : (
170
- < >
171
- { ! searchTerm && selectedUsers . length > 0 && (
172
- < >
173
- { selectedUsers . map ( renderItem ) }
174
- < Box paddingY = { 2 } >
175
- < MenuDivider />
176
- </ Box >
177
- </ >
178
- ) }
179
- { filteredOptions . map ( renderItem ) }
180
- </ >
181
- ) }
182
- </ div >
108
+ const selectedUsers = useMemo (
109
+ ( ) => mentionOptions . data ?. filter ( ( user ) => subscribersList . includes ( user . id ) ) ,
110
+ [ mentionOptions , subscribersList ] ,
183
111
)
184
- }
185
-
186
- export function TasksSubscribersMenu ( props : {
187
- onSelect : ( userId : string ) => void
188
- menuButton : React . ReactElement
189
- value ?: string [ ]
190
- } ) {
191
- const { onSelect, menuButton, value} = props
192
112
193
113
return (
194
114
< MenuButton
195
- button = { menuButton }
115
+ button = {
116
+ < UIButton type = "button" mode = "bleed" padding = { 1 } >
117
+ { value . length > 0 ? (
118
+ < AnimatePresence initial = { false } >
119
+ < AvatarStack maxLength = { 3 } size = { 0 } >
120
+ { value . map ( ( subscriberId ) => (
121
+ < motion . div
122
+ key = { subscriberId }
123
+ exit = { { opacity : 0 , translateX : '2px' , scale : 0.9 } }
124
+ animate = { {
125
+ opacity : 1 ,
126
+ translateX : 0 ,
127
+ scale : 1 ,
128
+ transition : { type : 'just' , duration : 0.2 } ,
129
+ } }
130
+ initial = { { opacity : 0 , translateX : '2px' , scale : 0.9 } }
131
+ >
132
+ < TasksUserAvatar user = { { id : subscriberId } } size = { 0 } />
133
+ </ motion . div >
134
+ ) ) }
135
+ </ AvatarStack >
136
+ </ AnimatePresence >
137
+ ) : (
138
+ < TasksUserAvatar size = { 0 } />
139
+ ) }
140
+ </ UIButton >
141
+ }
196
142
id = "assign-user-menu"
197
143
menu = {
198
144
< StyledMenu >
199
- < TasksSubscribers onSelect = { onSelect } value = { value } />
145
+ < SearchUsersMenu
146
+ renderItem = { renderItem }
147
+ selectedUsers = { selectedUsers }
148
+ loading = { mentionOptions . loading }
149
+ options = { mentionOptions . data || [ ] }
150
+ name = "subscribersSearch"
151
+ placeholder = { t ( 'form.subscribers.menu.input.placeholder' ) }
152
+ />
200
153
</ StyledMenu >
201
154
}
202
155
popover = { {
0 commit comments