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

Commit

Permalink
utilizes existing useFocus hook, fixes TypeScript issues in strict mo…
Browse files Browse the repository at this point in the history
…de, outsources selection & suggestion render function to separate components
  • Loading branch information
GoodGuyMarco committed Nov 29, 2022
1 parent 084ca68 commit 8fbb1ea
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 90 deletions.
35 changes: 24 additions & 11 deletions res/css/structures/_AutocompleteInput.pcss
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

.mx_AutocompleteInput {
position: relative;
}
Expand Down Expand Up @@ -59,24 +75,21 @@
transition: border-color 0.25s;
border: 1px solid $input-border-color;

> input[type="text"] {
margin: 6px 0 !important;
height: 24px;
> input {
flex: 1;
height: $font-24px;
line-height: $font-24px;
font-size: $font-14px;
padding-left: $spacing-12;
border: 0 !important;
outline: 0 !important;
resize: none;
box-sizing: border-box;
min-width: 40%;
flex: 1 !important;
resize: none;
// `!important` is required to bypass global input styles.
margin: 6px 0 !important;
border-color: transparent !important;
color: $primary-content !important;
font-weight: normal !important;

&::placeholder {
color: $primary-content !important;
font-weight: normal !important;
font-size: 1.4rem;
}
}
}
Expand Down
182 changes: 105 additions & 77 deletions src/components/structures/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { useState, ReactNode, ChangeEvent, KeyboardEvent, useRef } from 'react';
import React, { useState, ReactNode, ChangeEvent, KeyboardEvent, useRef, ReactElement } from 'react';
import classNames from 'classnames';

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

interface AutocompleteInputProps {
provider: Autocompleter;
placeholder: string;
selection: ICompletion[];
onSelectionChange: (selection: ICompletion[]) => void;
maxSuggestions?: number;
renderSuggestion?: (s: ICompletion) => ReactNode;
renderSelection?: (m: ICompletion) => ReactNode;
renderSuggestion?: (s: ICompletion) => ReactElement;
renderSelection?: (m: ICompletion) => ReactElement;
additionalFilter?: (suggestion: ICompletion) => boolean;
}

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

const focusEditor = () => {
editorRef?.current?.focus();
Expand Down Expand Up @@ -111,72 +112,6 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
}
};

const _renderSuggestion = (completion: ICompletion): ReactNode => {
const isSelected = selection.findIndex(selection => selection.completionId === completion.completionId) >= 0;
const classes = classNames({
'mx_AutocompleteInput_suggestion': true,
'mx_AutocompleteInput_suggestion--selected': isSelected,
});

const withContainer = (children: ReactNode): ReactNode => (
<div className={classes}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();

toggleSelection(completion);
}}
key={completion.completionId}
data-testid={`autocomplete-suggestion-item-${completion.completionId}`}
>
<div>
{ children }
</div>
{ isSelected && <CheckmarkIcon height={16} width={16} /> }
</div>
);

if (renderSuggestion) {
return withContainer(renderSuggestion(completion));
}

return withContainer(
<>
<span className='mx_AutocompleteInput_suggestion_title'>{ completion.completion }</span>
<span className='mx_AutocompleteInput_suggestion_description'>{ completion.completionId }</span>
</>,
);
};

const _renderSelection = (s: ICompletion): ReactNode => {
const withContainer = (children: ReactNode): ReactNode => (
<span
className='mx_AutocompleteInput_editor_selection'
key={s.completionId}
data-testid={`autocomplete-selection-item-${s.completionId}`}
>
<span className='mx_AutocompleteInput_editor_selection_pill'>
{ children }
</span>
<AccessibleButton
className='mx_AutocompleteInput_editor_selection_remove'
onClick={() => removeSelection(s)}
data-testid={`autocomplete-selection-remove-button-${s.completionId}`}
>
<PillRemoveIcon width={8} height={8} />
</AccessibleButton>
</span>
);

if (renderSelection) {
return withContainer(renderSelection(s));
}

return withContainer(
<span className='mx_AutocompleteInput_editor_selection_text'>{ s.completion }</span>,
);
};

const hasPlaceholder = (): boolean => selection.length === 0 && query.length === 0;

return (
Expand All @@ -187,18 +122,26 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
onClick={onClickInputArea}
data-testid="autocomplete-editor"
>
{ selection.map(s => _renderSelection(s)) }
{
selection.map(item => (
<SelectionItem
key={item.completionId}
item={item}
onClick={removeSelection}
render={renderSelection}
/>
))
}
<input
ref={editorRef}
type="text"
onKeyDown={onKeyDown}
onChange={onQueryChange}
value={query}
autoComplete="off"
placeholder={hasPlaceholder() ? placeholder : null}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder={hasPlaceholder() ? placeholder : undefined}
data-testid="autocomplete-input"
{...onFocusChangeHandlerFunctions}
/>
</div>
{
Expand All @@ -209,11 +152,96 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
data-testid="autocomplete-matches"
>
{
suggestions.map((s) => _renderSuggestion(s))
suggestions.map((item) => (
<SuggestionItem
key={item.completionId}
item={item}
selection={selection}
onClick={toggleSelection}
render={renderSuggestion}
/>
))
}
</div>
) : null
}
</div>
);
};

type SelectionItemProps = {
item: ICompletion;
onClick: (completion: ICompletion) => void;
render?: (completion: ICompletion) => ReactElement;
};

const SelectionItem: React.FC<SelectionItemProps> = ({ item, onClick, render }) => {
const withContainer = (children: ReactNode): ReactElement => (
<span
className='mx_AutocompleteInput_editor_selection'
data-testid={`autocomplete-selection-item-${item.completionId}`}
>
<span className='mx_AutocompleteInput_editor_selection_pill'>
{ children }
</span>
<AccessibleButton
className='mx_AutocompleteInput_editor_selection_remove'
onClick={() => onClick(item)}
data-testid={`autocomplete-selection-remove-button-${item.completionId}`}
>
<PillRemoveIcon width={8} height={8} />
</AccessibleButton>
</span>
);

if (render) {
return withContainer(render(item));
}

return withContainer(
<span className='mx_AutocompleteInput_editor_selection_text'>{ item.completion }</span>,
);
};

type SuggestionItemProps = {
item: ICompletion;
selection: ICompletion[];
onClick: (completion: ICompletion) => void;
render?: (completion: ICompletion) => ReactElement;
};

const SuggestionItem: React.FC<SuggestionItemProps> = ({ item, selection, onClick, render }) => {
const isSelected = selection.some(selection => selection.completionId === item.completionId);
const classes = classNames({
'mx_AutocompleteInput_suggestion': true,
'mx_AutocompleteInput_suggestion--selected': isSelected,
});

const withContainer = (children: ReactNode): ReactElement => (
<div
className={classes}
// `onClick` cannot be used here as it would lead to focus loss and closing the suggestion list.
onMouseDown={(event) => {
event.preventDefault();
onClick(item);
}}
data-testid={`autocomplete-suggestion-item-${item.completionId}`}
>
<div>
{ children }
</div>
{ isSelected && <CheckmarkIcon height={16} width={16} /> }
</div>
);

if (render) {
return withContainer(render(item));
}

return withContainer(
<>
<span className='mx_AutocompleteInput_suggestion_title'>{ item.completion }</span>
<span className='mx_AutocompleteInput_suggestion_description'>{ item.completionId }</span>
</>,
);
};
16 changes: 15 additions & 1 deletion src/components/views/settings/AddPrivilegedUsers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,19 @@ export const AddPrivilegedUsers: React.FC<AddPrivilegedUsersProps> = ({ room, de
const [powerLevel, setPowerLevel] = useState<number>(defaultUserLevel);
const [selectedUsers, setSelectedUsers] = useState<ICompletion[]>([]);
const filterSuggestions = useCallback(
(user: ICompletion) => room.getMember(user.completionId)?.powerLevel <= defaultUserLevel,
(user: ICompletion) => {
if (user.completionId === undefined) {
return false;
}

const member = room.getMember(user.completionId);

if (member === null) {
return false;
}

return member.powerLevel <= defaultUserLevel;
},
[room, defaultUserLevel],
);

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

try {
// TODO: Remove @ts-ignore as soon as https://github.com/matrix-org/matrix-js-sdk/pull/2892 is merged.
// @ts-ignore
await client.setPowerLevel(room.roomId, userIds, powerLevel, powerLevelEvent);
setSelectedUsers([]);
setPowerLevel(defaultUserLevel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,11 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
<div className="mx_SettingsTab mx_RolesRoomSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Roles & Permissions") }</div>
{ privilegedUsersSection }
{ canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} /> }
{
(canChangeLevels && room !== null) && (
<AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />
)
}
{ mutedUsersSection }
{ bannedUsersSection }
<SettingsFieldset
Expand Down

0 comments on commit 8fbb1ea

Please sign in to comment.