Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

This changelog covers all three packages, as they are (for now) updated as a whole

## URELEASED

### Atomic Browser

- Improve performance collapsed sidebar items.

### @tomic/lib

- Add `store.getResourceAncestry` method, which returns the ancestry of a resource, including the resource itself.
- Add `resource.title` property, which returns the name of a resource, or the first property that is can be used to name the resource.

#### Breaking changes

- `buildSearchSubject` now takes a serverURL instead of the store.

## v0.35.0

### @tomic/browser
Expand Down
135 changes: 26 additions & 109 deletions data-browser/src/components/Collapse.tsx
Original file line number Diff line number Diff line change
@@ -1,137 +1,54 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { timeoutEffect, timeoutEffects } from '../helpers/timeoutEffect';
import { timeoutEffect } from '../helpers/timeoutEffect';
import { animationDuration } from '../styling';

export interface CollapseProps {
open: boolean;
/** Animation speed in ms, Defaults to the animation duration defined in the theme */
duration?: number;
interface CollapseProps {
open?: boolean;
className?: string;
}

const SPEED_MODIFIER = 1.5;
// the styling file is not loaded at boot so we have to use a function here
const ANIMATION_DURATION = () => animationDuration * 1.5;

/**
* Collapsible Component that only shows `children` when `open={true}`. Animates
* the transition for `{duration}`ms. Designed to work in the sidebar with
* potentially deeply nested, recurring Collapse units.
*/
export function Collapse({
open,
children,
duration,
className,
children,
}: React.PropsWithChildren<CollapseProps>): JSX.Element {
const node = useRef<HTMLDivElement>(null);
const [initialHeight, setInitialHeight] = React.useState(0);

// We can't use a `useState` here.
// When `measureAndSet` is used as a callback for the MutationObserver,
// the scope will be outdated when the props update.
// To work around this we have to use a ref in order to reference the most up to date value.
const openRef = useRef(open);

const measureAndSet = () => {
const div = node.current;

if (!div) {
return;
}

div.style.height = 'inital';
const height = div.scrollHeight;

setInitialHeight(height);

div.style.position = 'static';

if (!openRef.current) {
div.style.height = '0px';
div.style.visibility = 'hidden';
} else {
div.style.height = `${height}px`;
div.style.visibility = 'visible';
}
};

const mutationObserver = useRef(new MutationObserver(measureAndSet));
const [mountChildren, setMountChildren] = useState(open);

const speed = duration ?? animationDuration * SPEED_MODIFIER;

// Measure the height of the element when it is added to the DOM.
const onRefConnect = useCallback((div: HTMLDivElement) => {
if (!div) return;

// @ts-ignore this works and is mutable, ts doesn't like it
node.current = div;
measureAndSet();
}, []);

// Measure the height again when a child is added or removed.
useEffect(() => {
node.current &&
mutationObserver.current.observe(node.current, { childList: true });

return () => mutationObserver.current.disconnect();
}, []);

// Perform the animation when the `open` prop changes.
useLayoutEffect(() => {
openRef.current = open;
if (!node.current) return;

const wrapper = node.current;

if (open) {
wrapper.style.visibility = 'visible';
wrapper.style.height = `${initialHeight}px`;

// after the animation has finished, remove the set height so the element can grow if it needs to.
if (!open) {
return timeoutEffect(() => {
wrapper.style.overflowY = 'visible';
wrapper.style.height = 'initial';
}, speed);
} else {
// recalculate the height as it might have changed, then set it so it can be animated from.
const newHeight = wrapper.scrollHeight;
setInitialHeight(newHeight);
wrapper.style.height = `${newHeight}px`;
wrapper.style.overflowY = 'hidden';

return timeoutEffects(
[
() => {
wrapper.style.height = '0px';
},
0,
],
[
() => {
wrapper.style.visibility = 'hidden';
},
speed,
],
);
setMountChildren(false);
}, ANIMATION_DURATION());
}

setMountChildren(true);
}, [open]);

return (
<Wrapper ref={onRefConnect} duration={speed} className={className}>
{children}
</Wrapper>
<GridCollapser open={open} className={className}>
<CollapseInner>{mountChildren && children}</CollapseInner>
</GridCollapser>
);
}

interface WrapperProps {
duration: number;
interface GridCollapserProps {
open?: boolean;
}

const Wrapper = styled.div<WrapperProps>`
overflow-y: hidden;
transition: height ${p => p.duration}ms ease-in-out;
const GridCollapser = styled.div<GridCollapserProps>`
display: grid;
grid-template-rows: ${({ open }) => (open ? '1fr' : '0fr')};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so clean, love it

transition: grid-template-rows ${() => ANIMATION_DURATION()}ms ease-in-out;
@media (prefers-reduced-motion) {
transition: unset;
}
`;

const CollapseInner = styled.div`
overflow: hidden;
`;
8 changes: 6 additions & 2 deletions data-browser/src/components/Details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ export function Details({
const [isOpen, setIsOpen] = React.useState(initialState);

useEffect(() => {
console.log('opening details');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naughty

setIsOpen(open);
}, [open]);

const toggleOpen = useCallback(() => {
onStateToggle?.(!isOpen);
setIsOpen(p => !p);
setIsOpen(p => {
onStateToggle?.(!p);

return !p;
});
}, []);

return (
Expand Down
119 changes: 65 additions & 54 deletions data-browser/src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button } from '../Button';
import { DropdownTriggerRenderFunction } from './DropdownTrigger';
import { shortcuts } from '../HotKeyWrapper';
import { Shortcut } from '../Shortcut';
import { transition } from '../../helpers/transition';

export const DIVIDER = 'divider' as const;

Expand Down Expand Up @@ -96,13 +97,17 @@ export function DropdownMenu({
const dropdownRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const [isActive, setIsActive] = useState(false);
const [visible, setVisible] = useState(false);

const handleClose = useCallback(() => {
setIsActive(false);
setVisible(false);
// Whenever the menu closes, assume that the next one will be opened with mouse
setUseKeys(false);
// Always reset to the top item on close
setSelectedIndex(0);
setTimeout(() => {
setIsActive(false);
setSelectedIndex(0);
}, 100);
}, []);

useClickAwayListener([triggerRef, dropdownRef], handleClose, isActive, [
Expand All @@ -126,27 +131,31 @@ export function DropdownMenu({
return;
}

const triggerRect = triggerRef.current!.getBoundingClientRect();
const menuRect = dropdownRef.current!.getBoundingClientRect();
const topPos = triggerRect.y - menuRect.height;
setIsActive(true);

// If the top is outside of the screen, render it below
if (topPos < 0) {
setY(triggerRect.y + triggerRect.height / 2);
} else {
setY(topPos + triggerRect.height / 2);
}
requestAnimationFrame(() => {
const triggerRect = triggerRef.current!.getBoundingClientRect();
const menuRect = dropdownRef.current!.getBoundingClientRect();
const topPos = triggerRect.y - menuRect.height;

const leftPos = triggerRect.x - menuRect.width;
// If the top is outside of the screen, render it below
if (topPos < 0) {
setY(triggerRect.y + triggerRect.height / 2);
} else {
setY(topPos + triggerRect.height / 2);
}

// If the left is outside of the screen, render it to the right
if (leftPos < 0) {
setX(triggerRect.x);
} else {
setX(triggerRect.x - menuRect.width + triggerRect.width);
}
const leftPos = triggerRect.x - menuRect.width;

setIsActive(true);
// If the left is outside of the screen, render it to the right
if (leftPos < 0) {
setX(triggerRect.x);
} else {
setX(triggerRect.x - menuRect.width + triggerRect.width);
}

setVisible(true);
});
}, [isActive]);

const handleTriggerClick = useCallback(() => {
Expand Down Expand Up @@ -219,44 +228,39 @@ export function DropdownMenu({
isActive={isActive}
menuId={menuId}
/>
<Menu ref={dropdownRef} isActive={isActive} x={x} y={y} id={menuId}>
{normalizedItems.map((props, i) => {
if (!isItem(props)) {
return <ItemDivider key={i} />;
}

const { label, onClick, helper, id, disabled, shortcut, icon } =
props;

return (
<MenuItem
onClick={() => {
handleClose();
onClick();
}}
id={id}
data-test={`menu-item-${id}`}
disabled={disabled}
key={id}
helper={shortcut ? `${helper} (${shortcut})` : helper}
label={label}
selected={useKeys && selectedIndex === i}
icon={icon}
shortcut={shortcut}
/>
);
})}
<Menu ref={dropdownRef} visible={visible} x={x} y={y} id={menuId}>
{isActive &&
normalizedItems.map((props, i) => {
if (!isItem(props)) {
return <ItemDivider key={i} />;
}

const { label, onClick, helper, id, disabled, shortcut, icon } =
props;

return (
<MenuItem
onClick={() => {
handleClose();
onClick();
}}
id={id}
data-test={`menu-item-${id}`}
disabled={disabled}
key={id}
helper={shortcut ? `${helper} (${shortcut})` : helper}
label={label}
selected={useKeys && selectedIndex === i}
icon={icon}
shortcut={shortcut}
/>
);
})}
</Menu>
</>
);
}

interface MenuProps {
isActive: boolean;
x: number;
y: number;
}

export interface MenuItemSidebarProps extends MenuItemMinimial {
handleClickItem?: () => unknown;
}
Expand Down Expand Up @@ -342,6 +346,12 @@ const ItemDivider = styled.div`
border-bottom: 1px solid ${p => p.theme.colors.bg2};
`;

interface MenuProps {
visible: boolean;
x: number;
y: number;
}

const Menu = styled.div<MenuProps>`
font-size: ${p => p.theme.fontSizeBody}rem;
overflow: hidden;
Expand All @@ -357,6 +367,7 @@ const Menu = styled.div<MenuProps>`
left: ${p => p.x}px;
width: auto;
box-shadow: ${p => p.theme.boxShadowSoft};
opacity: ${p => (p.isActive ? 1 : 0)};
visibility: ${p => (p.isActive ? 'visible' : 'hidden')};
opacity: ${p => (p.visible ? 1 : 0)};

transition: ${() => transition('opacity')};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const Wrapper = styled.span`
export const floatingHoverStyles = css`
position: relative;

&:hover ${Wrapper}, &:focus ${Wrapper} {
&:hover ${Wrapper}, &:focus-within ${Wrapper} {
visibility: visible;
display: inline;
}
Expand Down
Loading