Skip to content

Commit

Permalink
Make all container components take containerRef prop
Browse files Browse the repository at this point in the history
  • Loading branch information
lyzadanger committed Aug 13, 2021
1 parent 42b72f6 commit 65c9079
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 90 deletions.
58 changes: 50 additions & 8 deletions src/components/containers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,48 @@

import classnames from 'classnames';

import { downcastRef } from '../util/typing';

/**
* @typedef {import('preact').ComponentChildren} Children
*
* @typedef ContainerProps
* @prop {Children} children
* @prop {string} [classes] - Additional CSS classes to apply
* @prop {import('preact').Ref<HTMLElement>} [containerRef] - Access to the
* wrapping element.
*/

/**
* Render content inside of a "frame"
*
* @param {ContainerProps} props
*/
export function Frame({ children, classes = '' }) {
return <div className={classnames('Hyp-Frame', classes)}>{children}</div>;
export function Frame({ children, classes = '', containerRef }) {
return (
<div
className={classnames('Hyp-Frame', classes)}
ref={downcastRef(containerRef)}
>
{children}
</div>
);
}

/**
* Render content inside of a "card"
*
* @param {ContainerProps} props
*/
export function Card({ children, classes = '' }) {
return <div className={classnames('Hyp-Card', classes)}>{children}</div>;
export function Card({ children, classes = '', containerRef }) {
return (
<div
className={classnames('Hyp-Card', classes)}
ref={downcastRef(containerRef)}
>
{children}
</div>
);
}

/**
Expand All @@ -43,9 +61,21 @@ export function Card({ children, classes = '' }) {
*
* @param {ActionBaseProps & ContainerProps} props
*/
export function Actions({ children, direction = 'row', classes = '' }) {
export function Actions({
children,
direction = 'row',
classes = '',
containerRef,
}) {
const baseClass = `Hyp-Actions--${direction}`;
return <div className={classnames(baseClass, classes)}>{children}</div>;
return (
<div
className={classnames(baseClass, classes)}
ref={downcastRef(containerRef)}
>
{children}
</div>
);
}

/**
Expand All @@ -60,7 +90,19 @@ export function Actions({ children, direction = 'row', classes = '' }) {
*
* @param {ScrollboxBaseProps & ContainerProps} props
*/
export function Scrollbox({ children, classes = '', withHeader = false }) {
export function Scrollbox({
children,
classes = '',
containerRef,
withHeader = false,
}) {
const baseClass = withHeader ? 'Hyp-Scrollbox--with-header' : 'Hyp-Scrollbox';
return <div className={classnames(baseClass, classes)}>{children}</div>;
return (
<div
className={classnames(baseClass, classes)}
ref={downcastRef(containerRef)}
>
{children}
</div>
);
}
135 changes: 53 additions & 82 deletions src/components/test/containers-test.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,70 @@
import { mount } from 'enzyme';
import { createRef } from 'preact';

import { Frame, Card, Actions, Scrollbox } from '../containers';

describe('Frame', () => {
const createComponent = (props = {}) =>
mount(
<Frame {...props}>
<div>This is content inside of a frame</div>
</Frame>
describe('Container components', () => {
const createComponent = (Component, props = {}) => {
return mount(
<Component {...props}>
<div>This is content inside of a container</div>
</Component>
);

it('renders children inside of a div with appropriate classnames', () => {
const wrapper = createComponent();

assert.isTrue(wrapper.find('div').first().hasClass('Hyp-Frame'));
});

it('applies extra classes', () => {
const wrapper = createComponent({ classes: 'foo bar' });

assert.isTrue(wrapper.find('div.Hyp-Frame.foo.bar').exists());
});
});

describe('Card', () => {
const createComponent = (props = {}) =>
mount(
<Card {...props}>
<div>This is content inside of a card</div>
</Card>
);

it('renders children inside of a div with appropriate classnames', () => {
const wrapper = createComponent();

assert.isTrue(wrapper.find('div').first().hasClass('Hyp-Card'));
});

it('applies extra classes', () => {
const wrapper = createComponent({ classes: 'foo bar' });

assert.isTrue(wrapper.find('div.Hyp-Card.foo.bar').exists());
};

const commonTests = (Component, className) => {
it('renders children inside of a div with appropriate classnames', () => {
const wrapper = createComponent(Component);

assert.isTrue(wrapper.find('div').first().hasClass(className));
});

it('applies extra classes', () => {
const wrapper = createComponent(Component, { classes: 'foo bar' });
assert.deepEqual(
[
...wrapper
.find(`div.${className}.foo.bar`)
.getDOMNode()
.classList.values(),
],
[className, 'foo', 'bar']
);
});

it('passes along a `ref` to the input element through `containerRef`', () => {
const containerRef = createRef();
createComponent(Component, { containerRef });

assert.instanceOf(containerRef.current, Node);
});
};

describe('Frame', () => {
commonTests(Frame, 'Hyp-Frame');
});
});

describe('Actions', () => {
const createComponent = (props = {}) =>
mount(
<Actions {...props}>
<div>This is content inside of Actions</div>
</Actions>
);

it('renders children inside of a div with appropriate classnames', () => {
const wrapper = createComponent();

assert.isTrue(wrapper.find('div').first().hasClass('Hyp-Actions--row'));
});

it('applies extra classes', () => {
const wrapper = createComponent({ classes: 'foo bar' });

assert.isTrue(wrapper.find('div.Hyp-Actions--row.foo.bar').exists());
describe('Card', () => {
commonTests(Card, 'Hyp-Card');
});

it('applies columnar layout if `direction` is `column`', () => {
const wrapper = createComponent({ direction: 'column' });
describe('Actions', () => {
commonTests(Actions, 'Hyp-Actions--row');

assert.isTrue(wrapper.find('div.Hyp-Actions--column').exists());
});
});

describe('Scrollbox', () => {
const createComponent = (props = {}) =>
mount(
<Scrollbox {...props}>
<div>This is content inside of a Scrollbox</div>
</Scrollbox>
);
it('applies columnar layout if `direction` is `column`', () => {
const wrapper = createComponent(Actions, { direction: 'column' });

it('renders children inside of a div with appropriate classnames', () => {
const wrapper = createComponent();

assert.isTrue(wrapper.find('.Hyp-Scrollbox').exists());
assert.isTrue(wrapper.find('div.Hyp-Actions--column').exists());
});
});

it('applies extra classes', () => {
const wrapper = createComponent({ classes: 'foo bar' });

assert.isTrue(wrapper.find('div.Hyp-Scrollbox.foo.bar').exists());
});
describe('Scrollbox', () => {
commonTests(Scrollbox, 'Hyp-Scrollbox');

it('applies header-affordance layout class if `withHeader`', () => {
const wrapper = createComponent({ withHeader: true });
it('applies header-affordance layout class if `withHeader`', () => {
const wrapper = createComponent(Scrollbox, { withHeader: true });

assert.isTrue(wrapper.find('div.Hyp-Scrollbox--with-header').exists());
assert.isTrue(wrapper.find('div.Hyp-Scrollbox--with-header').exists());
});
});
});
21 changes: 21 additions & 0 deletions src/util/typing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @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);
}

0 comments on commit 65c9079

Please sign in to comment.