diff --git a/src/assets/styles/routes/datasets.scss b/src/assets/styles/routes/datasets.scss
index 091c2fa..453a0c8 100644
--- a/src/assets/styles/routes/datasets.scss
+++ b/src/assets/styles/routes/datasets.scss
@@ -1185,7 +1185,7 @@
.dataset-table-context-menu {
position: fixed;
z-index: 9999;
- min-width: 10rem;
+ min-width: 14rem;
padding: 0.25rem 0;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.12);
@@ -1193,7 +1193,9 @@
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
&__item {
- display: block;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.85rem;
border: none;
@@ -1202,10 +1204,30 @@
font-size: 0.875rem;
cursor: pointer;
- &:hover {
+ &:hover,
+ &:focus-visible {
+ cursor: pointer;
background: $color_bg-light;
}
}
+
+ &__icon {
+ display: inline-flex;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center;
+ width: 1rem;
+ color: $color_font-medium;
+ cursor: pointer;
+ pointer-events: none;
+ }
+
+ &__label {
+ flex: 1;
+ min-width: 0;
+ cursor: pointer;
+ pointer-events: none;
+ }
}
.table-wrapper {
@@ -1473,57 +1495,50 @@
gap: 0.2rem;
}
- .sort-icon-wrap {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 1.35rem;
- height: 1.35rem;
- margin-left: 0;
- }
-
- .sort-icon {
- font-size: 0.8125rem;
- line-height: 1;
- color: $color_font-medium;
- vertical-align: middle;
-
- svg {
- display: block;
- }
-
- &--inactive {
- opacity: 0.45;
- }
-
- &--active {
- opacity: 0.9;
- }
- }
-
- .dataset-table__hide-btn {
+ .dataset-table__header-menu-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: none;
+ border-radius: 4px;
background: transparent;
color: $color_font-medium;
- font-size: 1rem;
- line-height: 1;
width: 1.35rem;
height: 1.35rem;
padding: 0;
cursor: pointer;
- opacity: 0;
- transition: opacity 0.15s ease;
+ opacity: 0.55;
+ transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease;
+
+ &:hover,
+ &:focus-visible {
+ cursor: pointer;
+ }
&:hover {
+ opacity: 1;
+ background: rgba($color_brand-secondary, 0.1);
+ color: $color_brand-secondary--dark;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $color_brand-secondary;
+ outline-offset: 1px;
+ opacity: 1;
+ }
+
+ svg {
+ pointer-events: none;
+ }
+
+ &--sorted {
+ opacity: 0.9;
color: $color_brand-secondary--dark;
}
}
- th:hover .dataset-table__hide-btn {
+ th:hover .dataset-table__header-menu-btn {
opacity: 0.9;
}
diff --git a/src/components/partials/DatasetTable.jsx b/src/components/partials/DatasetTable.jsx
index 8747606..1f296a2 100644
--- a/src/components/partials/DatasetTable.jsx
+++ b/src/components/partials/DatasetTable.jsx
@@ -1,12 +1,19 @@
import React from "react";
import PropTypes from "prop-types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faArrowDown, faArrowUp, faArrowsUpDown, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
+import {
+ faArrowDown,
+ faArrowUp,
+ faChevronDown,
+ faEyeSlash,
+} from "@fortawesome/free-solid-svg-icons";
import DataRow from "./DataRow";
import DatasetTableContextMenu from "./DatasetTableContextMenu";
import {
applyPreviewRowOrder,
getDatasetRowKey,
+ isVisibleColumnOrderCustom,
+ mergeVisibleColumnReorder,
orderColumnKeys,
reorderList,
} from "../../utils/datasetTablePreview";
@@ -26,7 +33,7 @@ class DatasetTable extends React.Component {
this.onPageNumberUpdate = this.onPageNumberUpdate.bind(this);
this.onPageNumberBlur = this.onPageNumberBlur.bind(this);
this.closeContextMenu = this.closeContextMenu.bind(this);
- this.openColumnContextMenu = this.openColumnContextMenu.bind(this);
+ this.openColumnHeaderMenu = this.openColumnHeaderMenu.bind(this);
}
componentDidUpdate(prevProps) {
@@ -39,25 +46,39 @@ class DatasetTable extends React.Component {
this.setState({ contextMenu: null });
}
- openColumnContextMenu(e, column) {
+ openColumnHeaderMenu(e, column) {
e.preventDefault();
e.stopPropagation();
- const { updateSelectedColumns } = this.props;
- if (!updateSelectedColumns) return;
+ const { sortColumn, sortDirection } = this.state;
+ const { updateSelectedColumns, selectedColumns } = this.props;
+ const rect = e.currentTarget.getBoundingClientRect();
+ const isSorted = sortColumn === column.name;
+ const sortAscendingNext = !isSorted || sortDirection === "desc";
+
+ const items = [
+ {
+ label: sortAscendingNext
+ ? "Sort this column in ascending order"
+ : "Sort this column in descending order",
+ icon: sortAscendingNext ? faArrowUp : faArrowDown,
+ onSelect: () => this.handleSort(column.name),
+ },
+ ];
+
+ if (updateSelectedColumns && selectedColumns.includes(column.name)) {
+ items.push({
+ label: "Hide this column",
+ icon: faEyeSlash,
+ onSelect: () => updateSelectedColumns(column.name),
+ });
+ }
+
this.setState({
contextMenu: {
- x: e.clientX,
- y: e.clientY,
- items: [
- {
- label: "Hide column",
- onSelect: () => {
- if (this.props.selectedColumns.includes(column.name)) {
- updateSelectedColumns(column.name);
- }
- },
- },
- ],
+ x: rect.left,
+ y: rect.bottom + 4,
+ columnName: column.name,
+ items,
},
});
}
@@ -79,28 +100,12 @@ class DatasetTable extends React.Component {
});
}
- getSortIcon(columnName) {
+ getHeaderMenuIcon(columnName) {
const { sortColumn, sortDirection } = this.state;
if (sortColumn !== columnName) {
- return ;
+ return faChevronDown;
}
- return sortDirection === "asc" ? (
-
- ) : (
-
- );
- }
-
- getSortTooltip(columnName, columnAlias) {
- const { sortColumn, sortDirection } = this.state;
- const label = columnAlias || columnName;
- if (sortColumn !== columnName) {
- return `Sort ascending by ${label}`;
- }
- if (sortDirection === "asc") {
- return `Sorted ascending by ${label}. Click to sort descending.`;
- }
- return `Sorted descending by ${label}. Click to sort ascending.`;
+ return sortDirection === "asc" ? faArrowUp : faArrowDown;
}
getAriaSort(columnName) {
@@ -109,12 +114,53 @@ class DatasetTable extends React.Component {
return sortDirection === "asc" ? "ascending" : "descending";
}
- handleHideColumnClick(e, column) {
- e.preventDefault();
- e.stopPropagation();
- const { updateSelectedColumns, selectedColumns } = this.props;
- if (!updateSelectedColumns || !selectedColumns.includes(column.name)) return;
- updateSelectedColumns(column.name);
+ renderVisibleHeader(header, visibleIndex, columnNames) {
+ const { sortColumn } = this.state;
+ const isSorted = sortColumn === header.name;
+
+ return (
+
this.handleColumnDrop(e, visibleIndex, columnNames)}
+ >
+
+ this.handleColumnDragStart(e, visibleIndex)}
+ onDragEnd={() => this.setState({ dragColumnIndex: null })}
+ onMouseDown={(e) => e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()}
+ title="Drag to reorder column"
+ aria-label="Drag to reorder column"
+ />
+ {header.alias}
+
+
+
+
+ |
+ );
+ }
+
+ setTableHeaders(orderedColumnKeys) {
+ const columnNames = orderedColumnKeys.map((col) => col.name);
+ return orderedColumnKeys.map((header, index) =>
+ this.renderVisibleHeader(header, index, columnNames),
+ );
}
handleColumnDragStart(e, index) {
@@ -134,14 +180,18 @@ class DatasetTable extends React.Component {
const { previewColumnOrder, onPreviewColumnOrderChange } = this.props;
if (dragColumnIndex == null || !onPreviewColumnOrderChange || !columnNames?.length) return;
- const order = previewColumnOrder?.length
+ const visibleOrder = previewColumnOrder?.length
? previewColumnOrder.filter((name) => columnNames.includes(name))
: [...columnNames];
columnNames.forEach((name) => {
- if (!order.includes(name)) order.push(name);
+ if (!visibleOrder.includes(name)) visibleOrder.push(name);
});
- onPreviewColumnOrderChange(reorderList(order, dragColumnIndex, toIndex));
+ const reorderedVisible = reorderList(visibleOrder, dragColumnIndex, toIndex);
+ const fullOrder = previewColumnOrder?.length ? previewColumnOrder : [...columnNames];
+ const nextOrder = mergeVisibleColumnReorder(fullOrder, columnNames, reorderedVisible);
+
+ onPreviewColumnOrderChange(nextOrder);
this.setState({ dragColumnIndex: null });
}
@@ -171,50 +221,6 @@ class DatasetTable extends React.Component {
this.setState({ dragRowIndex: null });
}
- setTableHeaders(orderedColumnKeys) {
- const columnNames = orderedColumnKeys.map((col) => col.name);
- return orderedColumnKeys.map((header, index) => (
- this.handleSort(header.name)}
- onContextMenu={(e) => this.openColumnContextMenu(e, header)}
- onDragOver={this.handleColumnDragOver}
- onDrop={(e) => this.handleColumnDrop(e, index, columnNames)}
- style={{ cursor: "pointer" }}
- >
-
- this.handleColumnDragStart(e, index)}
- onDragEnd={() => this.setState({ dragColumnIndex: null })}
- onMouseDown={(e) => e.stopPropagation()}
- onClick={(e) => e.stopPropagation()}
- title="Drag to reorder column"
- aria-label="Drag to reorder column"
- />
- {header.alias}
-
-
- {this.getSortIcon(header.name)}
-
-
-
-
- |
- ));
- }
sortData(data, columnName, direction) {
if (!columnName) return data;
@@ -302,29 +308,15 @@ class DatasetTable extends React.Component {
const { sortColumn, sortDirection, inputPageNum, contextMenu, dragRowIndex } = this.state;
const orderedColumnKeys = orderColumnKeys(columnKeys, selectedColumns, previewColumnOrder);
+ const hasVisibleColumns = orderedColumnKeys.length > 0;
// Avoid a broken table (row drag gutter only) when all columns are deselected.
- if (columnKeys.length > 0 && orderedColumnKeys.length === 0) {
- const showRowPreviewControls = Boolean(updateSelectedColumns || onPreviewRowOrderChange);
- const canCustomizeLayout = Boolean(
- showRowPreviewControls || onPreviewColumnOrderChange,
- );
- const defaultColumnNames = orderedColumnKeys.map((col) => col.name);
- const columnOrderCustom =
- previewColumnOrder.length > 0 && previewColumnOrder.join("|") !== defaultColumnNames.join("|");
- const hasPreviewCustomization = columnOrderCustom || previewRowOrder.length > 0;
+ if (columnKeys.length > 0 && !hasVisibleColumns) {
const isEmbedView = new URLSearchParams(location.search).get("embed") === "1";
return (
- {canCustomizeLayout && hasPreviewCustomization && onResetPreviewLayout ? (
-
-
-
- ) : null}
@@ -352,6 +344,7 @@ class DatasetTable extends React.Component {
);
}
+ const visibleColumnNames = orderedColumnKeys.map((col) => col.name);
const renderedHeaders = this.setTableHeaders(orderedColumnKeys);
const selectedYearsSet = new Set(selectedYears);
@@ -391,7 +384,7 @@ class DatasetTable extends React.Component {
key.name)}
+ headers={visibleColumnNames}
linkRowsToDatasetView={linkRowsToDatasetView}
showRowDragControls={showRowDragControls}
isDragging={dragRowIndex === i}
@@ -409,15 +402,17 @@ class DatasetTable extends React.Component {
const backButtonClasses = currentPage === 1 ? "button-wrapper lift disabled" : "button-wrapper lift";
const forwardButtonClasses = currentPage === numOfPages ? "button-wrapper list disabled" : "button-wrapper lift";
const isEmbedView = new URLSearchParams(location.search).get("embed") === "1";
- const defaultColumnNames = orderedColumnKeys.map((col) => col.name);
- const columnOrderCustom =
- previewColumnOrder.length > 0 && previewColumnOrder.join("|") !== defaultColumnNames.join("|");
+ const columnOrderCustom = isVisibleColumnOrderCustom(
+ previewColumnOrder,
+ visibleColumnNames,
+ columnKeys,
+ );
const hasPreviewCustomization = columnOrderCustom || previewRowOrder.length > 0;
return (
- {canCustomizeLayout && hasPreviewCustomization && onResetPreviewLayout ? (
+ {hasVisibleColumns && canCustomizeLayout && hasPreviewCustomization && onResetPreviewLayout ? (
@@ -48,6 +54,7 @@ DatasetTableContextMenu.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
+ icon: PropTypes.object,
onSelect: PropTypes.func.isRequired,
}),
).isRequired,
diff --git a/src/utils/datasetTablePreview.js b/src/utils/datasetTablePreview.js
index 46b3f87..1b8dda2 100644
--- a/src/utils/datasetTablePreview.js
+++ b/src/utils/datasetTablePreview.js
@@ -35,21 +35,70 @@ export function getDatasetRowKey(row, fallbackIndex = 0) {
}
}
-/** Keep custom column order in sync when selection changes. */
-export function syncPreviewColumnOrder(prevOrder, selectedColumns, columnKeys) {
- const selectedSet = new Set(selectedColumns || []);
- const kept = (prevOrder || []).filter((name) => selectedSet.has(name));
- const keptSet = new Set(kept);
- const added = (columnKeys || [])
+/**
+ * user selected column(includes hidden/deselected columns).
+ * Deselected columns stay in place so re-adding restores their prior position.
+ */
+export function syncPreviewColumnOrder(prevOrder, _selectedColumns, columnKeys) {
+ const allColumnNames = columnKeys.map((col) => col.name);
+ const columnKeySet = new Set(allColumnNames);
+ // Filter out columns that are not in the columnKeys
+ const order = prevOrder.filter((name) => columnKeySet.has(name));
+
+ allColumnNames.forEach((name) => {
+ if (!order.includes(name)) {
+ order.push(name);
+ }
+ });
+
+ return order;
+}
+
+/**
+ * Apply a reorder of visible columns while keeping hidden columns at their slots.
+ */
+export function mergeVisibleColumnReorder(fullOrder, visibleColumnNames, reorderedVisible) {
+ const visibleSet = new Set(visibleColumnNames);
+ const queue = [...reorderedVisible];
+ const merged = [];
+
+ fullOrder.forEach((name) => {
+ if (visibleSet.has(name)) {
+ if (queue.length) merged.push(queue.shift());
+ } else {
+ merged.push(name);
+ }
+ });
+
+ queue.forEach((name) => merged.push(name));
+ return merged;
+}
+
+/**
+ * True when visible columns are ordered differently than metadata default.
+ * Compares layout (previewColumnOrder) to columnKeys order — not to the already-rendered order.
+ */
+export function isVisibleColumnOrderCustom(previewColumnOrder, visibleColumnNames, columnKeys) {
+ if (!visibleColumnNames?.length) return false;
+
+ const visibleSet = new Set(visibleColumnNames);
+ const defaultVisibleOrder = (columnKeys || [])
.map((col) => col.name)
- .filter((name) => selectedSet.has(name) && !keptSet.has(name));
- return [...kept, ...added];
+ .filter((name) => visibleSet.has(name));
+
+ const layoutVisibleOrder = (previewColumnOrder || []).filter((name) => visibleSet.has(name));
+ const currentVisibleOrder =
+ layoutVisibleOrder.length === defaultVisibleOrder.length
+ ? layoutVisibleOrder
+ : defaultVisibleOrder;
+
+ return currentVisibleOrder.join("|") !== defaultVisibleOrder.join("|");
}
export function orderColumnKeys(columnKeys, selectedColumns, previewColumnOrder) {
const selectedSet = new Set(selectedColumns || []);
- const visible = (columnKeys || []).filter((col) => selectedSet.has(col.name));
- if (!previewColumnOrder?.length) return visible;
+ const visible = columnKeys.filter((col) => selectedSet.has(col.name));
+ if (!previewColumnOrder.length) return visible;
const byName = new Map(visible.map((col) => [col.name, col]));
const ordered = [];
@@ -65,9 +114,9 @@ export function orderColumnKeys(columnKeys, selectedColumns, previewColumnOrder)
/** Merge custom row order with filtered rows; append rows not yet in order. */
export function applyPreviewRowOrder(rows, previewRowOrder) {
- const visible = (rows || []).map((row, i) => ({ row, key: getDatasetRowKey(row, i) }));
+ const visible = (rows).map((row, i) => ({ row, key: getDatasetRowKey(row, i) }));
- if (!previewRowOrder?.length) return visible.map(({ row }) => row);
+ if (!previewRowOrder.length) return visible.map(({ row }) => row);
const buckets = new Map();
visible.forEach(({ row, key }) => {
@@ -89,7 +138,7 @@ export function applyPreviewRowOrder(rows, previewRowOrder) {
}
export function reorderList(list, fromIndex, toIndex) {
- if (!list?.length || fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return list;
+ if (!list.length || fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return list;
const next = [...list];
if (fromIndex >= next.length || toIndex >= next.length) return list;
const [item] = next.splice(fromIndex, 1);