mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Filtering improvement - part 1
Summary: - Makes search input alway visible - Gives search input focus on open - Adds `Future Values` Checkbox - Show `All Shown` `All Excpet` when values are filtered - Show `Others` instead of `Future Values` when values are filtered - Escape close search input - Enter does the same as `All Shown` when filtering values Test Plan: - Updated existing projects and nbrowser test - Adds new projects test Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2753
This commit is contained in:
parent
6e844a2e76
commit
e2d3b70509
@ -1,6 +1,6 @@
|
|||||||
import {CellValue} from 'app/common/DocActions';
|
import {CellValue} from 'app/common/DocActions';
|
||||||
import {nativeCompare} from 'app/common/gutil';
|
import {nativeCompare} from 'app/common/gutil';
|
||||||
import {Disposable, Observable} from 'grainjs';
|
import {Computed, Disposable, Observable} from 'grainjs';
|
||||||
|
|
||||||
export type ColumnFilterFunc = (value: CellValue) => boolean;
|
export type ColumnFilterFunc = (value: CellValue) => boolean;
|
||||||
|
|
||||||
@ -15,15 +15,26 @@ interface FilterState {
|
|||||||
values: Set<CellValue>;
|
values: Set<CellValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a JSON string for a filter to a FilterState
|
// Creates a FilterState. Accepts spec as a json string or a FilterSpec.
|
||||||
function makeFilterState(filterJson: string): FilterState {
|
function makeFilterState(spec: string | FilterSpec): FilterState {
|
||||||
const spec: FilterSpec = (filterJson && JSON.parse(filterJson)) || {};
|
if (typeof(spec) === 'string') {
|
||||||
|
return makeFilterState((spec && JSON.parse(spec)) || {});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
include: Boolean(spec.included),
|
include: Boolean(spec.included),
|
||||||
values: new Set(spec.included || spec.excluded || []),
|
values: new Set(spec.included || spec.excluded || []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if state and spec are equivalent, false otherwise.
|
||||||
|
export function isEquivalentFilter(state: FilterState, spec: FilterSpec): boolean {
|
||||||
|
const other = makeFilterState(spec);
|
||||||
|
if (state.include !== other.include) { return false; }
|
||||||
|
if (state.values.size !== other.values.size) { return false; }
|
||||||
|
for (const val of other.values) { if (!state.values.has(val)) { return false; }}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns a filter function for a particular column: the function takes a cell value and returns
|
// Returns a filter function for a particular column: the function takes a cell value and returns
|
||||||
// whether it's accepted according to the given FilterState.
|
// whether it's accepted according to the given FilterState.
|
||||||
function makeFilterFunc({include, values}: FilterState): ColumnFilterFunc {
|
function makeFilterFunc({include, values}: FilterState): ColumnFilterFunc {
|
||||||
@ -47,18 +58,23 @@ export function getFilterFunc(filterJson: string): ColumnFilterFunc|null {
|
|||||||
* been customized.
|
* been customized.
|
||||||
*/
|
*/
|
||||||
export class ColumnFilter extends Disposable {
|
export class ColumnFilter extends Disposable {
|
||||||
public readonly filterFunc: Observable<ColumnFilterFunc>;
|
public readonly filterFunc = Observable.create<ColumnFilterFunc>(this, () => true);
|
||||||
|
|
||||||
|
// Computed that returns true if filter is an inclusion filter, false otherwise.
|
||||||
|
public readonly isInclusionFilter: Computed<boolean> = Computed.create(this, this.filterFunc, () => this._include);
|
||||||
|
|
||||||
|
// Computed that returns the current filter state.
|
||||||
|
public readonly state: Computed<FilterState> = Computed.create(this, this.filterFunc, () => this._getState());
|
||||||
|
|
||||||
private _include: boolean;
|
private _include: boolean;
|
||||||
private _values: Set<CellValue>;
|
private _values: Set<CellValue>;
|
||||||
|
|
||||||
constructor(private _initialFilterJson: string) {
|
constructor(private _initialFilterJson: string) {
|
||||||
super();
|
super();
|
||||||
this.filterFunc = Observable.create<ColumnFilterFunc>(this, () => true);
|
|
||||||
this.setState(_initialFilterJson);
|
this.setState(_initialFilterJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setState(filterJson: string) {
|
public setState(filterJson: string|FilterSpec) {
|
||||||
const state = makeFilterState(filterJson);
|
const state = makeFilterState(filterJson);
|
||||||
this._include = state.include;
|
this._include = state.include;
|
||||||
this._values = state.values;
|
this._values = state.values;
|
||||||
@ -102,7 +118,11 @@ export class ColumnFilter extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _updateState(): void {
|
private _updateState(): void {
|
||||||
this.filterFunc.set(makeFilterFunc({include: this._include, values: this._values}));
|
this.filterFunc.set(makeFilterFunc(this._getState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getState(): FilterState {
|
||||||
|
return {include: this._include, values: this._values};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
* but on Cancel the model is reset to its initial state prior to menu closing.
|
* but on Cancel the model is reset to its initial state prior to menu closing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {allInclusive, ColumnFilter} from 'app/client/models/ColumnFilter';
|
import {allInclusive, ColumnFilter, isEquivalentFilter} from 'app/client/models/ColumnFilter';
|
||||||
import {ViewFieldRec} from 'app/client/models/DocModel';
|
import {ViewFieldRec} from 'app/client/models/DocModel';
|
||||||
import {FilteredRowSource} from 'app/client/models/rowset';
|
import {FilteredRowSource} from 'app/client/models/rowset';
|
||||||
import {SectionFilter} from 'app/client/models/SectionFilter';
|
import {SectionFilter} from 'app/client/models/SectionFilter';
|
||||||
@ -13,10 +13,10 @@ import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
|||||||
import {cssCheckboxSquare, cssLabel, cssLabelText} from 'app/client/ui2018/checkbox';
|
import {cssCheckboxSquare, cssLabel, cssLabelText} from 'app/client/ui2018/checkbox';
|
||||||
import {colors, vars} from 'app/client/ui2018/cssVars';
|
import {colors, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {menuCssClass, menuDivider, menuIcon} from 'app/client/ui2018/menus';
|
import {menuCssClass, menuDivider} from 'app/client/ui2018/menus';
|
||||||
import {CellValue} from 'app/common/DocActions';
|
import {CellValue} from 'app/common/DocActions';
|
||||||
import {localeCompare} from 'app/common/gutil';
|
import {localeCompare} from 'app/common/gutil';
|
||||||
import {Computed, dom, input, makeTestId, Observable, styled} from 'grainjs';
|
import {Computed, dom, IDisposableOwner, input, makeTestId, Observable, styled} from 'grainjs';
|
||||||
import escapeRegExp = require('lodash/escapeRegExp');
|
import escapeRegExp = require('lodash/escapeRegExp');
|
||||||
import identity = require('lodash/identity');
|
import identity = require('lodash/identity');
|
||||||
import {IOpenController} from 'popweasel';
|
import {IOpenController} from 'popweasel';
|
||||||
@ -34,18 +34,13 @@ interface IFilterMenuOptions {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function columnFilterMenu({ columnFilter, valueCounts, doSave, onClose }: IFilterMenuOptions): HTMLElement {
|
export function columnFilterMenu(owner: IDisposableOwner,
|
||||||
|
{ columnFilter, valueCounts, doSave, onClose }: IFilterMenuOptions): HTMLElement {
|
||||||
// Save the initial state to allow reverting back to it on Cancel
|
// Save the initial state to allow reverting back to it on Cancel
|
||||||
const initialStateJson = columnFilter.makeFilterJson();
|
const initialStateJson = columnFilter.makeFilterJson();
|
||||||
|
|
||||||
const testId = makeTestId('test-filter-menu-');
|
const testId = makeTestId('test-filter-menu-');
|
||||||
|
|
||||||
// Computed boolean reflecting whether current filter state is all-inclusive.
|
|
||||||
const includesAll: Computed<boolean> = Computed.create(null, columnFilter.filterFunc, () => {
|
|
||||||
const spec = columnFilter.makeFilterJson();
|
|
||||||
return spec === allInclusive;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Map to keep track of displayed checkboxes
|
// Map to keep track of displayed checkboxes
|
||||||
const checkboxMap: Map<CellValue, HTMLInputElement> = new Map();
|
const checkboxMap: Map<CellValue, HTMLInputElement> = new Map();
|
||||||
|
|
||||||
@ -58,79 +53,102 @@ export function columnFilterMenu({ columnFilter, valueCounts, doSave, onClose }:
|
|||||||
|
|
||||||
const valueCountArr: Array<[CellValue, IFilterCount]> = Array.from(valueCounts);
|
const valueCountArr: Array<[CellValue, IFilterCount]> = Array.from(valueCounts);
|
||||||
|
|
||||||
const openSearch = Observable.create(null, false);
|
const searchValueObs = Observable.create(owner, '');
|
||||||
const searchValueObs = Observable.create(null, '');
|
|
||||||
const filteredValues = Computed.create(null, openSearch, searchValueObs, (_use, isOpen, searchValue) => {
|
// computes a set of all keys that matches the search text.
|
||||||
|
const filterSet = Computed.create(owner, searchValueObs, (_use, searchValue) => {
|
||||||
const searchRegex = new RegExp(escapeRegExp(searchValue), 'i');
|
const searchRegex = new RegExp(escapeRegExp(searchValue), 'i');
|
||||||
return valueCountArr.filter(([key]) => !isOpen || searchRegex.test(key as string))
|
return new Set(valueCountArr.filter(([key]) => searchRegex.test(key as string)).map(([key]) => key));
|
||||||
|
});
|
||||||
|
|
||||||
|
// computes the sorted array of all values (ie: pair of key and IFilterCount) that matches the search text.
|
||||||
|
const filteredValues = Computed.create(owner, filterSet, (_use, filter) => {
|
||||||
|
return valueCountArr.filter(([key]) => filter.has(key))
|
||||||
.sort((a, b) => localeCompare(a[1].label, b[1].label));
|
.sort((a, b) => localeCompare(a[1].label, b[1].label));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// computes the array of all values that does NOT matches the search text
|
||||||
|
const otherValues = Computed.create(owner, filterSet, (_use, filter) => {
|
||||||
|
return valueCountArr.filter(([key]) => !filter.has(key));
|
||||||
|
});
|
||||||
|
|
||||||
|
// computes the total count across all values that don’t match the search text
|
||||||
|
const othersCount = Computed.create(owner, otherValues, (_use, others) => {
|
||||||
|
return others.reduce((acc, val) => acc + val[1].count, 0).toLocaleString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// computes the array of keys that matches the search text
|
||||||
|
const filteredKeys = Computed.create(owner, filteredValues, (_use, values) => values.map(([key]) => key));
|
||||||
|
|
||||||
let searchInput: HTMLInputElement;
|
let searchInput: HTMLInputElement;
|
||||||
let reset = false;
|
let reset = false;
|
||||||
|
|
||||||
|
// Gives focus to the searchInput on open
|
||||||
|
setTimeout(() => searchInput.focus(), 0);
|
||||||
|
|
||||||
const filterMenu: HTMLElement = cssMenu(
|
const filterMenu: HTMLElement = cssMenu(
|
||||||
{ tabindex: '-1' }, // Allow menu to be focused
|
{ tabindex: '-1' }, // Allow menu to be focused
|
||||||
testId('wrapper'),
|
testId('wrapper'),
|
||||||
dom.cls(menuCssClass),
|
dom.cls(menuCssClass),
|
||||||
dom.autoDispose(includesAll),
|
|
||||||
dom.autoDispose(filterListener),
|
dom.autoDispose(filterListener),
|
||||||
dom.autoDispose(openSearch),
|
|
||||||
dom.autoDispose(searchValueObs),
|
|
||||||
dom.autoDispose(filteredValues),
|
|
||||||
(elem) => { setTimeout(() => elem.focus(), 0); }, // Grab focus on open
|
|
||||||
dom.onDispose(() => doSave(reset)), // Save on disposal, which should always happen as part of closing.
|
dom.onDispose(() => doSave(reset)), // Save on disposal, which should always happen as part of closing.
|
||||||
dom.onKeyDown({
|
dom.onKeyDown({
|
||||||
Enter: () => onClose(),
|
Enter: () => onClose(),
|
||||||
Escape: () => onClose()
|
Escape: () => onClose()
|
||||||
}),
|
}),
|
||||||
cssMenuHeader(
|
cssMenuHeader(
|
||||||
cssSelectAll(testId('select-all'),
|
cssSearchIcon('Search'),
|
||||||
dom.hide(openSearch),
|
searchInput = cssSearch(
|
||||||
dom.on('click', () => includesAll.get() ? columnFilter.clear() : columnFilter.selectAll()),
|
searchValueObs, { onInput: true },
|
||||||
dom.domComputed(includesAll, yesNo => [
|
|
||||||
menuIcon(yesNo ? 'CrossSmall' : 'Tick'),
|
|
||||||
yesNo ? 'Select none' : 'Select all'
|
|
||||||
])
|
|
||||||
),
|
|
||||||
dom.maybe(openSearch, () => { return [
|
|
||||||
cssLabel(
|
|
||||||
cssCheckboxSquare({type: 'checkbox', checked: includesAll.get()}, testId('search-select'),
|
|
||||||
dom.on('change', (_ev, elem) => {
|
|
||||||
if (!searchValueObs.get()) { // If no search has been entered, treat select/deselect as Select All
|
|
||||||
elem.checked ? columnFilter.selectAll() : columnFilter.clear();
|
|
||||||
} else { // Otherwise, add/remove specific matched values
|
|
||||||
filteredValues.get()
|
|
||||||
.forEach(([key]) => elem.checked ? columnFilter.add(key) : columnFilter.delete(key));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
),
|
|
||||||
searchInput = cssSearch(searchValueObs, { onInput: true },
|
|
||||||
testId('search-input'),
|
testId('search-input'),
|
||||||
{ type: 'search', placeholder: 'Search values' },
|
{ type: 'search', placeholder: 'Search values' },
|
||||||
dom.show(openSearch),
|
|
||||||
dom.onKeyDown({
|
dom.onKeyDown({
|
||||||
Enter: () => undefined,
|
Enter: () => {
|
||||||
Escape: () => {
|
if (searchValueObs.get()) {
|
||||||
setTimeout(() => filterMenu.focus(), 0); // Give focus back to menu
|
columnFilter.setState({included: filteredKeys.get()});
|
||||||
openSearch.set(false);
|
}
|
||||||
|
},
|
||||||
|
Escape$: (ev) => {
|
||||||
|
if (searchValueObs.get()) {
|
||||||
|
searchValueObs.set('');
|
||||||
|
searchInput.focus();
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
]; }),
|
dom.maybe(searchValueObs, () => cssSearchIcon(
|
||||||
dom.domComputed(openSearch, isOpen => isOpen ?
|
'CrossSmall', testId('search-close'),
|
||||||
cssSearchIcon('CrossBig', testId('search-close'), dom.on('click', () => {
|
dom.on('click', () => {
|
||||||
openSearch.set(false);
|
|
||||||
searchValueObs.set('');
|
searchValueObs.set('');
|
||||||
})) :
|
searchInput.focus();
|
||||||
cssSearchIcon('Search', testId('search-open'), dom.on('click', () => {
|
}),
|
||||||
openSearch.set(true);
|
)),
|
||||||
setTimeout(() => searchInput.focus(), 0);
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
cssMenuDivider(),
|
cssMenuDivider(),
|
||||||
|
cssMenuItem(
|
||||||
|
dom.domComputed((use) => {
|
||||||
|
const searchValue = use(searchValueObs);
|
||||||
|
const allSpec = searchValue ? {included: use(filteredKeys)} : {excluded: []};
|
||||||
|
const noneSpec = searchValue ? {excluded: use(filteredKeys)} : {included: []};
|
||||||
|
const state = use(columnFilter.state);
|
||||||
|
return [
|
||||||
|
cssSelectAll(
|
||||||
|
dom.text(searchValue ? 'All Shown' : 'All'),
|
||||||
|
cssSelectAll.cls('-disabled', isEquivalentFilter(state, allSpec)),
|
||||||
|
dom.on('click', () => columnFilter.setState(allSpec)),
|
||||||
|
testId('select-all'),
|
||||||
|
),
|
||||||
|
cssDotSeparator('•'),
|
||||||
|
cssSelectAll(
|
||||||
|
searchValue ? 'All Except' : 'None',
|
||||||
|
cssSelectAll.cls('-disabled', isEquivalentFilter(state, noneSpec)),
|
||||||
|
dom.on('click', () => columnFilter.setState(noneSpec)),
|
||||||
|
testId('select-all'),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
})
|
||||||
|
),
|
||||||
cssItemList(
|
cssItemList(
|
||||||
testId('list'),
|
testId('list'),
|
||||||
dom.maybe(use => use(filteredValues).length === 0, () => cssNoResults('No matching values')),
|
dom.maybe(use => use(filteredValues).length === 0, () => cssNoResults('No matching values')),
|
||||||
@ -142,15 +160,33 @@ export function columnFilterMenu({ columnFilter, valueCounts, doSave, onClose }:
|
|||||||
(elem) => { elem.checked = columnFilter.includes(key); checkboxMap.set(key, elem); }),
|
(elem) => { elem.checked = columnFilter.includes(key); checkboxMap.set(key, elem); }),
|
||||||
cssItemValue(value.label === undefined ? key as string : value.label),
|
cssItemValue(value.label === undefined ? key as string : value.label),
|
||||||
),
|
),
|
||||||
cssItemCount(value.count.toLocaleString())) // Include comma separator
|
cssItemCount(value.count.toLocaleString(), testId('count')))) // Include comma separator
|
||||||
)
|
|
||||||
),
|
),
|
||||||
cssMenuDivider(),
|
cssMenuDivider(),
|
||||||
cssMenuFooter(
|
cssMenuFooter(
|
||||||
|
cssMenuItem(
|
||||||
|
testId('summary'),
|
||||||
|
cssLabel(
|
||||||
|
cssCheckboxSquare(
|
||||||
|
{type: 'checkbox'},
|
||||||
|
dom.on('change', (_ev, elem) => columnFilter.setState(
|
||||||
|
elem.checked ?
|
||||||
|
{excluded: filteredKeys.get().filter((key) => !columnFilter.includes(key))} :
|
||||||
|
{included: filteredKeys.get().filter((key) => columnFilter.includes(key))}
|
||||||
|
)),
|
||||||
|
dom.prop('checked', (use) => !use(columnFilter.isInclusionFilter))
|
||||||
|
),
|
||||||
|
cssItemValue(dom.text((use) => use(searchValueObs) ? 'Others' : 'Future Values')),
|
||||||
|
),
|
||||||
|
dom.maybe(searchValueObs, () => cssItemCount(dom.text(othersCount)))
|
||||||
|
),
|
||||||
|
cssMenuItem(
|
||||||
cssApplyButton('Apply', testId('apply-btn'),
|
cssApplyButton('Apply', testId('apply-btn'),
|
||||||
dom.on('click', () => { reset = true; onClose(); })),
|
dom.on('click', () => { reset = true; onClose(); })),
|
||||||
basicButton('Cancel', testId('cancel-btn'),
|
basicButton('Cancel', testId('cancel-btn'),
|
||||||
dom.on('click', () => { columnFilter.setState(initialStateJson); onClose(); } )))
|
dom.on('click', () => { columnFilter.setState(initialStateJson); onClose(); } ))
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return filterMenu;
|
return filterMenu;
|
||||||
}
|
}
|
||||||
@ -167,13 +203,16 @@ export function createFilterMenu(openCtl: IOpenController, sectionFilter: Sectio
|
|||||||
const valueMapFunc = (rowId: number) => formatter.formatAny(labelGetter(rowId));
|
const valueMapFunc = (rowId: number) => formatter.formatAny(labelGetter(rowId));
|
||||||
|
|
||||||
const valueCounts: Map<CellValue, {label: string, count: number}> = new Map();
|
const valueCounts: Map<CellValue, {label: string, count: number}> = new Map();
|
||||||
|
// TODO: as of now, this is not working for non text-or-numeric columns, ie: for Date column it is
|
||||||
|
// not possible to search for anything. Likely caused by the key being something completely
|
||||||
|
// different than the label.
|
||||||
addCountsToMap(valueCounts, rowSource.getAllRows() as Iterable<number>, valueGetter, valueMapFunc);
|
addCountsToMap(valueCounts, rowSource.getAllRows() as Iterable<number>, valueGetter, valueMapFunc);
|
||||||
addCountsToMap(valueCounts, rowSource.getHiddenRows() as Iterable<number>, valueGetter, valueMapFunc);
|
addCountsToMap(valueCounts, rowSource.getHiddenRows() as Iterable<number>, valueGetter, valueMapFunc);
|
||||||
|
|
||||||
const columnFilter = ColumnFilter.create(openCtl, field.activeFilter.peek());
|
const columnFilter = ColumnFilter.create(openCtl, field.activeFilter.peek());
|
||||||
sectionFilter.setFilterOverride(field.getRowId(), columnFilter); // Will be removed on menu disposal
|
sectionFilter.setFilterOverride(field.getRowId(), columnFilter); // Will be removed on menu disposal
|
||||||
|
|
||||||
return columnFilterMenu({
|
return columnFilterMenu(openCtl, {
|
||||||
columnFilter,
|
columnFilter,
|
||||||
valueCounts,
|
valueCounts,
|
||||||
onClose: () => openCtl.close(),
|
onClose: () => openCtl.close(),
|
||||||
@ -214,34 +253,45 @@ const cssMenu = styled('div', `
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
`);
|
`);
|
||||||
const cssMenuHeader = styled('div', `
|
const cssMenuHeader = styled('div', `
|
||||||
|
height: 40px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
margin: 0 8px;
|
margin: 0 16px;
|
||||||
`);
|
`);
|
||||||
const cssSelectAll = styled('div', `
|
const cssSelectAll = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
color: ${colors.lightGreen};
|
color: ${colors.lightGreen};
|
||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
&-disabled {
|
||||||
|
color: ${colors.slate};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const cssDotSeparator = styled('span', `
|
||||||
|
color: ${colors.lightGreen};
|
||||||
|
margin: 0 4px;
|
||||||
`);
|
`);
|
||||||
const cssMenuDivider = styled(menuDivider, `
|
const cssMenuDivider = styled(menuDivider, `
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin: 8px 0;
|
margin: 0;
|
||||||
`);
|
`);
|
||||||
const cssItemList = styled('div', `
|
const cssItemList = styled('div', `
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 8px; /* Space for scrollbar */
|
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-bottom: 8px;
|
||||||
`);
|
`);
|
||||||
const cssMenuItem = styled('div', `
|
const cssMenuItem = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px 8px;
|
padding: 8px 16px;
|
||||||
`);
|
`);
|
||||||
const cssItemValue = styled(cssLabelText, `
|
const cssItemValue = styled(cssLabelText, `
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
@ -256,8 +306,11 @@ const cssItemCount = styled('div', `
|
|||||||
`);
|
`);
|
||||||
const cssMenuFooter = styled('div', `
|
const cssMenuFooter = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 8px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
& .${cssMenuItem.className} {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
const cssApplyButton = styled(primaryButton, `
|
const cssApplyButton = styled(primaryButton, `
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
@ -268,7 +321,7 @@ const cssSearch = styled(input, `
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
|
|
||||||
font-size: ${vars.controlFontSize};
|
font-size: ${vars.mediumFontSize};
|
||||||
|
|
||||||
margin: 0px 16px 0px 8px;
|
margin: 0px 16px 0px 8px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
Loading…
Reference in New Issue
Block a user