Device manager - updated dropdown style in filtered device list (PSG-689) (#9226)

* add FilterDropdown wrapper on Dropdown for filter styles

* test and fix strict errors

* fix comment
This commit is contained in:
Kerry 2022-08-30 19:11:33 +02:00 committed by GitHub
parent 825a0af4a9
commit 50f6986f6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 389 additions and 20 deletions

View file

@ -68,7 +68,7 @@ class MenuOption extends React.Component<IMenuOptionProps> {
}
}
interface IProps {
export interface DropdownProps {
id: string;
// ARIA label
label: string;
@ -108,13 +108,13 @@ interface IState {
* but somewhat simpler as react-select is 79KB of minified
* javascript.
*/
export default class Dropdown extends React.Component<IProps, IState> {
export default class Dropdown extends React.Component<DropdownProps, IState> {
private readonly buttonRef = createRef<HTMLDivElement>();
private dropdownRootElement: HTMLDivElement = null;
private ignoreEvent: MouseEvent = null;
private childrenByKey: Record<string, ReactNode> = {};
constructor(props: IProps) {
constructor(props: DropdownProps) {
super(props);
this.reindexChildren(this.props.children);

View file

@ -0,0 +1,86 @@
/*
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.
*/
import React from 'react';
import classNames from 'classnames';
import { Icon as CheckmarkIcon } from '../../../../res/img/element-icons/roomlist/checkmark.svg';
import Dropdown, { DropdownProps } from './Dropdown';
export type FilterDropdownOption<FilterKeysType extends string> = {
id: FilterKeysType;
label: string;
description?: string;
};
type FilterDropdownProps<FilterKeysType extends string> = Omit<DropdownProps, 'children'> & {
value: FilterKeysType;
options: FilterDropdownOption<FilterKeysType>[];
// A label displayed before the selected value
// in the dropdown input
selectedLabel?: string;
};
const getSelectedFilterOptionComponent = <FilterKeysType extends string>(
options: FilterDropdownOption<FilterKeysType>[], selectedLabel?: string,
) => (filterKey: FilterKeysType) => {
const option = options.find(({ id }) => id === filterKey);
if (!option) {
return null;
}
if (selectedLabel) {
return `${selectedLabel}: ${option.label}`;
}
return option.label;
};
/**
* Dropdown styled for list filtering
*/
export const FilterDropdown = <FilterKeysType extends string = string>(
{
value,
options,
selectedLabel,
className,
...restProps
}: FilterDropdownProps<FilterKeysType>,
): React.ReactElement<FilterDropdownProps<FilterKeysType>> => {
return <Dropdown
{...restProps}
value={value}
className={classNames('mx_FilterDropdown', className)}
getShortOption={getSelectedFilterOptionComponent<FilterKeysType>(options, selectedLabel)}
>
{ options.map(({ id, label, description }) =>
<div
className='mx_FilterDropdown_option'
data-testid={`filter-option-${id}`}
key={id}
>
{ id === value && <CheckmarkIcon className='mx_FilterDropdown_optionSelectedIcon' /> }
<span className='mx_FilterDropdown_optionLabel'>
{ label }
</span>
{
!!description
&& <span
className='mx_FilterDropdown_optionDescription'
>{ description }</span>
}
</div>,
) }
</Dropdown>;
};

View file

@ -18,7 +18,7 @@ import React from 'react';
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import Dropdown from '../../elements/Dropdown';
import { FilterDropdown, FilterDropdownOption } from '../../elements/FilterDropdown';
import DeviceDetails from './DeviceDetails';
import DeviceExpandDetailsButton from './DeviceExpandDetailsButton';
import DeviceSecurityCard from './DeviceSecurityCard';
@ -45,13 +45,14 @@ interface Props {
const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) =>
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
const getFilteredSortedDevices = (devices: DevicesDictionary, filter: DeviceSecurityVariation) =>
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
.sort(sortDevicesByLatestActivity);
const ALL_FILTER_ID = 'ALL';
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
const FilterSecurityCard: React.FC<{ filter?: DeviceSecurityVariation | string }> = ({ filter }) => {
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
switch (filter) {
case DeviceSecurityVariation.Verified:
return <div className='mx_FilteredDeviceList_securityCard'>
@ -95,7 +96,7 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceSecurityVariation | string }
}
};
const getNoResultsMessage = (filter: DeviceSecurityVariation): string => {
const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
switch (filter) {
case DeviceSecurityVariation.Verified:
return _t('No verified sessions found.');
@ -107,7 +108,7 @@ const getNoResultsMessage = (filter: DeviceSecurityVariation): string => {
return _t('No sessions found.');
}
};
interface NoResultsProps { filter: DeviceSecurityVariation, clearFilter: () => void}
interface NoResultsProps { filter?: DeviceSecurityVariation, clearFilter: () => void}
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
<div className='mx_FilteredDeviceList_noResults'>
{ getNoResultsMessage(filter) }
@ -158,7 +159,7 @@ const FilteredDeviceList: React.FC<Props> = ({
}) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);
const options = [
const options: FilterDropdownOption<DeviceFilterKey>[] = [
{ id: ALL_FILTER_ID, label: _t('All') },
{
id: DeviceSecurityVariation.Verified,
@ -180,7 +181,7 @@ const FilteredDeviceList: React.FC<Props> = ({
},
];
const onFilterOptionChange = (filterId: DeviceSecurityVariation | typeof ALL_FILTER_ID) => {
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
};
@ -189,16 +190,14 @@ const FilteredDeviceList: React.FC<Props> = ({
<span className='mx_FilteredDeviceList_headerLabel'>
{ _t('Sessions') }
</span>
<Dropdown
<FilterDropdown<DeviceFilterKey>
id='device-list-filter'
label={_t('Filter devices')}
value={filter || ALL_FILTER_ID}
onOptionChange={onFilterOptionChange}
>
{ options.map(({ id, label }) =>
<div data-test-id={`device-filter-option-${id}`} key={id}>{ label }</div>,
) }
</Dropdown>
options={options}
selectedLabel={_t('Show')}
/>
</div>
{ !!sortedDevices.length
? <FilterSecurityCard filter={filter} />