diff --git a/res/css/structures/_AutocompleteInput.pcss b/res/css/structures/_AutocompleteInput.pcss index 7a54261913c8..a851ab3628b2 100644 --- a/res/css/structures/_AutocompleteInput.pcss +++ b/res/css/structures/_AutocompleteInput.pcss @@ -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; } @@ -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; } } } diff --git a/src/components/structures/AutocompleteInput.tsx b/src/components/structures/AutocompleteInput.tsx index db7d1b1cb254..97118d36aa11 100644 --- a/src/components/structures/AutocompleteInput.tsx +++ b/src/components/structures/AutocompleteInput.tsx @@ -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"; @@ -23,6 +23,7 @@ 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; @@ -30,8 +31,8 @@ interface AutocompleteInputProps { 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; } @@ -47,9 +48,9 @@ export const AutocompleteInput: React.FC = ({ }) => { const [query, setQuery] = useState(''); const [suggestions, setSuggestions] = useState([]); - const [isFocused, setFocused] = useState(false); - const editorContainerRef = useRef(); - const editorRef = useRef(); + const [isFocused, onFocusChangeHandlerFunctions] = useFocus(); + const editorContainerRef = useRef(null); + const editorRef = useRef(null); const focusEditor = () => { editorRef?.current?.focus(); @@ -111,72 +112,6 @@ export const AutocompleteInput: React.FC = ({ } }; - 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 => ( -
{ - e.preventDefault(); - e.stopPropagation(); - - toggleSelection(completion); - }} - key={completion.completionId} - data-testid={`autocomplete-suggestion-item-${completion.completionId}`} - > -
- { children } -
- { isSelected && } -
- ); - - if (renderSuggestion) { - return withContainer(renderSuggestion(completion)); - } - - return withContainer( - <> - { completion.completion } - { completion.completionId } - , - ); - }; - - const _renderSelection = (s: ICompletion): ReactNode => { - const withContainer = (children: ReactNode): ReactNode => ( - - - { children } - - removeSelection(s)} - data-testid={`autocomplete-selection-remove-button-${s.completionId}`} - > - - - - ); - - if (renderSelection) { - return withContainer(renderSelection(s)); - } - - return withContainer( - { s.completion }, - ); - }; - const hasPlaceholder = (): boolean => selection.length === 0 && query.length === 0; return ( @@ -187,7 +122,16 @@ export const AutocompleteInput: React.FC = ({ onClick={onClickInputArea} data-testid="autocomplete-editor" > - { selection.map(s => _renderSelection(s)) } + { + selection.map(item => ( + + )) + } = ({ 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} /> { @@ -209,7 +152,15 @@ export const AutocompleteInput: React.FC = ({ data-testid="autocomplete-matches" > { - suggestions.map((s) => _renderSuggestion(s)) + suggestions.map((item) => ( + + )) } ) : null @@ -217,3 +168,80 @@ export const AutocompleteInput: React.FC = ({ ); }; + +type SelectionItemProps = { + item: ICompletion; + onClick: (completion: ICompletion) => void; + render?: (completion: ICompletion) => ReactElement; +}; + +const SelectionItem: React.FC = ({ item, onClick, render }) => { + const withContainer = (children: ReactNode): ReactElement => ( + + + { children } + + onClick(item)} + data-testid={`autocomplete-selection-remove-button-${item.completionId}`} + > + + + + ); + + if (render) { + return withContainer(render(item)); + } + + return withContainer( + { item.completion }, + ); +}; + +type SuggestionItemProps = { + item: ICompletion; + selection: ICompletion[]; + onClick: (completion: ICompletion) => void; + render?: (completion: ICompletion) => ReactElement; +}; + +const SuggestionItem: React.FC = ({ 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 => ( +
{ + event.preventDefault(); + onClick(item); + }} + data-testid={`autocomplete-suggestion-item-${item.completionId}`} + > +
+ { children } +
+ { isSelected && } +
+ ); + + if (render) { + return withContainer(render(item)); + } + + return withContainer( + <> + { item.completion } + { item.completionId } + , + ); +}; diff --git a/src/components/views/settings/AddPrivilegedUsers.tsx b/src/components/views/settings/AddPrivilegedUsers.tsx index 1c5497fb5fe2..a5e8252cd3e0 100644 --- a/src/components/views/settings/AddPrivilegedUsers.tsx +++ b/src/components/views/settings/AddPrivilegedUsers.tsx @@ -41,7 +41,19 @@ export const AddPrivilegedUsers: React.FC = ({ room, de const [powerLevel, setPowerLevel] = useState(defaultUserLevel); const [selectedUsers, setSelectedUsers] = useState([]); 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], ); @@ -53,6 +65,8 @@ export const AddPrivilegedUsers: React.FC = ({ 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); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index a961a13c2716..db0e772b2614 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -470,7 +470,11 @@ export default class RolesRoomSettingsTab extends React.Component {
{ _t("Roles & Permissions") }
{ privilegedUsersSection } - { canChangeLevels && } + { + (canChangeLevels && room !== null) && ( + + ) + } { mutedUsersSection } { bannedUsersSection }