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

Commit 95ac957

Browse files
authored
add-privileged-users-in-room (#9596)
1 parent 982c83d commit 95ac957

File tree

10 files changed

+927
-3
lines changed

10 files changed

+927
-3
lines changed

res/css/_components.pcss

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
@import "./components/views/typography/_Caption.pcss";
4747
@import "./compound/_Icon.pcss";
4848
@import "./structures/_AutoHideScrollbar.pcss";
49+
@import "./structures/_AutocompleteInput.pcss";
4950
@import "./structures/_BackdropPanel.pcss";
5051
@import "./structures/_CompatibilityPage.pcss";
5152
@import "./structures/_ContextualMenu.pcss";
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
17+
.mx_AutocompleteInput {
18+
position: relative;
19+
}
20+
21+
.mx_AutocompleteInput_search_icon {
22+
margin-left: $spacing-8;
23+
fill: $secondary-content;
24+
}
25+
26+
.mx_AutocompleteInput_editor {
27+
flex: 1;
28+
display: flex;
29+
flex-wrap: wrap;
30+
align-items: center;
31+
overflow-x: hidden;
32+
overflow-y: auto;
33+
border: 1px solid $input-border-color;
34+
border-radius: 4px;
35+
transition: border-color 0.25s;
36+
37+
> input {
38+
flex: 1;
39+
min-width: 40%;
40+
resize: none;
41+
// `!important` is required to bypass global input styles.
42+
margin: 0 !important;
43+
padding: $spacing-8 9px;
44+
border: none !important;
45+
color: $primary-content !important;
46+
font-weight: normal !important;
47+
48+
&::placeholder {
49+
color: $primary-content !important;
50+
font-weight: normal !important;
51+
}
52+
}
53+
}
54+
55+
.mx_AutocompleteInput_editor--focused {
56+
border-color: $links;
57+
}
58+
59+
.mx_AutocompleteInput_editor--has-suggestions {
60+
border-bottom-left-radius: 0;
61+
border-bottom-right-radius: 0;
62+
}
63+
64+
.mx_AutocompleteInput_editor_selection {
65+
display: flex;
66+
margin-left: $spacing-8;
67+
}
68+
69+
.mx_AutocompleteInput_editor_selection_pill {
70+
display: flex;
71+
align-items: center;
72+
border-radius: 12px;
73+
padding-left: $spacing-8;
74+
padding-right: $spacing-8;
75+
background-color: $username-variant1-color;
76+
color: #ffffff;
77+
font-size: $font-12px;
78+
}
79+
80+
.mx_AutocompleteInput_editor_selection_remove_button {
81+
padding: 0 $spacing-4;
82+
}
83+
84+
.mx_AutocompleteInput_matches {
85+
position: absolute;
86+
left: 0;
87+
right: 0;
88+
background-color: $background;
89+
border: 1px solid $links;
90+
border-top-color: $input-border-color;
91+
border-bottom-left-radius: 4px;
92+
border-bottom-right-radius: 4px;
93+
z-index: 1000;
94+
}
95+
96+
.mx_AutocompleteInput_suggestion {
97+
display: flex;
98+
align-items: center;
99+
padding: $spacing-8;
100+
cursor: pointer;
101+
102+
> * {
103+
user-select: none;
104+
}
105+
106+
&:hover {
107+
background-color: $quinary-content;
108+
border-bottom-left-radius: 4px;
109+
border-bottom-right-radius: 4px;
110+
}
111+
}
112+
113+
.mx_AutocompleteInput_suggestion--selected {
114+
background-color: $quinary-content;
115+
116+
&:last-child {
117+
border-bottom-left-radius: 4px;
118+
border-bottom-right-radius: 4px;
119+
}
120+
}
121+
122+
.mx_AutocompleteInput_suggestion_title {
123+
margin-right: $spacing-8;
124+
}
125+
126+
.mx_AutocompleteInput_suggestion_description {
127+
color: $secondary-content;
128+
font-size: $font-12px;
129+
}
+1-1
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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+
17+
import React, { useState, ReactNode, ChangeEvent, KeyboardEvent, useRef, ReactElement } from 'react';
18+
import classNames from 'classnames';
19+
20+
import Autocompleter from "../../autocomplete/AutocompleteProvider";
21+
import { Key } from '../../Keyboard';
22+
import { ICompletion } from '../../autocomplete/Autocompleter';
23+
import AccessibleButton from '../../components/views/elements/AccessibleButton';
24+
import { Icon as PillRemoveIcon } from '../../../res/img/icon-pill-remove.svg';
25+
import { Icon as SearchIcon } from '../../../res/img/element-icons/roomlist/search.svg';
26+
import useFocus from "../../hooks/useFocus";
27+
28+
interface AutocompleteInputProps {
29+
provider: Autocompleter;
30+
placeholder: string;
31+
selection: ICompletion[];
32+
onSelectionChange: (selection: ICompletion[]) => void;
33+
maxSuggestions?: number;
34+
renderSuggestion?: (s: ICompletion) => ReactElement;
35+
renderSelection?: (m: ICompletion) => ReactElement;
36+
additionalFilter?: (suggestion: ICompletion) => boolean;
37+
}
38+
39+
export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
40+
provider,
41+
renderSuggestion,
42+
renderSelection,
43+
maxSuggestions = 5,
44+
placeholder,
45+
onSelectionChange,
46+
selection,
47+
additionalFilter,
48+
}) => {
49+
const [query, setQuery] = useState<string>('');
50+
const [suggestions, setSuggestions] = useState<ICompletion[]>([]);
51+
const [isFocused, onFocusChangeHandlerFunctions] = useFocus();
52+
const editorContainerRef = useRef<HTMLDivElement>(null);
53+
const editorRef = useRef<HTMLInputElement>(null);
54+
55+
const focusEditor = () => {
56+
editorRef?.current?.focus();
57+
};
58+
59+
const onQueryChange = async (e: ChangeEvent<HTMLInputElement>) => {
60+
const value = e.target.value.trim();
61+
setQuery(value);
62+
63+
let matches = await provider.getCompletions(
64+
query,
65+
{ start: query.length, end: query.length },
66+
true,
67+
maxSuggestions,
68+
);
69+
70+
if (additionalFilter) {
71+
matches = matches.filter(additionalFilter);
72+
}
73+
74+
setSuggestions(matches);
75+
};
76+
77+
const onClickInputArea = () => {
78+
focusEditor();
79+
};
80+
81+
const onKeyDown = (e: KeyboardEvent) => {
82+
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
83+
84+
// when the field is empty and the user hits backspace remove the right-most target
85+
if (!query && selection.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
86+
removeSelection(selection[selection.length - 1]);
87+
}
88+
};
89+
90+
const toggleSelection = (completion: ICompletion) => {
91+
const newSelection = [...selection];
92+
const index = selection.findIndex(selection => selection.completionId === completion.completionId);
93+
94+
if (index >= 0) {
95+
newSelection.splice(index, 1);
96+
} else {
97+
newSelection.push(completion);
98+
}
99+
100+
onSelectionChange(newSelection);
101+
focusEditor();
102+
};
103+
104+
const removeSelection = (completion: ICompletion) => {
105+
const newSelection = [...selection];
106+
const index = selection.findIndex(selection => selection.completionId === completion.completionId);
107+
108+
if (index >= 0) {
109+
newSelection.splice(index, 1);
110+
onSelectionChange(newSelection);
111+
}
112+
};
113+
114+
const hasPlaceholder = (): boolean => selection.length === 0 && query.length === 0;
115+
116+
return (
117+
<div className="mx_AutocompleteInput">
118+
<div
119+
ref={editorContainerRef}
120+
className={classNames({
121+
'mx_AutocompleteInput_editor': true,
122+
'mx_AutocompleteInput_editor--focused': isFocused,
123+
'mx_AutocompleteInput_editor--has-suggestions': suggestions.length > 0,
124+
})}
125+
onClick={onClickInputArea}
126+
data-testid="autocomplete-editor"
127+
>
128+
<SearchIcon className="mx_AutocompleteInput_search_icon" width={16} height={16} />
129+
{
130+
selection.map(item => (
131+
<SelectionItem
132+
key={item.completionId}
133+
item={item}
134+
onClick={removeSelection}
135+
render={renderSelection}
136+
/>
137+
))
138+
}
139+
<input
140+
ref={editorRef}
141+
type="text"
142+
onKeyDown={onKeyDown}
143+
onChange={onQueryChange}
144+
value={query}
145+
autoComplete="off"
146+
placeholder={hasPlaceholder() ? placeholder : undefined}
147+
data-testid="autocomplete-input"
148+
{...onFocusChangeHandlerFunctions}
149+
/>
150+
</div>
151+
{
152+
(isFocused && suggestions.length) ? (
153+
<div
154+
className="mx_AutocompleteInput_matches"
155+
style={{ top: editorContainerRef.current?.clientHeight }}
156+
data-testid="autocomplete-matches"
157+
>
158+
{
159+
suggestions.map((item) => (
160+
<SuggestionItem
161+
key={item.completionId}
162+
item={item}
163+
selection={selection}
164+
onClick={toggleSelection}
165+
render={renderSuggestion}
166+
/>
167+
))
168+
}
169+
</div>
170+
) : null
171+
}
172+
</div>
173+
);
174+
};
175+
176+
type SelectionItemProps = {
177+
item: ICompletion;
178+
onClick: (completion: ICompletion) => void;
179+
render?: (completion: ICompletion) => ReactElement;
180+
};
181+
182+
const SelectionItem: React.FC<SelectionItemProps> = ({ item, onClick, render }) => {
183+
const withContainer = (children: ReactNode): ReactElement => (
184+
<span
185+
className='mx_AutocompleteInput_editor_selection'
186+
data-testid={`autocomplete-selection-item-${item.completionId}`}
187+
>
188+
<span className='mx_AutocompleteInput_editor_selection_pill'>
189+
{ children }
190+
</span>
191+
<AccessibleButton
192+
className='mx_AutocompleteInput_editor_selection_remove_button'
193+
onClick={() => onClick(item)}
194+
data-testid={`autocomplete-selection-remove-button-${item.completionId}`}
195+
>
196+
<PillRemoveIcon width={8} height={8} />
197+
</AccessibleButton>
198+
</span>
199+
);
200+
201+
if (render) {
202+
return withContainer(render(item));
203+
}
204+
205+
return withContainer(
206+
<span className='mx_AutocompleteInput_editor_selection_text'>{ item.completion }</span>,
207+
);
208+
};
209+
210+
type SuggestionItemProps = {
211+
item: ICompletion;
212+
selection: ICompletion[];
213+
onClick: (completion: ICompletion) => void;
214+
render?: (completion: ICompletion) => ReactElement;
215+
};
216+
217+
const SuggestionItem: React.FC<SuggestionItemProps> = ({ item, selection, onClick, render }) => {
218+
const isSelected = selection.some(selection => selection.completionId === item.completionId);
219+
const classes = classNames({
220+
'mx_AutocompleteInput_suggestion': true,
221+
'mx_AutocompleteInput_suggestion--selected': isSelected,
222+
});
223+
224+
const withContainer = (children: ReactNode): ReactElement => (
225+
<div
226+
className={classes}
227+
// `onClick` cannot be used here as it would lead to focus loss and closing the suggestion list.
228+
onMouseDown={(event) => {
229+
event.preventDefault();
230+
onClick(item);
231+
}}
232+
data-testid={`autocomplete-suggestion-item-${item.completionId}`}
233+
>
234+
{ children }
235+
</div>
236+
);
237+
238+
if (render) {
239+
return withContainer(render(item));
240+
}
241+
242+
return withContainer(
243+
<>
244+
<span className='mx_AutocompleteInput_suggestion_title'>{ item.completion }</span>
245+
<span className='mx_AutocompleteInput_suggestion_description'>{ item.completionId }</span>
246+
</>,
247+
);
248+
};

0 commit comments

Comments
 (0)