Commit 163f3899 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Extract `SidebarContent` presentational component

- Add utility for generating presentational (styling-only) components
- Use `SidebarContent` in `TopBar`
parent a1aaab39
import classnames from 'classnames';
import { downcastRef } from './type-coercions';
/**
* @typedef {import('preact').JSX.HTMLAttributes<HTMLDivElement>} HTMLDivAttributes
*/
/**
* @typedef PresentationalComponentProps
* @prop {import('preact').ComponentChildren} [children]
* @prop {string|string[]} [classes] - Optional extra CSS classes to append to the
* component's default classes
* @prop {import('preact').Ref<HTMLElement>} [elementRef]
*/
/**
* Make a presentational component which wraps children to apply layout and
* styling.
*
* @param {string} displayName
* @param {string|string[]} classes - CSS classes for this component
*/
export function makePresentationalComponent(displayName, classes) {
/**
* @param {HTMLDivAttributes & PresentationalComponentProps} props
*/
function PresentationalComponent({
classes: extraClasses,
elementRef,
children,
...htmlAttributes
}) {
return (
<div
className={classnames(classes, extraClasses)}
ref={downcastRef(elementRef)}
{...htmlAttributes}
>
{children}
</div>
);
}
PresentationalComponent.displayName = displayName;
return PresentationalComponent;
}
......@@ -67,3 +67,25 @@ export function toString(value) {
}
return '';
}
/**
* @template T
* @typedef {import('preact').Ref<T>} Ref
*/
/**
* Helper for downcasting a ref to a more specific type, where that is safe
* to do.
*
* This is mainly useful to cast a generic `Ref<HTMLElement>` to a more specific
* element type (eg. `Ref<HTMLDivElement>`) for use with the `ref` prop of a JSX element.
* Since Preact only writes to the `ref` prop, such a cast is safe.
*
* @template T
* @template {T} U
* @param {Ref<T>|undefined} ref
* @return {Ref<U>|undefined}
*/
export function downcastRef(ref) {
return /** @type {Ref<U>|undefined} */ (ref);
}
import { makePresentationalComponent } from '../../shared/presentational-component';
/**
* Render sidebar "page" content. This is used in the narrow sidebar frame
* as well as wider interfaces like the Notebook and single-annotation view.
*/
const SidebarContent = makePresentationalComponent(
'SidebarContent',
// Center this content (auto margins). For larger viewports, set a
// maximum width (768px) and add some horizontal padding.
'mx-auto lg:px-16 lg:max-w-3xl'
);
export default SidebarContent;
......@@ -9,6 +9,7 @@ import { useStoreProxy } from '../store/use-store';
import GroupList from './GroupList';
import SearchInput from './SearchInput';
import SidebarContent from './SidebarContent';
import SortMenu from './SortMenu';
import StreamSearchInput from './StreamSearchInput';
import UserMenu from './UserMenu';
......@@ -21,30 +22,6 @@ import UserMenu from './UserMenu';
* @typedef {import('../services/streamer').StreamerService} StreamerService
*/
/**
* Render "page" content in a container that centers it and constrains its
* maximum width.
*
* @param {object} props
* @param {Children} props.children
* @param {string} [props.classes]
* @returns
*/
function SidebarContent({ children, classes }) {
return (
<div
className={classnames(
// Center this content (auto margins). For larger viewports, set a
// maximum width (768px) and add some horizontal padding.
'mx-auto lg:px-16 lg:max-w-3xl',
classes
)}
>
{children}
</div>
);
}
/**
* @typedef TopBarProps
* @prop {AuthState} auth
......
......@@ -34,6 +34,11 @@ describe('TopBar', () => {
};
$imports.$mock(mockImportedComponents());
// SidebarContent is a presentational-only component without its own tests
$imports.$restore({
'./SidebarContent': true,
});
$imports.$mock({
'../store/use-store': { useStoreProxy: () => fakeStore },
'../helpers/is-third-party-service': {
......
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