diff --git a/e2e-tests/elementView.spec.ts b/e2e-tests/elementView.spec.ts index c5f889c2..123b0664 100644 --- a/e2e-tests/elementView.spec.ts +++ b/e2e-tests/elementView.spec.ts @@ -208,7 +208,6 @@ test('Query Selection', async ({ page }) => { await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193'); await page.getByLabel('Element View Sidebar Toggle').click(); await page.locator('[id="Subset_School\\~\\&\\~Male"] g').filter({ hasText: /^Blue Hair$/ }).locator('circle').click(); - await page.getByLabel('Selected intersection School').click(); // Selected elements for testing const ralphCell = page.getByRole('cell', { name: 'Ralph' }); diff --git a/packages/upset/src/components/ElementView/BookmarkChips.tsx b/packages/upset/src/components/ElementView/BookmarkChips.tsx index 6eb62a72..a3c367f1 100644 --- a/packages/upset/src/components/ElementView/BookmarkChips.tsx +++ b/packages/upset/src/components/ElementView/BookmarkChips.tsx @@ -115,6 +115,7 @@ export const BookmarkChips = () => { }); } }} + onClick={() => actions.setSelected(null)} label={`${currentIntersectionDisplayName} - ${currentIntersection.size}`} onDelete={() => { actions.addBookmark({ @@ -144,6 +145,7 @@ export const BookmarkChips = () => { actions.addBookmark(structuredClone(currentSelection)); } }} + onClick={() => actions.setElementSelection(null)} label={`${currentSelection.label}`} onDelete={() => { actions.addBookmark(structuredClone(currentSelection)); diff --git a/packages/upset/src/components/ElementView/ElementSidebar.tsx b/packages/upset/src/components/ElementView/ElementSidebar.tsx index a3b82305..4df141a9 100644 --- a/packages/upset/src/components/ElementView/ElementSidebar.tsx +++ b/packages/upset/src/components/ElementView/ElementSidebar.tsx @@ -22,6 +22,7 @@ import { ElementVisualization } from './ElementVisualization'; import { UpsetActions } from '../../provenance'; import { ProvenanceContext } from '../Root'; import { QueryInterface } from './QueryInterface'; +import { bookmarkSelector, currentIntersectionSelector } from '../../atoms/config/currentIntersectionAtom'; /** * Props for the ElementSidebar component @@ -33,8 +34,14 @@ type Props = { close: () => void } -const initialDrawerWidth = 450; -const minDrawerWidth = 100; +/** + * The *exact* width at which we don't get a horizontal scrollbar in the table controls + */ +const initialDrawerWidth = 462; +/** + * The *exact* width at which the 'apply' button in the element query controls is forced onto a new line + */ +const minDrawerWidth = 368; /** * Immediately downloads a csv containing items with the given columns @@ -80,12 +87,14 @@ function downloadElementsAsCSV(items: Item[], columns: string[], name: string) { export const ElementSidebar = ({ open, close }: Props) => { const [fullWidth, setFullWidth] = useState(false); const [drawerWidth, setDrawerWidth] = useState(initialDrawerWidth); - const currentSelection = useRecoilValue(selectedElementSelector); + const currentElementSelection = useRecoilValue(selectedElementSelector); const selectedItems = useRecoilValue(selectedItemsSelector); const itemCount = useRecoilValue(selectedItemsCounter); const columns = useRecoilValue(columnsAtom); const [hideElementSidebar, setHideElementSidebar] = useState(!open); const { actions }: {actions: UpsetActions} = useContext(ProvenanceContext); + const bookmarked = useRecoilValue(bookmarkSelector); + const currentIntersection = useRecoilValue(currentIntersectionSelector); /** * Effects @@ -156,6 +165,9 @@ export const ElementSidebar = ({ open, close }: Props) => { left: 0, zIndex: 100, backgroundColor: '#f4f7f9', + // I cannot comprehend why this is the value that works. It is. + // The 'rows per page' controls overflow otherwise (: + paddingBottom: '1625px', }} onMouseDown={(e) => handleMouseDown(e)} /> @@ -189,7 +201,7 @@ export const ElementSidebar = ({ open, close }: Props) => { { setHideElementSidebar(true); - actions.setElementSelection(currentSelection); + actions.setElementSelection(currentElementSelection); close(); }} aria-label="Close the sidebar" @@ -198,16 +210,20 @@ export const ElementSidebar = ({ open, close }: Props) => {
- + Element View
- - Bookmarked Queries - - - + {(bookmarked.length > 0 || currentIntersection || currentElementSelection) && ( + <> + + Bookmarked Queries + + + + + )} Element Visualization @@ -226,7 +242,7 @@ export const ElementSidebar = ({ open, close }: Props) => { downloadElementsAsCSV( selectedItems, columns, - currentSelection?.label ?? 'upset_elements', + currentElementSelection?.label ?? 'upset_elements', ); }} > diff --git a/packages/upset/src/components/Header/AttributeButton.tsx b/packages/upset/src/components/Header/AttributeButton.tsx index 90a3fc32..cd694779 100644 --- a/packages/upset/src/components/Header/AttributeButton.tsx +++ b/packages/upset/src/components/Header/AttributeButton.tsx @@ -1,4 +1,4 @@ -import { FC, useContext } from 'react'; +import React, { FC, useContext } from 'react'; import { useSetRecoilState, useRecoilValue } from 'recoil'; import { SortByOrder, AttributePlotType } from '@visdesignlab/upset2-core'; @@ -63,12 +63,14 @@ export const AttributeButton: FC = ({ label, tooltip }) => { * If the attribute is not currently sorted, it sorts it in ascending order. * If the attribute is already sorted, it toggles between ascending and descending order. */ - const handleOnClick = () => { + const handleOnClick = (e: React.MouseEvent) => { if (sortBy !== label) { sortByHeader('Ascending'); } else { sortByHeader(sortByOrder === 'Ascending' ? 'Descending' : 'Ascending'); } + // To prevent the handler on SvgBase that deselects the current intersection + e.stopPropagation(); }; /** diff --git a/packages/upset/src/components/Header/DegreeHeader.tsx b/packages/upset/src/components/Header/DegreeHeader.tsx index 1bbf3645..78f58030 100644 --- a/packages/upset/src/components/Header/DegreeHeader.tsx +++ b/packages/upset/src/components/Header/DegreeHeader.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { useContext } from 'react'; +import React, { useContext } from 'react'; import { Tooltip } from '@mui/material'; import { sortByOrderSelector, sortBySelector } from '../../atoms/config/sortByAtom'; import translate from '../../utils/transform'; @@ -23,12 +23,14 @@ export const DegreeHeader = () => { actions.sortBy('Degree', order); }; - const handleOnClick = () => { + const handleOnClick = (e: React.MouseEvent) => { if (sortBy !== 'Degree') { sortByDegree('Ascending'); } else { sortByDegree(sortByOrder === 'Ascending' ? 'Descending' : 'Ascending'); } + // To prevent the handler on SvgBase that deselects the current intersection + e.stopPropagation(); }; const handleContextMenuClose = () => { diff --git a/packages/upset/src/components/Header/SizeHeader.tsx b/packages/upset/src/components/Header/SizeHeader.tsx index 893844ac..9c1327cb 100644 --- a/packages/upset/src/components/Header/SizeHeader.tsx +++ b/packages/upset/src/components/Header/SizeHeader.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import { drag } from 'd3-drag'; import { select } from 'd3-selection'; -import { +import React, { FC, useContext, useEffect, useRef, useState, } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; @@ -51,12 +51,14 @@ export const SizeHeader: FC = () => { actions.sortBy('Size', order); }; - const handleOnClick = () => { + const handleOnClick = (e: React.MouseEvent) => { if (sortBy !== 'Size') { sortBySize('Ascending'); } else { sortBySize(sortByOrder === 'Ascending' ? 'Descending' : 'Ascending'); } + // To prevent the handler on SvgBase that deselects the current intersection + e.stopPropagation(); }; const handleContextMenuClose = () => { diff --git a/packages/upset/src/components/SvgBase.tsx b/packages/upset/src/components/SvgBase.tsx index 99bec98d..c5eda997 100644 --- a/packages/upset/src/components/SvgBase.tsx +++ b/packages/upset/src/components/SvgBase.tsx @@ -1,9 +1,11 @@ import { css } from '@emotion/react'; -import { FC } from 'react'; +import { FC, useContext } from 'react'; import { useRecoilValue } from 'recoil'; import translate from '../utils/transform'; import { dimensionsSelector } from '../atoms/dimensionsAtom'; +import { ProvenanceContext } from './Root'; +import { currentIntersectionSelector } from '../atoms/config/currentIntersectionAtom'; /** @jsxImportSource @emotion/react */ type SvgBaseSettings = { @@ -18,13 +20,18 @@ type Props = { export const SvgBase: FC = ({ children, defaultSettings }) => { const { height, width, margin } = defaultSettings || useRecoilValue(dimensionsSelector); + const { actions } = useContext(ProvenanceContext); + const selectedIntersection = useRecoilValue(currentIntersectionSelector); return ( + // These rules are for accessibility; unnecessary here as the plot is not accessible anyway. + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
{ if (selectedIntersection != null) actions.setSelected(null); }} >