Skip to content
Open
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
7,178 changes: 4,898 additions & 2,280 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
"default": "./tools/*/index.js"
}
},
"overrides": {
"enzyme": {
"cheerio": "1.0.0-rc.12"
},
"@types/babel__traverse": "7.18.5"
},
"dependencies": {
"@babel/runtime": "7.16.7",
"classnames": "2.3.1"
Expand Down Expand Up @@ -94,7 +100,7 @@
"rollup-plugin-postcss": "4.0.2",
"rollup-plugin-terser": "7.0.2",
"sass": "1.49.0",
"semantic-release": "19.0.2",
"semantic-release": "21.1.2",
"sinon": "13.0.0",
"typescript": "4.5.5"
},
Expand Down
2 changes: 1 addition & 1 deletion rollup/rollup.cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export const development = {
...base.plugins(),
postcss({sourceMap: true}),
],
};
};
2 changes: 1 addition & 1 deletion rollup/rollup.es.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export const development = {
...base.plugins(),
postcss({sourceMap: true}),
],
}
}
2 changes: 1 addition & 1 deletion rollup/rollup.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ export const development = {
...base.plugins(),
postcss({sourceMap: true}),
],
};
};
43 changes: 15 additions & 28 deletions src/components/Collapsible/Collapsible.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,34 @@ import classNames from 'classnames';
import {propTypes, defaultProps} from './Collapsible.props';
import './Collapsible.scss';

export const Collapsible = ({expanded, children, onTransitionEnd, ...props}) => {
export const Collapsible = ({expanded: expandedProp, defaultExpanded, children, onTransitionEnd, onChange, ...props}) => {
const isControlled = expandedProp !== undefined;
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
const expanded = isControlled ? expandedProp : internalExpanded;
const [{motion, height}, setState] = useState({motion: '', height: expanded ? 'auto' : 0});
const hasChildren = !!React.Children.count(children);
const contentRef = useRef();
const mounted = useRef();
const handleOnTransitionEnd = useCallback(e => {
// 'transform' is the longest transition property out of multiple ones
if (e.propertyName === 'transform') {
setState(state => ({...state, motion: '', height: state.height ? 'auto' : 0}));
onTransitionEnd(e);
}
}, [onTransitionEnd]);

const toggle = useCallback(() => {
if (!isControlled) { setInternalExpanded(v => { const n = !v; onChange?.(n); return n; }); }
}, [isControlled, onChange]);
useEffect(() => {
mounted.current && setState({
height: contentRef.current.clientHeight, // if was "auto" - measure again and change
motion: expanded ? 'expanding' : 'collapsing',
});

mounted.current && setState({height: contentRef.current.clientHeight, motion: expanded ? 'expanding' : 'collapsing'});
mounted.current = true;
}, [expanded]);

useEffect(() => {
// forces repaint so the next part will only take affect after previous height
// change and not have React merge both setStates
document.body.scrollTop;

if (motion === 'collapsing')
setState(state => ({...state, height: 0}))
}, [motion]);

useEffect(() => { document.body.scrollTop; if (motion === 'collapsing') setState(s => ({...s, height: 0})); }, [motion]);
const hasChildren = !!React.Children.count(children);
return (
<div {...props} className={classNames('collapsible', motion, {expanded: expanded && !motion}, props.className)}>
{hasChildren && (
<div
className='content-wrapper'
onTransitionEnd={handleOnTransitionEnd}
style={{height}}>
<div className='content' ref={contentRef}>{children}</div>
</div>
)}
<div {...props} className={classNames('collapsible', motion, {expanded: expanded && !motion}, props.className)}
data-expanded={expanded} onClick={props.onClick || toggle}>
{hasChildren && <div className='content-wrapper' onTransitionEnd={handleOnTransitionEnd} style={{height}}>
<div className='content' ref={contentRef}>{children}</div>
</div>}
</div>
);
};
Expand Down
10 changes: 9 additions & 1 deletion src/components/Collapsible/Collapsible.props.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ import {bool, func, node} from 'prop-types';
import {noop} from 'utility/memory';

export const propTypes = {
/** Controlled expanded state. If provided, the component operates in controlled mode. */
expanded: bool,
/** Default expanded state for uncontrolled mode. */
defaultExpanded: bool,
/** Callback fired when the transition ends */
onTransitionEnd: func,
/** Callback fired when expanded state changes (uncontrolled mode only) */
onChange: func,
children: node.isRequired,
};

export const defaultProps = {
expanded: false,
expanded: undefined,
defaultExpanded: false,
onTransitionEnd: noop,
onChange: undefined,
};
43 changes: 43 additions & 0 deletions src/components/Collapsible/Collapsible.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,47 @@ describe('<Collapsible/>', () => {
wrapper.unmount();
});
});

describe('Uncontrolled Mode', () => {
it('should support defaultExpanded prop', () => {
const wrapper = mount(<Collapsible defaultExpanded>foo</Collapsible>);
expect(wrapper.find('.collapsible.expanded')).toHaveLength(1);
wrapper.unmount();
});

it('should toggle on click in uncontrolled mode', () => {
const onChange = sinon.spy();
const wrapper = mount(<Collapsible onChange={onChange}>foo</Collapsible>);

expect(wrapper.find('.collapsible').prop('data-expanded')).toBe(false);

// Click to expand
wrapper.find('.collapsible').simulate('click');
expect(onChange.callCount).toEqual(1);
expect(onChange.calledWith(true)).toBe(true);

wrapper.unmount();
});

it('should not toggle on click in controlled mode', () => {
const onChange = sinon.spy();
const wrapper = mount(<Collapsible expanded={false} onChange={onChange}>foo</Collapsible>);

// Click should not change state in controlled mode
wrapper.find('.collapsible').simulate('click');
expect(onChange.callCount).toEqual(0);

wrapper.unmount();
});

it('should use custom onClick when provided', () => {
const onClick = sinon.spy();
const wrapper = mount(<Collapsible onClick={onClick}>foo</Collapsible>);

wrapper.find('.collapsible').simulate('click');
expect(onClick.callCount).toEqual(1);

wrapper.unmount();
});
});
});
4 changes: 3 additions & 1 deletion src/components/Poppable/Poppable.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
* limitations under the License.
*/

export const HIDDEN_PLACEMENT = {top: 0, left: 0, name: 'hidden'};
// Use values that are extremely unlikely to be valid placements
// This ensures that {top: 0, left: 0} can be used as a valid placement
export const HIDDEN_PLACEMENT = {top: -9999, left: -9999, name: 'hidden'};
7 changes: 6 additions & 1 deletion src/components/Scrollable/components/Shadow/Shadow.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.scroll-shadow {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
}
4 changes: 3 additions & 1 deletion src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export {default as useEventListener} from './useEventListener';
export {default as useThrottle} from './useThrottle';
export {default as useObject} from './useObject';
export {default as usePrevious} from './usePrevious';
export {default as useTimeout} from './useTimeout';
export {default as useTimeout} from './useTimeout';
export {default as useResizeObserver} from './useResizeObserver';
export {default as useDepEffect} from './useDepEffect';
24 changes: 17 additions & 7 deletions src/hooks/useBooleanState/useBooleanState.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@ import {useCallback, useState} from 'react';
* to update it: setTrue, setFalse, toggle, setValue.
*
* @param initial The initial state value
* @returns {{value: boolean, setTrue: (function(): void), setFalse: (function(): void), setValue: React.Dispatch<React.SetStateAction<boolean>>, toggle: (function(): void)}}
* @returns {{value: boolean, setTrue: (function(): void), setFalse: (function(): void), setValue: React.Dispatch<React.SetStateAction<boolean>>, toggle: (function(boolean=): void)}}
*/
export const useBooleanState = (initial = false) => {
const [value, setValue] = useState(initial);
const setTrue = useCallback(() => setValue(true), [setValue]);
const setFalse = useCallback(() => setValue(false), [setValue]);
const toggle = useCallback(() => setValue(!value), [setValue, value]);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
/**
* Toggle the boolean value. Optionally pass a force argument to set a specific value.
* @param {boolean} [force] - If provided, sets the value to this instead of toggling
*/
const toggle = useCallback((force) => {
if (typeof force === 'boolean') {
setValue(force);
} else {
setValue(v => !v);
}
}, []);
return {value, setTrue, setFalse, toggle, setValue};
};

Expand All @@ -36,7 +46,7 @@ export const useBooleanState = (initial = false) => {
* to update it: focus, blur, toggle and setFocused.
*
* @param {boolean} initial The initial state value
* @return {{hide: (function(): void), visible: boolean, show: (function(): void), toggle: (function(): void), setVisible: (function(): void)}}
* @return {{blur: (function(): void), focused: boolean, focus: (function(): void), toggle: (function(boolean=): void), setFocused: (function(): void)}}
*/
export const useFocusabilityState = (initial = false) => {
const {value, setTrue, setFalse, toggle, setValue} = useBooleanState(initial);
Expand All @@ -48,9 +58,9 @@ export const useFocusabilityState = (initial = false) => {
* to update it: show, hide, toggle and setVisible.
*
* @param {boolean} initial The initial state value
* @return {{hide: (function(): void), visible: boolean, show: (function(): void), toggle: (function(): void), setVisible: (function(): void)}}
* @return {{hide: (function(): void), visible: boolean, show: (function(): void), toggle: (function(boolean=): void), setVisible: (function(): void)}}
*/
export const useVisibilityState = (initial = false) => {
const {value, setTrue, setFalse, toggle, setValue} = useBooleanState(initial);
return {visible: value, show: setTrue, hide: setFalse, toggle, setVisible: setValue};
};
};
58 changes: 56 additions & 2 deletions src/hooks/useBooleanState/useBooleanState.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React from 'react';
import {act} from 'react-dom/test-utils';
import {shallow} from 'enzyme';
import {useVisibilityState, useFocusabilityState} from './useBooleanState';
import {useBooleanState, useVisibilityState, useFocusabilityState} from './useBooleanState';

const Elem = ({initial}) => {
const Elem = ({initial, forceValue}) => {
const {visible, show, hide, toggle} = useVisibilityState(initial);
return (
<div className={visible ? 'visible' : 'hidden'}>
<div className='show' onClick={show}/>
<div className='hide' onClick={hide}/>
<div className='toggle' onClick={toggle}/>
<div className='toggle-force' onClick={() => toggle(forceValue)}/>
</div>
);
};
Expand All @@ -34,6 +35,28 @@ describe('useVisibilityState()', () => {
wrapper.find('.hide').simulate('click');
expect(wrapper.find('.hidden')).toHaveLength(1);
});

it('Should support force argument in toggle', () => {
let wrapper = null;

// Test forcing to true
act(() => {wrapper = shallow(<Elem forceValue={true}/>)});
expect(wrapper.find('.hidden')).toHaveLength(1);
wrapper.find('.toggle-force').simulate('click');
expect(wrapper.find('.visible')).toHaveLength(1);
// Clicking again with force=true should keep it true
wrapper.find('.toggle-force').simulate('click');
expect(wrapper.find('.visible')).toHaveLength(1);

// Test forcing to false
act(() => {wrapper = shallow(<Elem initial={true} forceValue={false}/>)});
expect(wrapper.find('.visible')).toHaveLength(1);
wrapper.find('.toggle-force').simulate('click');
expect(wrapper.find('.hidden')).toHaveLength(1);
// Clicking again with force=false should keep it false
wrapper.find('.toggle-force').simulate('click');
expect(wrapper.find('.hidden')).toHaveLength(1);
});
});

const FocusedElem = ({initial}) => {
Expand Down Expand Up @@ -66,3 +89,34 @@ describe('useFocusabilityState()', () => {
expect(wrapper.find('.blurred')).toHaveLength(1);
});
});

describe('useBooleanState()', () => {
const BoolElem = ({forceValue}) => {
const {value, setTrue, setFalse, toggle} = useBooleanState(false);
return (
<div className={value ? 'on' : 'off'}>
<div className='set-true' onClick={setTrue}/>
<div className='set-false' onClick={setFalse}/>
<div className='toggle' onClick={toggle}/>
<div className='toggle-force' onClick={() => toggle(forceValue)}/>
</div>
);
};

it('Should toggle with force argument', () => {
let wrapper = null;

act(() => {wrapper = shallow(<BoolElem forceValue={true}/>)});
expect(wrapper.find('.off')).toHaveLength(1);
wrapper.find('.toggle-force').simulate('click');
expect(wrapper.find('.on')).toHaveLength(1);

// Force to true again - should stay true
wrapper.find('.toggle-force').simulate('click');
expect(wrapper.find('.on')).toHaveLength(1);

// Regular toggle should still work
wrapper.find('.toggle').simulate('click');
expect(wrapper.find('.off')).toHaveLength(1);
});
});
17 changes: 17 additions & 0 deletions src/hooks/useDepEffect/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2020, Amdocs Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export {default} from './useDepEffect';
2 changes: 2 additions & 0 deletions src/hooks/useDepEffect/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
See the full documentation of `useDepEffect()` at the
[Official Webrix Documentation Site](https://webrix.amdocs.com/docs/hooks/useDepEffect)
Loading