zotero/scss/components/_virtualized-table.scss
Dan Stillman 962e1334c8
Some checks are pending
CI / Build, Upload, Test (push) Waiting to run
Inset item tree focus ring so its top isn't clipped
Regressed by f4adb452ee, which dropped the items-tree body's top padding
to 0 for sticky section headers; the scroll container's overflow then
clipped the top of the first row's focus ring, which drew outside the
row box.
2026-06-23 09:35:25 -04:00

524 lines
11 KiB
SCSS

//
// Virtualized table
// --------------------------------------------------
/**
<hbox class="virtualized-table-container" flex="1">
<html:div id="virtualized-table-div"/>
</hbox>
*/
.virtualized-table-container {
display: flex;
height: 0;
flex-direction: column;
> div {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
background: var(--material-background);
}
}
.virtualized-table, .drag-image-container {
width: 100%;
display: flex;
flex-direction: column;
position: relative;
// Used in virtualized-table.jsx when setting column widths
// to reserve extra space for the first column. See onResize()
--extra-width: 0px;
--first-column-extra-width: 0px;
&:focus {
outline: none;
}
&.resizing {
cursor: col-resize;
.cell {
cursor: col-resize;
}
}
.cell {
// NOTE: To avoid the complexities of dealing with padding in the
// flexbox model, `.cell` padding must be identical in the header and
// body. See #5198, #5162.
padding: 0 8px;
min-width: 30px;
cursor: default;
white-space: nowrap;
flex-grow: 1;
box-sizing: border-box;
&.label {
padding-inline-start: 0;
padding-inline-end: 4px;
}
&:first-child {
--extra-width: var(--first-column-extra-width, 0px);
// No padding on the first cell since it's done via twisty and indent
padding-inline-start: 0;
}
&:first-child,
&.primary {
display: flex;
align-items: center;
:not(.cell-text) {
flex-shrink: 0
}
.cell-text {
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.twisty + .cell-text, .spacer-twisty + .cell-text {
margin-inline-start: 0;
margin-inline-end: 0;
}
// Add margin on the end side if the directionality of the
// .cell-text is the opposite of that of the table
&:dir(ltr) .cell-text:dir(rtl),
&:dir(rtl) .cell-text:dir(ltr) {
margin-inline-end: 5px;
}
&:dir(ltr) .cell-text {
text-align: left;
}
&:dir(rtl) .cell-text {
text-align: right;
}
}
.cell-icon {
min-width: 16px;
}
}
.row {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
box-sizing: border-box;
border-radius: 5px;
&.drop:not(.flashing) {
color: var(--material-background) !important;
background: var(--fill-secondary) !important;
* {
pointer-events: none !important;
}
}
span.drop-before, span.drop-after {
position: absolute;
width: 20%;
height: 1px;
background-color: var(--fill-secondary);
pointer-events: none;
}
span.drop-before {
top: 0;
}
span.drop-after {
bottom: -1px;
}
&.selected {
background-color: var(--color-accent);
color: var(--color-accent-text);
@include state(".virtualized-table:not(:focus-within)") {
color: var(--fill-primary);
background-color: var(--color-quarternary-on-sidepane);
}
}
&.highlighted:not(.selected) {
background: var(--highlight-color, var(--accent-highlight)) !important;
}
&.unread {
font-weight: 600;
}
&.context-row:not(.selected) {
color: var(--fill-secondary);
}
.spacer-twisty {
display: inline-block;
min-width: 16px;
}
.twisty {
margin-inline-end: 0 !important;
display: flex;
align-items: center;
transition: transform 0.125s ease;
transform: rotate(-90deg);
&.open {
transform: rotate(0deg) !important;
}
&.icon-css {
width: 16px;
height: 16px;
padding: 4px;
box-sizing: border-box;
background: url("chrome://zotero/skin/8/universal/chevron-8.svg") content-box no-repeat;
-moz-context-properties: fill, fill-opacity;
}
}
}
.column-drag-marker {
z-index: 99999;
position: absolute;
top: 0;
height: 100%;
width: 2px;
background-color: #ccc;
}
.twisty {
width: 19px;
svg {
fill: #888;
width: 16px;
}
}
.selected:not(.highlighted) .twisty svg {
fill: #fff;
}
.spacer-twisty {
min-width: 19px;
}
.spacer-header {
min-width: 0px;
}
}
.virtualized-table.multi-select:focus {
.row.focused {
border: 1px dotted highlight;
z-index: 10000;
> *:first-child {
margin-inline-start: -1px;
}
> *:last-child {
margin-inline-end: -1px;
}
}
}
.virtualized-table-header {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
background: var(--material-background);
height: 1.83333333em; // 22px @ 12px font size
overflow: hidden;
border-bottom: 1px solid var(--material-border-quarternary);
padding-inline: 8px;
box-sizing: border-box;
padding-inline-end: calc(8px + var(--scrollbar-width, 0px));
@include comfortable {
height: 2.33333333em; // 28px @ 12px font size
padding: 0 8px;
}
&.static-columns {
pointer-events: none;
.cell {
&:hover {
background: inherit;
}
}
[title] {
pointer-events: auto;
}
}
&::after {
content: "";
display: block;
border-bottom: var(--material-border-quarternary);
height: 1px;
width: 100%;
position: absolute;
top: calc(1.83333333em - 1px);
left: 0;
right: 0;
z-index: 1;
@include comfortable {
top: calc(2.33333333em - 1px);
}
}
.column-picker {
text-align: center;
}
.cell:first-child > :first-child {
// offset header's column label/icon to align with the text in the first column, without introducing padding to the cell itself to avoid flexbox issues.
// The extra 8px (COLUMN_PADDING / 2) makes up for the inline-start padding that the first cell omits.
padding-inline-start: calc(var(--first-column-extra-width, 0px) + 8px);
}
.cell {
display: flex;
position: relative;
height: 100%;
align-items: center;
&:hover {
background: var(--material-mix-quinary);
}
&.dragging {
background: var(--material-mix-quarternary);
}
.resizer {
background: linear-gradient(var(--fill-quarternary), var(--fill-quarternary)) no-repeat center/1px 66.666667%; // 14px @ 12px font size
cursor: col-resize;
height: 100%;
content: "\00a0";
display: block;
position: absolute;
left: -5px;
min-width: 10px;
@include comfortable {
background-size: 1px 74.074074%; // 20px @ 12px font size
}
}
.label {
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
&.cell-icon {
> .label {
margin-inline-start: 0;
display: flex;
justify-content: center;
}
.icon-css {
fill: var(--fill-secondary);
}
padding: 0;
}
.sort-indicator {
-moz-appearance: toolbarbutton-dropdown;
display: block;
margin-right: 3px; // + 5px padding = 8px total
&.icon-css {
background: url("chrome://zotero/skin/8/universal/chevron-8.svg") content-box no-repeat;
-moz-context-properties: fill, fill-opacity;
fill: var(--fill-primary);
}
&.ascending {
transform: rotate(180deg);
}
}
}
}
// Pinned copy of the current section's header row. It's the first child of the scrolling
// body and pins itself with position: sticky, so its width comes from the body's content box
// for free and it lines up with the real rows without any JS geometry. It has zero height --
// so it takes no space in the flow, doesn't shift the rows down, and pins flush with the top
// of the rows -- and its content overflows downward over the rows. (The body's scrollbar is
// kept clear of it by the reserved scrollbar gutter, not by paint order; see below.)
.virtualized-table-sticky-section-header {
position: sticky;
top: 0;
height: 0;
// Above the rows scrolling under it, including a focused row (z-index: 10000), so they
// pass underneath the pinned header rather than over it
z-index: 10001;
// The zero-height outer box never overlaps anything, so it takes no pointer events;
// the opaque content below sets pointer-events: auto so it absorbs them
pointer-events: none;
.virtualized-table-sticky-section-header-content {
box-sizing: border-box;
// Opaque so the pinned header occludes the rows (and the real header row) beneath it,
// and opaque to pointer events too, so clicks, drags, and drops land on the pinned
// header rather than passing through to the occluded row underneath
background-color: var(--material-background);
pointer-events: auto;
.row {
position: static;
}
}
}
.virtualized-table-body, .drag-image-container {
flex: 1 0;
max-width: 100%;
overflow: auto;
padding: 4px 8px 8px;
// With sticky section headers, reserve the scrollbar's track so it doesn't float over the
// content. The pinned header is opaque and must paint above the rows to occlude them, which
// in Gecko also paints it above the scrollbar; keeping the scrollbar out of the content box
// is what stops it from covering the scrollbar's edge (see the sticky header rules above).
// The body must also have no top padding so rows can't scroll up into it and show above the
// pinned header; that's handled per-consumer (e.g. #zotero-items-tree) where the padding is
// set with higher specificity.
&.has-sticky-section-headers {
scrollbar-gutter: stable;
}
.cell {
// NOTE: Do not add padding here. See #5198
text-overflow: ellipsis;
overflow: hidden;
max-height: 100%;
}
}
.virtualized-table.multi-select:focus {
.row.focused:not(.selected) {
outline: var(--default-focusring-width) dotted var(--color-accent);
// Inset the ring so it draws inside the row box rather than outside it, where the
// scroll container's overflow clip would cut off the top edge on the first row (the
// body has no top padding with sticky section headers)
outline-offset: calc(-1 * var(--default-focusring-width));
z-index: 10000;
}
}
*[dir=rtl] {
.virtualized-table-header {
.cell .sort-indicator {
left: 3px; // + 5px padding = 8px total
right: initial;
}
.resizer {
right: -5px;
left: initial;
}
}
.row .twisty {
transform: rotate(90deg);
}
}
.virtualized-table.focus-states-target {
.row .twisty.icon-css {
@include focus-states using ($color) {
@if $color =="white" {
fill: var(--accent-white);
}
@else {
fill: var(--fill-secondary);
}
}
}
}
.drag-image-container {
box-sizing: border-box;
.row .twisty.icon-css {
@include focus-states('.row.selected', '.drag-image-container:focus-within') using ($color) {
fill: var(--fill-secondary);
}
}
}
// Styling for clickable button-cells
.virtualized-table {
// the cell with the clickable button
.cell.clickable {
// make sure the button is centered
display: flex;
justify-content: center;
padding: 0;
// the actual clickable button
.icon-action {
display: flex;
justify-content: center;
width: 20px;
height: 20px;
align-items: center;
border-radius: 6px;
.icon {
width: 16px;
height: 16px;
}
&:hover:not([disabled]) {
background-color: var(--fill-quinary);
}
&:active:not([disabled]) {
background-color: var(--fill-quarternary);
}
&[disabled] {
color: var(--color-gray-50);
}
// show focus ring around the actual icon so that the outline won't overlap with other rows
&:focus-visible {
outline: none;
.icon {
border-radius: 5px;
outline: var(--default-focusring-width) solid var(--color-accent);
}
}
}
}
// lighter hover and active effects on clickable buttons in selected rows
.row.selected {
.icon-action {
&:hover:not([disabled]) {
background-color: #ffffff1a;
}
&:active:not([disabled]) {
background-color: #ffffff33;
}
}
}
}