Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 2bc204b

Browse files
committed
utilizes existing useFocus hook, fixes TypeScript issues in strict mode, outsources selection & suggestion render function to separate components
1 parent 1028f03 commit 2bc204b

File tree

4 files changed

+149
-90
lines changed

4 files changed

+149
-90
lines changed

res/css/structures/_AutocompleteInput.pcss

+24-11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
117
.mx_AutocompleteInput {
218
position: relative;
319
}
@@ -59,24 +75,21 @@
5975
transition: border-color 0.25s;
6076
border: 1px solid $input-border-color;
6177

62-
> input[type="text"] {
63-
margin: 6px 0 !important;
64-
height: 24px;
78+
> input {
79+
flex: 1;
80+
height: $font-24px;
6581
line-height: $font-24px;
66-
font-size: $font-14px;
67-
padding-left: $spacing-12;
68-
border: 0 !important;
69-
outline: 0 !important;
70-
resize: none;
71-
box-sizing: border-box;
7282
min-width: 40%;
73-
flex: 1 !important;
83+
resize: none;
84+
// `!important` is required to bypass global input styles.
85+
margin: 6px 0 !important;
86+
border-color: transparent !important;
7487
color: $primary-content !important;
88+
font-weight: normal !important;
7589

7690
&::placeholder {
7791
color: $primary-content !important;
7892
font-weight: normal !important;
79-
font-size: 1.4rem;
8093
}
8194
}
8295
}

src/components/structures/AutocompleteInput.tsx

+105-77
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { useState, ReactNode, ChangeEvent, KeyboardEvent, useRef } from 'react';
17+
import React, { useState, ReactNode, ChangeEvent, KeyboardEvent, useRef, ReactElement } from 'react';
1818
import classNames from 'classnames';
1919

2020
import Autocompleter from "../../autocomplete/AutocompleteProvider";
@@ -23,15 +23,16 @@ import { ICompletion } from '../../autocomplete/Autocompleter';
2323
import AccessibleButton from '../../components/views/elements/AccessibleButton';
2424
import { Icon as PillRemoveIcon } from '../../../res/img/icon-pill-remove.svg';
2525
import { Icon as CheckmarkIcon } from '../../../res/img/element-icons/roomlist/checkmark.svg';
26+
import useFocus from "../../hooks/useFocus";
2627

2728
interface AutocompleteInputProps {
2829
provider: Autocompleter;
2930
placeholder: string;
3031
selection: ICompletion[];
3132
onSelectionChange: (selection: ICompletion[]) => void;
3233
maxSuggestions?: number;
33-
renderSuggestion?: (s: ICompletion) => ReactNode;
34-
renderSelection?: (m: ICompletion) => ReactNode;
34+
renderSuggestion?: (s: ICompletion) => ReactElement;
35+
renderSelection?: (m: ICompletion) => ReactElement;
3536
additionalFilter?: (suggestion: ICompletion) => boolean;
3637
}
3738

@@ -47,9 +48,9 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
4748
}) => {
4849
const [query, setQuery] = useState<string>('');
4950
const [suggestions, setSuggestions] = useState<ICompletion[]>([]);
50-
const [isFocused, setFocused] = useState<boolean>(false);
51-
const editorContainerRef = useRef<HTMLDivElement>();
52-
const editorRef = useRef<HTMLInputElement>();
51+
const [isFocused, onFocusChangeHandlerFunctions] = useFocus();
52+
const editorContainerRef = useRef<HTMLDivElement>(null);
53+
const editorRef = useRef<HTMLInputElement>(null);
5354

5455
const focusEditor = () => {
5556
editorRef?.current?.focus();
@@ -111,72 +112,6 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
111112
}
112113
};
113114

114-
const _renderSuggestion = (completion: ICompletion): ReactNode => {
115-
const isSelected = selection.findIndex(selection => selection.completionId === completion.completionId) >= 0;
116-
const classes = classNames({
117-
'mx_AutocompleteInput_suggestion': true,
118-
'mx_AutocompleteInput_suggestion--selected': isSelected,
119-
});
120-
121-
const withContainer = (children: ReactNode): ReactNode => (
122-
<div className={classes}
123-
onMouseDown={(e) => {
124-
e.preventDefault();
125-
e.stopPropagation();
126-
127-
toggleSelection(completion);
128-
}}
129-
key={completion.completionId}
130-
data-testid={`autocomplete-suggestion-item-${completion.completionId}`}
131-
>
132-
<div>
133-
{ children }
134-
</div>
135-
{ isSelected && <CheckmarkIcon height={16} width={16} /> }
136-
</div>
137-
);
138-
139-
if (renderSuggestion) {
140-
return withContainer(renderSuggestion(completion));
141-
}
142-
143-
return withContainer(
144-
<>
145-
<span className='mx_AutocompleteInput_suggestion_title'>{ completion.completion }</span>
146-
<span className='mx_AutocompleteInput_suggestion_description'>{ completion.completionId }</span>
147-
</>,
148-
);
149-
};
150-
151-
const _renderSelection = (s: ICompletion): ReactNode => {
152-
const withContainer = (children: ReactNode): ReactNode => (
153-
<span
154-
className='mx_AutocompleteInput_editor_selection'
155-
key={s.completionId}
156-
data-testid={`autocomplete-selection-item-${s.completionId}`}
157-
>
158-
<span className='mx_AutocompleteInput_editor_selection_pill'>
159-
{ children }
160-
</span>
161-
<AccessibleButton
162-
className='mx_AutocompleteInput_editor_selection_remove'
163-
onClick={() => removeSelection(s)}
164-
data-testid={`autocomplete-selection-remove-button-${s.completionId}`}
165-
>
166-
<PillRemoveIcon width={8} height={8} />
167-
</AccessibleButton>
168-
</span>
169-
);
170-
171-
if (renderSelection) {
172-
return withContainer(renderSelection(s));
173-
}
174-
175-
return withContainer(
176-
<span className='mx_AutocompleteInput_editor_selection_text'>{ s.completion }</span>,
177-
);
178-
};
179-
180115
const hasPlaceholder = (): boolean => selection.length === 0 && query.length === 0;
181116

182117
return (
@@ -187,18 +122,26 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
187122
onClick={onClickInputArea}
188123
data-testid="autocomplete-editor"
189124
>
190-
{ selection.map(s => _renderSelection(s)) }
125+
{
126+
selection.map(item => (
127+
<SelectionItem
128+
key={item.completionId}
129+
item={item}
130+
onClick={removeSelection}
131+
render={renderSelection}
132+
/>
133+
))
134+
}
191135
<input
192136
ref={editorRef}
193137
type="text"
194138
onKeyDown={onKeyDown}
195139
onChange={onQueryChange}
196140
value={query}
197141
autoComplete="off"
198-
placeholder={hasPlaceholder() ? placeholder : null}
199-
onFocus={() => setFocused(true)}
200-
onBlur={() => setFocused(false)}
142+
placeholder={hasPlaceholder() ? placeholder : undefined}
201143
data-testid="autocomplete-input"
144+
{...onFocusChangeHandlerFunctions}
202145
/>
203146
</div>
204147
{
@@ -209,11 +152,96 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
209152
data-testid="autocomplete-matches"
210153
>
211154
{
212-
suggestions.map((s) => _renderSuggestion(s))
155+
suggestions.map((item) => (
156+
<SuggestionItem
157+
key={item.completionId}
158+
item={item}
159+
selection={selection}
160+
onClick={toggleSelection}
161+
render={renderSuggestion}
162+
/>
163+
))
213164
}
214165
</div>
215166
) : null
216167
}
217168
</div>
218169
);
219170
};
171+
172+
type SelectionItemProps = {
173+
item: ICompletion;
174+
onClick: (completion: ICompletion) => void;
175+
render?: (completion: ICompletion) => ReactElement;
176+
};
177+
178+
const SelectionItem: React.FC<SelectionItemProps> = ({ item, onClick, render }) => {
179+
const withContainer = (children: ReactNode): ReactElement => (
180+
<span
181+
className='mx_AutocompleteInput_editor_selection'
182+
data-testid={`autocomplete-selection-item-${item.completionId}`}
183+
>
184+
<span className='mx_AutocompleteInput_editor_selection_pill'>
185+
{ children }
186+
</span>
187+
<AccessibleButton
188+
className='mx_AutocompleteInput_editor_selection_remove'
189+
onClick={() => onClick(item)}
190+
data-testid={`autocomplete-selection-remove-button-${item.completionId}`}
191+
>
192+
<PillRemoveIcon width={8} height={8} />
193+
</AccessibleButton>
194+
</span>
195+
);
196+
197+
if (render) {
198+
return withContainer(render(item));
199+
}
200+
201+
return withContainer(
202+
<span className='mx_AutocompleteInput_editor_selection_text'>{ item.completion }</span>,
203+
);
204+
};
205+
206+
type SuggestionItemProps = {
207+
item: ICompletion;
208+
selection: ICompletion[];
209+
onClick: (completion: ICompletion) => void;
210+
render?: (completion: ICompletion) => ReactElement;
211+
};
212+
213+
const SuggestionItem: React.FC<SuggestionItemProps> = ({ item, selection, onClick, render }) => {
214+
const isSelected = selection.some(selection => selection.completionId === item.completionId);
215+
const classes = classNames({
216+
'mx_AutocompleteInput_suggestion': true,
217+
'mx_AutocompleteInput_suggestion--selected': isSelected,
218+
});
219+
220+
const withContainer = (children: ReactNode): ReactElement => (
221+
<div
222+
className={classes}
223+
// `onClick` cannot be used here as it would lead to focus loss and closing the suggestion list.
224+
onMouseDown={(event) => {
225+
event.preventDefault();
226+
onClick(item);
227+
}}
228+
data-testid={`autocomplete-suggestion-item-${item.completionId}`}
229+
>
230+
<div>
231+
{ children }
232+
</div>
233+
{ isSelected && <CheckmarkIcon height={16} width={16} /> }
234+
</div>
235+
);
236+
237+
if (render) {
238+
return withContainer(render(item));
239+
}
240+
241+
return withContainer(
242+
<>
243+
<span className='mx_AutocompleteInput_suggestion_title'>{ item.completion }</span>
244+
<span className='mx_AutocompleteInput_suggestion_description'>{ item.completionId }</span>
245+
</>,
246+
);
247+
};

src/components/views/settings/AddPrivilegedUsers.tsx

+15-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,19 @@ export const AddPrivilegedUsers: React.FC<AddPrivilegedUsersProps> = ({ room, de
4141
const [powerLevel, setPowerLevel] = useState<number>(defaultUserLevel);
4242
const [selectedUsers, setSelectedUsers] = useState<ICompletion[]>([]);
4343
const filterSuggestions = useCallback(
44-
(user: ICompletion) => room.getMember(user.completionId)?.powerLevel <= defaultUserLevel,
44+
(user: ICompletion) => {
45+
if (user.completionId === undefined) {
46+
return false;
47+
}
48+
49+
const member = room.getMember(user.completionId);
50+
51+
if (member === null) {
52+
return false;
53+
}
54+
55+
return member.powerLevel <= defaultUserLevel;
56+
},
4557
[room, defaultUserLevel],
4658
);
4759

@@ -53,6 +65,8 @@ export const AddPrivilegedUsers: React.FC<AddPrivilegedUsersProps> = ({ room, de
5365
const powerLevelEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
5466

5567
try {
68+
// TODO: Remove @ts-ignore as soon as https://github.com/matrix-org/matrix-js-sdk/pull/2892 is merged.
69+
// @ts-ignore
5670
await client.setPowerLevel(room.roomId, userIds, powerLevel, powerLevelEvent);
5771
setSelectedUsers([]);
5872
setPowerLevel(defaultUserLevel);

src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,11 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
470470
<div className="mx_SettingsTab mx_RolesRoomSettingsTab">
471471
<div className="mx_SettingsTab_heading">{ _t("Roles & Permissions") }</div>
472472
{ privilegedUsersSection }
473-
{ canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} /> }
473+
{
474+
(canChangeLevels && room !== null) && (
475+
<AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />
476+
)
477+
}
474478
{ mutedUsersSection }
475479
{ bannedUsersSection }
476480
<SettingsFieldset

0 commit comments

Comments
 (0)