Commit f9fda4bc authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Alejandro Celaya

Use Slider component from frontend-shared and drop local one

parent 89c46e70
import { CaretUpIcon, MenuExpandIcon } from '@hypothesis/frontend-shared';
import {
CaretUpIcon,
MenuExpandIcon,
Slider,
} from '@hypothesis/frontend-shared';
import type { IconComponent } from '@hypothesis/frontend-shared/lib/types';
import classnames from 'classnames';
import type { ComponentChildren, Ref } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import MenuKeyboardNavigation from './MenuKeyboardNavigation';
import Slider from './Slider';
type SubmenuToggleProps = {
title: string;
......@@ -305,7 +308,7 @@ export default function MenuItem({
<>
{menuItem}
{hasSubmenuVisible && (
<Slider visible={isSubmenuVisible}>
<Slider direction={isSubmenuVisible ? 'in' : 'out'}>
<MenuKeyboardNavigation
closeMenu={onCloseSubmenu}
visible={isSubmenuVisible}
......
import { Dialog } from '@hypothesis/frontend-shared';
import { Dialog, Slider } from '@hypothesis/frontend-shared';
import type { IconComponent } from '@hypothesis/frontend-shared/lib/types';
import type { ComponentChildren } from 'preact';
import { useCallback, useEffect, useRef } from 'preact/hooks';
......@@ -6,7 +6,6 @@ import scrollIntoView from 'scroll-into-view';
import type { PanelName } from '../../types/sidebar';
import { useSidebarStore } from '../store';
import Slider from './Slider';
export type SidebarPanelProps = {
children: ComponentChildren;
......
import type { ComponentChildren } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
export type SliderProps = {
/** The content to hide or reveal. */
children?: ComponentChildren;
/** Whether the content should be visible or not. */
visible: boolean;
/** Invoked once the open/close transitions have finished */
onTransitionEnd?: (direction: 'in' | 'out') => void;
};
/**
* A container which reveals its content when `visible` is `true` using
* a sliding animation.
*
* When the content is not partially or wholly visible, it is removed from the
* DOM using `display: none` so it does not appear in the keyboard navigation
* order.
*
* Currently, the only reveal/expand direction supported is top-down.
*/
export default function Slider({
children,
visible,
onTransitionEnd,
}: SliderProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const [containerHeight, setContainerHeight] = useState(visible ? 'auto' : 0);
// Whether the content is currently partially or wholly visible. This is
// different from `visible` when collapsing as it is true until the collapse
// animation completes.
const [contentVisible, setContentVisible] = useState(visible);
// Adjust the container height when the `visible` prop changes.
useEffect(() => {
const isVisible = containerHeight !== 0;
if (visible === isVisible) {
// Do nothing after the initial mount.
return;
}
const el = containerRef.current!;
if (visible) {
// Show the content synchronously so that we can measure it here.
el.style.display = '';
// Make content visible in future renders.
setContentVisible(true);
// When expanding, transition the container to the current fixed height
// of the content. After the transition completes, we'll reset to "auto"
// height to adapt to future content changes.
setContainerHeight(el.scrollHeight);
} else {
// When collapsing, immediately change the current height to a fixed height
// (in case it is currently "auto"), force a synchronous layout,
// then transition to 0.
//
// These steps are needed because browsers will not animate transitions
// from "auto" => "0" and may not animate "auto" => fixed height => 0
// if the layout tree transitions directly from "auto" => 0.
el.style.height = `${el.scrollHeight}px`;
// Force a sync layout.
el.getBoundingClientRect();
setContainerHeight(0);
}
}, [containerHeight, visible]);
const handleTransitionEnd = useCallback(() => {
if (visible) {
setContainerHeight('auto');
onTransitionEnd?.('in');
} else {
// When the collapse animation completes, stop rendering the content so
// that the browser has fewer nodes to render and the content is removed
// from keyboard navigation.
setContentVisible(false);
onTransitionEnd?.('out');
}
}, [setContainerHeight, visible, onTransitionEnd]);
const isFullyVisible = containerHeight === 'auto';
return (
<div
// nb. Preact uses "ontransitionend" rather than "onTransitionEnd".
// See https://bugs.chromium.org/p/chromium/issues/detail?id=961193
//
// @ts-ignore
// eslint-disable-next-line react/no-unknown-property
ontransitionend={handleTransitionEnd}
ref={containerRef}
style={{
display: contentVisible ? '' : 'none',
height: containerHeight,
// When the Slider is fully open, overflow is made visible so that
// focus rings, which may extend outside the bounds of the Slider content,
// are visible.
overflow: isFullyVisible ? 'visible' : 'hidden',
transition: `height 0.15s ease-in`,
}}
>
{children}
</div>
);
}
......@@ -190,7 +190,7 @@ describe('MenuItem', () => {
isSubmenuVisible: true,
submenu: <div role="menuitem">Submenu content</div>,
});
assert.equal(wrapper.find('Slider').prop('visible'), true);
assert.equal(wrapper.find('Slider').prop('direction'), 'in');
assert.equal(
wrapper.find('MenuKeyboardNavigation').prop('visible'),
true
......@@ -203,7 +203,7 @@ describe('MenuItem', () => {
isSubmenuVisible: false,
submenu: <div>Submenu content</div>,
});
assert.equal(wrapper.find('Slider').prop('visible'), false);
assert.equal(wrapper.find('Slider').prop('direction'), 'out');
assert.equal(
wrapper.find('MenuKeyboardNavigation').prop('visible'),
false
......
......@@ -53,7 +53,7 @@ describe('SidebarPanel', () => {
it('shows content if active', () => {
fakeStore.isSidebarPanelOpen.returns(true);
const wrapper = createSidebarPanel();
assert.isTrue(wrapper.find('Slider').prop('visible'));
assert.equal(wrapper.find('Slider').prop('direction'), 'in');
});
it('hides content if not active', () => {
......
import { mount } from 'enzyme';
import { checkAccessibility } from '../../../test-util/accessibility';
import Slider from '../Slider';
describe('Slider', () => {
let container;
const createSlider = (props = {}) => {
return mount(
<Slider visible={false} {...props}>
<div style={{ width: 100, height: 200 }}>Test content</div>
</Slider>,
{ attachTo: container }
);
};
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it('should render collapsed if `visible` is false on mount', () => {
const wrapper = createSlider({ visible: false });
const { height } = wrapper.getDOMNode().getBoundingClientRect();
assert.equal(height, 0);
// The content shouldn't be rendered, so it doesn't appear in the keyboard
// navigation order.
assert.equal(wrapper.getDOMNode().style.display, 'none');
});
it('should render expanded if `visible` is true on mount', () => {
const wrapper = createSlider({ visible: true });
const { height } = wrapper.getDOMNode().getBoundingClientRect();
assert.equal(height, 200);
});
it('should transition to expanded if `visible` changes to `true`', () => {
const wrapper = createSlider({ visible: false });
wrapper.setProps({ visible: true });
const containerStyle = wrapper.getDOMNode().style;
assert.equal(containerStyle.height, '200px');
});
it('should transition to collapsed if `visible` changes to `false`', done => {
const wrapper = createSlider({ visible: true });
wrapper.setProps({ visible: false });
setTimeout(() => {
const containerStyle = wrapper.getDOMNode().style;
assert.equal(containerStyle.height, '0px');
done();
}, 1);
});
it('should set the container height to "auto" when an expand transition finishes', () => {
const wrapper = createSlider({ visible: false });
wrapper.setProps({ visible: true });
let containerStyle = wrapper.getDOMNode().style;
assert.equal(containerStyle.height, '200px');
wrapper.find('div').first().simulate('transitionend');
containerStyle = wrapper.getDOMNode().style;
assert.equal(containerStyle.height, 'auto');
});
it('should hide overflowing content when not fully visible', () => {
// When fully collapsed, overflow should be hidden.
const wrapper = createSlider({ visible: false });
let containerStyle = wrapper.getDOMNode().style;
assert.equal(containerStyle.overflow, 'hidden');
// When starting to expand, or when collapsing, overflow should also be hidden.
wrapper.setProps({ visible: true });
assert.equal(containerStyle.overflow, 'hidden');
// When fully visible, we make overflow visible to make focus rings or
// other content which extends beyond the bounds of the Slider visible.
wrapper.find('div').first().simulate('transitionend');
containerStyle = wrapper.getDOMNode().style;
assert.equal(containerStyle.overflow, 'visible');
});
it('should stop rendering content when a collapse transition finishes', () => {
const wrapper = createSlider({ visible: true });
wrapper.setProps({ visible: false });
wrapper.find('div').first().simulate('transitionend');
const containerStyle = wrapper.getDOMNode().style;
assert.equal(containerStyle.display, 'none');
});
[true, false].forEach(visible => {
it('should handle unmounting while expanding or collapsing', () => {
const wrapper = createSlider({ visible });
wrapper.setProps({ visible: !visible });
wrapper.unmount();
});
});
it(
'should pass a11y checks',
checkAccessibility([
{
name: 'visible',
content: () => createSlider({ visible: true }),
},
{
name: 'hidden',
content: () => createSlider({ visible: false }),
},
])
);
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment