Add sticky selection toolbar (#6320)

This commit is contained in:
WithoutPants 2025-11-28 13:52:30 +11:00 committed by GitHub
parent d1ee64d36f
commit 1bc32a3099
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 121 additions and 32 deletions

View file

@ -8,11 +8,47 @@ import {
IListFilterOperation, IListFilterOperation,
ListOperationButtons, ListOperationButtons,
} from "./ListOperationButtons"; } from "./ListOperationButtons";
import { ButtonGroup, ButtonToolbar } from "react-bootstrap"; import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap";
import { View } from "./views"; import { View } from "./views";
import { IListSelect, useFilterOperations } from "./util"; import { IListSelect, useFilterOperations } from "./util";
import { SavedFilterDropdown } from "./SavedFilterList"; import { SavedFilterDropdown } from "./SavedFilterList";
import { FilterButton } from "./Filters/FilterButton"; import { FilterButton } from "./Filters/FilterButton";
import { Icon } from "../Shared/Icon";
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import { useIntl } from "react-intl";
import cx from "classnames";
const SelectionSection: React.FC<{
filter: ListFilterModel;
selected: number;
onSelectAll: () => void;
onSelectNone: () => void;
}> = ({ selected, onSelectAll, onSelectNone }) => {
const intl = useIntl();
return (
<div className="selected-items-info">
<Button
variant="secondary"
className="minimal"
onClick={() => onSelectNone()}
title={intl.formatMessage({ id: "actions.select_none" })}
>
<Icon icon={faTimes} />
</Button>
<span className="selected-count">{selected}</span>
<Button
variant="secondary"
className="minimal"
onClick={() => onSelectAll()}
title={intl.formatMessage({ id: "actions.select_all" })}
>
<Icon icon={faSquareCheck} />
</Button>
</div>
);
};
export interface IItemListOperation<T extends QueryResult> { export interface IItemListOperation<T extends QueryResult> {
text: string; text: string;
@ -62,9 +98,21 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
setFilter, setFilter,
}); });
const { selectedIds, onSelectAll, onSelectNone } = listSelect; const { selectedIds, onSelectAll, onSelectNone } = listSelect;
const hasSelection = selectedIds.size > 0;
return ( return (
<ButtonToolbar className="filtered-list-toolbar"> <ButtonToolbar
className={cx("filtered-list-toolbar", { "has-selection": hasSelection })}
>
{hasSelection ? (
<SelectionSection
filter={filter}
selected={selectedIds.size}
onSelectAll={onSelectAll}
onSelectNone={onSelectNone}
/>
) : (
<>
<SearchTermInput filter={filter} onFilterUpdate={setFilter} /> <SearchTermInput filter={filter} onFilterUpdate={setFilter} />
<ButtonGroup> <ButtonGroup>
@ -73,7 +121,10 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
onSetFilter={setFilter} onSetFilter={setFilter}
view={view} view={view}
/> />
<FilterButton onClick={() => showEditFilter()} count={filter.count()} /> <FilterButton
onClick={() => showEditFilter()}
count={filter.count()}
/>
</ButtonGroup> </ButtonGroup>
<SortBySelect <SortBySelect
@ -81,14 +132,20 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
sortDirection={filter.sortDirection} sortDirection={filter.sortDirection}
options={filterOptions.sortByOptions} options={filterOptions.sortByOptions}
onChangeSortBy={(e) => setFilter(filter.setSortBy(e ?? undefined))} onChangeSortBy={(e) => setFilter(filter.setSortBy(e ?? undefined))}
onChangeSortDirection={() => setFilter(filter.toggleSortDirection())} onChangeSortDirection={() =>
onReshuffleRandomSort={() => setFilter(filter.reshuffleRandomSort())} setFilter(filter.toggleSortDirection())
}
onReshuffleRandomSort={() =>
setFilter(filter.reshuffleRandomSort())
}
/> />
<PageSizeSelector <PageSizeSelector
pageSize={filter.itemsPerPage} pageSize={filter.itemsPerPage}
setPageSize={(size) => setFilter(filter.setPageSize(size))} setPageSize={(size) => setFilter(filter.setPageSize(size))}
/> />
</>
)}
<ListOperationButtons <ListOperationButtons
onSelectAll={onSelectAll} onSelectAll={onSelectAll}

View file

@ -918,9 +918,14 @@ input[type="range"].zoom-slider {
.filtered-list-toolbar { .filtered-list-toolbar {
align-items: center; align-items: center;
background-color: $body-bg;
gap: 0.5rem; gap: 0.5rem;
justify-content: center; justify-content: center;
margin-bottom: 0.5rem;
// offset the main padding
margin-top: -0.5rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
& > .btn-group { & > .btn-group {
flex-wrap: wrap; flex-wrap: wrap;
@ -1091,10 +1096,6 @@ input[type="range"].zoom-slider {
&.filtered-list-toolbar { &.filtered-list-toolbar {
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 1rem; gap: 1rem;
// offset the main padding
margin-top: -0.5rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
position: sticky; position: sticky;
top: $navbar-height; top: $navbar-height;
z-index: 10; z-index: 10;
@ -1141,11 +1142,6 @@ input[type="range"].zoom-slider {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.selected-items-info {
align-items: center;
display: flex;
}
> div:first-child, > div:first-child,
> div:last-child { > div:last-child {
flex: 1; flex: 1;
@ -1433,3 +1429,39 @@ input[type="range"].zoom-slider {
.duration-preset { .duration-preset {
cursor: pointer; cursor: pointer;
} }
.selected-items-info {
align-items: center;
border: 1px solid $secondary;
display: flex;
gap: 0.25rem;
justify-content: flex-end;
}
.scene-list-toolbar .selected-items-info {
justify-content: flex-start;
}
.item-list-container > .filtered-list-toolbar.has-selection {
border-radius: 0.5rem;
margin-left: auto;
margin-right: auto;
padding-left: 0.5rem;
padding-right: 0.5rem;
position: sticky;
top: $navbar-height;
width: fit-content;
z-index: 10;
@include media-breakpoint-down(xs) {
top: 0;
}
}
.detail-body .filtered-list-toolbar.has-selection {
top: calc($sticky-detail-header-height + $navbar-height);
@include media-breakpoint-down(xs) {
top: 0;
}
}