Skip to content

Commit

Permalink
feat: useInlineStyle to return CSSProperties to multiple elements
Browse files Browse the repository at this point in the history
useInlineStyle style property now returns object with keys set as in stylingFunction, this allows
multiple elements to be styled if they depend on the same events in referenced element

BREAKING CHANGE: style object is not containing CSSProperties but is a Record<string, CSSProperties>
  • Loading branch information
stopyransky committed Dec 25, 2021
1 parent 41d3a33 commit f6dce84
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 83 deletions.
29 changes: 0 additions & 29 deletions src/useInlineStyle/InlineStyleExample.tsx

This file was deleted.

31 changes: 31 additions & 0 deletions src/useInlineStyle/examples/BasicExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { StylingFn } from '..';
import { useInlineStyle } from '../useInlineStyle';

interface TestProps {
isMobile: boolean;
}

const stylingFn: StylingFn<TestProps> = (state, props) => {
return {
clicker: {
color: state.hover ? 'red' : 'black',
borderColor: state.focus ? 'red' : 'black',
backgroundColor: state.active ? 'red' : 'black',
cursor: 'pointer',
width: props.isMobile ? '90%' : '200px',
},
};
};

export function BasicExample(props: TestProps): React.ReactElement {
const [ref, style] = useInlineStyle<HTMLDivElement, TestProps>(
stylingFn,
props
);
return (
<div ref={ref} data-testid='clicker' style={style.clicker}>
test component
</div>
);
}
70 changes: 70 additions & 0 deletions src/useInlineStyle/examples/ComplexExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react';
import { StyleState } from '..';
import { useInlineStyle } from '../useInlineStyle';

interface TestProps {
label: boolean;
}

function stylingFn(
{ focus, hover, active }: Partial<StyleState>,
{ label }: TestProps
): Record<string, React.CSSProperties> {
return {
wrapper: {
backgroundColor: hover ? 'white' : 'blue',
borderColor: active ? 'red' : 'white',
},
label: {
position: 'absolute',
left: label ? '12px' : 0,
fontSize: '0.75rem',
},
input: {
backgroundColor: 'white !important',
margin: label ? '20px 0px 32px 0px' : '0px 0px 32px 0px',
padding: '12px 12px',
fontSize: '0.85rem',
border: '1px solid',
borderRadius: '6px',
borderColor: focus ? '#212121' : hover ? '#666' : '#aaa',
boxShadow: focus ? 'inset 0px 0px 2px #212121' : undefined,
},
error: {
position: 'absolute',
bottom: '12px',
paddingLeft: '12px',
left: '0px',
borderBottom: '1px solid #d30000',
borderLeft: '1px solid #d30000',
color: '#d30000',
fontSize: '0.75rem',
lineHeight: '1.0rem',
},
};
}
export function ComplexExample(props: TestProps): React.ReactElement {
const [ref, style] = useInlineStyle<HTMLInputElement, TestProps>(
stylingFn,
props
);
return (
<div data-testid='wrapper' style={style.wrapper}>
{props.label && (
<label data-testid='label' style={style.label} htmlFor='field-text'>
label
</label>
)}
<input
data-testid='input'
ref={ref}
style={style.input}
type='text'
id='field-text'
/>
<span data-testid='error' style={style.error}>
error
</span>
</div>
);
}
166 changes: 116 additions & 50 deletions src/useInlineStyle/useInlineStyle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,133 @@ import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { useInlineStyle } from './useInlineStyle';
import { InlineStyleExample } from './InlineStyleExample';
import { BasicExample } from './examples/BasicExample';
import { ComplexExample } from './examples/ComplexExample';

test('should return [ref, style] array', () => {
const { result, unmount } = renderHook(() =>
useInlineStyle(() => ({
cursor: 'pointer',
}))
);
describe('basic usage of useInlineStyle hook', () => {
it('should return [ref, style] tuple', () => {
const { result, unmount } = renderHook(() =>
useInlineStyle(() => ({
testElStyle: {
cursor: 'pointer',
},
}))
);

const [ref, style] = result.current;
const [ref, style] = result.current;

expect(typeof style).toBe('object');
expect(typeof ref).toBe('object');
unmount();
});
expect(typeof style).toBe('object');
expect(style.testElStyle).toBeDefined();
expect(typeof style.testElStyle).toBe('object');
expect(typeof ref).toBe('object');
unmount();
});

test('can simulate :hover style on pointerOver event', async () => {
const { getByRole, unmount } = render(
<InlineStyleExample isMobile={false} />
);
it('can simulate :hover style on pointerOver event', async () => {
const { getByTestId, unmount } = render(<BasicExample isMobile={false} />);

expect(getByRole('clicker').style.color).toEqual('black');
fireEvent.pointerOver(getByRole('clicker'));
await waitFor(() => getByRole('clicker'));
expect(getByRole('clicker').style.color).toEqual('red');
unmount();
});
const el = getByTestId('clicker');
expect(el.style.color).toEqual('black');
fireEvent.pointerOver(el);
await waitFor(() => el);
expect(el.style.color).toEqual('red');
unmount();
});

test('can simulate :focus style on focus event', async () => {
const { getByRole, unmount } = render(
<InlineStyleExample isMobile={false} />
);
it('can simulate :focus style on focus event', async () => {
const { getByTestId, unmount } = render(<BasicExample isMobile={false} />);
const el = getByTestId('clicker');
expect(el.style.borderColor).toEqual('black');
fireEvent.focus(el);
await waitFor(() => el);
expect(el.style.borderColor).toEqual('red');
unmount();
});

expect(getByRole('clicker').style.borderColor).toEqual('black');
fireEvent.focus(getByRole('clicker'));
await waitFor(() => getByRole('clicker'));
expect(getByRole('clicker').style.borderColor).toEqual('red');
unmount();
});
test('can simulate :active style on pointerDown event', async () => {
const { getByTestId, unmount } = render(<BasicExample isMobile={false} />);
const el = getByTestId('clicker');

expect(el.style.backgroundColor).toEqual('black');
fireEvent.pointerDown(el);
await waitFor(() => el);
expect(el.style.backgroundColor).toEqual('red');
unmount();
});

it('can modify style when props change', async () => {
const { rerender, getByTestId, unmount } = render(
<BasicExample isMobile={false} />
);
const el = getByTestId('clicker');

test('can simulate :active style on pointerDown event', async () => {
const { getByRole, unmount } = render(
<InlineStyleExample isMobile={false} />
);
expect(el.style.width).toEqual('200px');
rerender(<BasicExample isMobile={true} />);
await waitFor(() => el);
expect(el.style.width).toEqual('90%');

expect(getByRole('clicker').style.backgroundColor).toEqual('black');
fireEvent.pointerDown(getByRole('clicker'));
await waitFor(() => getByRole('clicker'));
expect(getByRole('clicker').style.backgroundColor).toEqual('red');
unmount();
unmount();
});
});

test('can modify style when props change', async () => {
const { rerender, getByRole, unmount } = render(
<InlineStyleExample isMobile={false} />
);
describe('complex case of useInlineStyle hook', () => {
it('style object contains style for multiple elements depending on events from given ref', async () => {
const { getByTestId, unmount } = render(<ComplexExample label={false} />);

const referenced = getByTestId('input');
const wrapper = getByTestId('wrapper');

expect(referenced.style.borderColor).toEqual('#aaa');
expect(wrapper.style.backgroundColor).toEqual('blue');

fireEvent.pointerOver(referenced);
await waitFor(() => referenced);

expect(referenced.style.borderColor).toEqual('#666');
expect(wrapper.style.backgroundColor).toEqual('white');

unmount();
});

it('can simulate :focus style on focus event', async () => {
const { getByTestId, unmount } = render(<ComplexExample label={false} />);
const el = getByTestId('input');
expect(el.style.boxShadow).toEqual('');
fireEvent.focus(el);
await waitFor(() => el);
expect(el.style.boxShadow).toEqual('inset 0px 0px 2px #212121');
unmount();
});

it('can simulate :active style on pointerDown event', async () => {
const { getByTestId, unmount } = render(<ComplexExample label={false} />);
const input = getByTestId('input');
const wrapper = getByTestId('wrapper');

expect(wrapper.style.borderColor).toEqual('white');

fireEvent.pointerDown(input);
await waitFor(() => input);

expect(wrapper.style.borderColor).toEqual('red');

unmount();
});

it('referenced element can still subscribe to custom events', async () => {
const { getByTestId, unmount } = render(<ComplexExample label={false} />);
const input = getByTestId('input');
const validate = jest.fn();

input.addEventListener('focus', validate);
expect(input.style.borderColor).toEqual('#aaa');

expect(getByRole('clicker').style.width).toEqual('200px');
rerender(<InlineStyleExample isMobile={true} />);
await waitFor(() => getByRole('clicker'));
expect(getByRole('clicker').style.width).toEqual('90%');
fireEvent.focus(input);
await waitFor(() => input);

unmount();
expect(input.style.borderColor).toEqual('#212121');
expect(validate).toHaveBeenCalledTimes(1);
input.removeEventListener('focus', validate);
unmount();
});
});
8 changes: 4 additions & 4 deletions src/useInlineStyle/useInlineStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export interface StyleState {

export type StylingFn<P> = (
state: StyleState,
props: P
) => Record<string, CSSProperties> | CSSProperties;
props: Partial<P>
) => Record<string, CSSProperties>;

const initialState: StyleState = {
hover: false,
Expand Down Expand Up @@ -47,8 +47,8 @@ const subscribeToEvents = (el, setStyle) => {

export function useInlineStyle<T extends HTMLElement, P>(
styleFn: StylingFn<P>,
props?: P
): [ref: RefObject<T>, style: Record<string, CSSProperties> | CSSProperties] {
props?: Partial<P>
): [ref: RefObject<T>, style: Record<string, CSSProperties>] {
const ref = useRef<T>(null);
const [styleState, setStyle] = useSmartReducer(initialState);

Expand Down

0 comments on commit f6dce84

Please sign in to comment.