refactor(listbox): add showGray option to disable gray colors (#815)

* fix: trim space when parsing engine URLs

* refactor: add showGray option to show/hide grey

* docs: update specs

* refactor: fetch start

* refactor: fetch start

* docs: spec
This commit is contained in:
Johan Lahti
2022-04-13 14:34:15 +02:00
committed by GitHub
parent 4529e58ad1
commit 02816e69fb
11 changed files with 236 additions and 20 deletions

View File

@@ -66,8 +66,10 @@ export default function ListBox({
rangeSelect = true,
checkboxes = false,
update = undefined,
fetchStart = undefined,
dense = false,
keyboard = {},
showGray = true,
selectDisabled = () => false,
}) {
const [layout] = useLayout(model);
@@ -133,7 +135,7 @@ export default function ListBox({
local.current.timeout = setTimeout(
() => {
const sorted = local.current.queue.slice(-2).sort((a, b) => a.start - b.start);
model
const reqPromise = model
.getListObjectData(
'/qListObjectDef',
sorted.map((s) => ({
@@ -150,6 +152,7 @@ export default function ListBox({
setIsLoadingData(false);
resolve();
});
fetchStart && fetchStart(reqPromise);
},
isScrolling ? scrollTimeout : 0
);
@@ -236,6 +239,7 @@ export default function ListBox({
frequencyMax,
histogram,
keyboard,
showGray,
}}
itemSize={itemSize}
onItemsRendered={onItemsRendered}

View File

@@ -51,11 +51,11 @@ const useStyles = makeStyles((theme) => ({
},
}));
const getIcon = (styles, excluded = false, alternative = false) => (
const getIcon = (styles, showGray = true, excluded = false, alternative = false) => (
<span className={styles.cbIcon}>
{(excluded || alternative) && (
<span
className={[excluded && styles.cbIconExcluded, alternative && styles.cbIconAlternative]
className={[showGray && excluded && styles.cbIconExcluded, showGray && alternative && styles.cbIconAlternative]
.filter(Boolean)
.join(' ')}
/>
@@ -63,7 +63,7 @@ const getIcon = (styles, excluded = false, alternative = false) => (
</span>
);
export default function ListboxCheckbox({ checked, label, dense, excluded, alternative }) {
export default function ListboxCheckbox({ checked, label, dense, excluded, alternative, showGray = true }) {
const styles = useStyles();
return (
@@ -74,7 +74,7 @@ export default function ListboxCheckbox({ checked, label, dense, excluded, alter
className={[styles.checkbox, dense && styles.dense].filter(Boolean).join(' ')}
inputProps={{ 'aria-labelledby': label }}
name={label}
icon={getIcon(styles, excluded, alternative)}
icon={getIcon(styles, showGray, excluded, alternative)}
checkedIcon={<span className={styles.cbIconChecked} />}
/>
);

View File

@@ -42,8 +42,10 @@ export default function ListBoxInline({ app, fieldIdentifier, stateName = '$', o
sessionModel = undefined,
selectionsApi = undefined,
update = undefined,
fetchStart = undefined,
dense = false,
selectDisabled = () => false,
showGray = true,
} = options;
let { frequencyMode, histogram = false } = options;
@@ -321,9 +323,11 @@ export default function ListBoxInline({ app, fieldIdentifier, stateName = '$', o
height={height}
width={width}
update={update}
fetchStart={fetchStart}
dense={dense}
selectDisabled={selectDisabled}
keyboard={keyboard}
showGray={showGray}
/>
)}
</AutoSizer>

View File

@@ -181,6 +181,7 @@ function RowColumn({ index, style, data, column = false }) {
frequencyMax = '',
histogram = false,
keyboard,
showGray = true,
} = data;
const handleKeyDownCallback = useCallback(getFieldKeyboardNavigation(actions), [actions]);
@@ -219,12 +220,12 @@ function RowColumn({ index, style, data, column = false }) {
const clazzArr = [column ? classes.column : classes.row];
if (!checkboxes) {
if (cell.qState === 'XS') {
clazzArr.push(classes.XS);
clazzArr.push(showGray ? classes.XS : classes.S);
} else if (cell.qState === 'S' || cell.qState === 'L') {
clazzArr.push(classes.S);
} else if (isAlternative(cell)) {
} else if (showGray && isAlternative(cell)) {
clazzArr.push(classes.A);
} else if (isExcluded(cell)) {
} else if (showGray && isExcluded(cell)) {
clazzArr.push(classes.X);
}
}
@@ -248,7 +249,7 @@ function RowColumn({ index, style, data, column = false }) {
classes.labelText,
highlighted && classes.highlighted,
dense && classes.labelDense,
excludedOrAlternative() && classes.excludedTextWithCheckbox,
showGray && excludedOrAlternative() && classes.excludedTextWithCheckbox,
])}
color={color}
>
@@ -271,6 +272,7 @@ function RowColumn({ index, style, data, column = false }) {
dense={dense}
excluded={isExcluded(cell)}
alternative={isAlternative(cell)}
showGray={showGray}
/>
);
const rb = <ListBoxRadioButton label={lbl} checked={isSelected} dense={dense} />;
@@ -384,7 +386,7 @@ function RowColumn({ index, style, data, column = false }) {
className={joinClassNames([
dense && classes.labelDense,
classes.labelText,
excludedOrAlternative() && classes.excludedTextWithCheckbox,
showGray && excludedOrAlternative() && classes.excludedTextWithCheckbox,
])}
>
{getFrequencyText()}

View File

@@ -63,10 +63,24 @@ describe('<ListBoxCheckbox />', () => {
expect(cb.props.icon.props.children.props.className).to.equal('cbIconAlternative');
});
it('should not render checkbox filled with alternative gray when showGray is false', async () => {
const testRenderer = await render(<ListBoxCheckbox alternative showGray={false} label="filled with gray" />);
const cb = testRenderer.root.findByType(Checkbox);
expect(cb.props.className).to.equal('checkbox');
expect(cb.props.icon.props.children.props.className).to.equal('');
});
it('should render checkbox filled with excluded gray', async () => {
const testRenderer = await render(<ListBoxCheckbox excluded label="filled with gray" />);
const cb = testRenderer.root.findByType(Checkbox);
expect(cb.props.className).to.equal('checkbox');
expect(cb.props.icon.props.children.props.className).to.equal('cbIconExcluded');
});
it('should not render checkbox filled with excluded gray when showGray is false', async () => {
const testRenderer = await render(<ListBoxCheckbox excluded showGray={false} label="filled with gray" />);
const cb = testRenderer.root.findByType(Checkbox);
expect(cb.props.className).to.equal('checkbox');
expect(cb.props.icon.props.children.props.className).to.equal('');
});
});

View File

@@ -96,7 +96,7 @@ describe('<ListboxInline />', () => {
[require.resolve('../../../hooks/useSessionModel'), () => useSessionModel],
[require.resolve('../../../hooks/useLayout'), () => () => [layout]],
[require.resolve('../../ActionsToolbar'), () => ActionsToolbar],
[require.resolve('../ListBox'), () => <div className="TheListBox" />],
[require.resolve('../ListBox'), () => <div className="theListBox" />],
[require.resolve('../ListBoxSearch'), () => ListBoxSearch],
[
require.resolve('../listbox-keyboard-navigation'),
@@ -134,6 +134,7 @@ describe('<ListboxInline />', () => {
sessionModel: undefined,
selectionsApi: undefined,
update: undefined,
fetchStart: 'fetchStart',
};
theme.spacing.returns('padding');

View File

@@ -300,6 +300,44 @@ describe('<ListBoxRowColumn />', () => {
await testRenderer.unmount();
});
it('should not add alternative class for A when showGray is false', async () => {
const index = 0;
const style = {};
const data = {
onMouseDown: sandbox.spy(),
onMouseUp: sandbox.spy(),
onMouseEnter: sandbox.spy(),
onClick: sandbox.spy(),
keyboard,
actions,
showGray: false,
pages: [
{
qArea: {
qLeft: 0,
qTop: 0,
qWidth: 0,
qHeight: 100,
},
qMatrix: [
[
{
qState: 'A',
},
],
],
},
],
};
const testRenderer = await render(
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
);
const testInstance = testRenderer.root;
const type = testInstance.findByType(Grid);
expect(type.props.className).not.to.include('alternative');
await testRenderer.unmount();
});
it('should set excluded - qState X', async () => {
const index = 0;
const style = {};
@@ -337,6 +375,44 @@ describe('<ListBoxRowColumn />', () => {
await testRenderer.unmount();
});
it('should not add excluded class for qState X when showGray is false', async () => {
const index = 0;
const style = {};
const data = {
onMouseDown: sandbox.spy(),
onMouseUp: sandbox.spy(),
onMouseEnter: sandbox.spy(),
onClick: sandbox.spy(),
keyboard,
actions,
showGray: false,
pages: [
{
qArea: {
qLeft: 0,
qTop: 0,
qWidth: 0,
qHeight: 100,
},
qMatrix: [
[
{
qState: 'X',
},
],
],
},
],
};
const testRenderer = await render(
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
);
const testInstance = testRenderer.root;
const type = testInstance.findByType(Grid);
expect(type.props.className).not.to.include('excluded');
await testRenderer.unmount();
});
it('should set excluded-selected - qState XS', async () => {
const index = 0;
const style = {};
@@ -374,6 +450,44 @@ describe('<ListBoxRowColumn />', () => {
await testRenderer.unmount();
});
it('should not add excluded-selected class when showGray is false', async () => {
const index = 0;
const style = {};
const data = {
onMouseDown: sandbox.spy(),
onMouseUp: sandbox.spy(),
onMouseEnter: sandbox.spy(),
onClick: sandbox.spy(),
keyboard,
actions,
showGray: false,
pages: [
{
qArea: {
qLeft: 0,
qTop: 0,
qWidth: 0,
qHeight: 100,
},
qMatrix: [
[
{
qState: 'XS',
},
],
],
},
],
};
const testRenderer = await render(
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
);
const testInstance = testRenderer.root;
const type = testInstance.findByType(Grid);
expect(type.props.className).not.to.include('excluded-selected');
await testRenderer.unmount();
});
it('should set excluded - qState XL', async () => {
const index = 0;
const style = {};

View File

@@ -13,11 +13,16 @@ describe('<Listbox />', () => {
let pages;
let selectDisabled;
let FixedSizeList;
let fetchStart;
let useCallbackStub;
let setTimeoutStub;
let useSelectionsInteractions;
before(() => {
sandbox = sinon.createSandbox({ useFakeTimers: true });
setTimeoutStub = sandbox.stub();
global.document = 'document';
layout = {
@@ -31,6 +36,9 @@ describe('<Listbox />', () => {
useSelectionsInteractions = sandbox.stub();
fetchStart = sandbox.stub();
useCallbackStub = sandbox.stub();
FixedSizeList = sandbox.stub().callsFake((funcArgs) => {
const { children } = funcArgs;
const RowOrColumn = children;
@@ -41,6 +49,13 @@ describe('<Listbox />', () => {
[
[require.resolve('react-window'), () => ({ FixedSizeList })],
[require.resolve('../../../hooks/useLayout'), () => () => [layout]],
[
require.resolve('react'),
() => ({
...React,
useCallback: useCallbackStub,
}),
],
[require.resolve('../useSelectionsInteractions'), () => useSelectionsInteractions],
[
require.resolve('react-window-infinite-loader'),
@@ -64,6 +79,8 @@ describe('<Listbox />', () => {
beforeEach(() => {
pages = [{ qArea: { qTop: 1, qHeight: 100 } }];
global.window = { setTimeout: setTimeoutStub };
selectDisabled = () => false;
useSelectionsInteractions.returns({
@@ -89,6 +106,7 @@ describe('<Listbox />', () => {
rangeSelect: false,
checkboxes: false,
selectDisabled,
fetchStart,
};
});
@@ -102,18 +120,21 @@ describe('<Listbox />', () => {
describe('Check rendering with different options', () => {
before(() => {
render = async () => {
render = async (overrides = {}) => {
const mergedArgs = { ...args, ...overrides };
await act(async () => {
renderer = create(
<ListBox
selections={args.selections}
direction={args.direction}
height={args.height}
width={args.width}
rangeSelect={args.rangeSelect}
listLayout={args.listLayout}
update={args.update}
checkboxes={args.checkboxes}
selections={mergedArgs.selections}
direction={mergedArgs.direction}
height={mergedArgs.height}
width={mergedArgs.width}
rangeSelect={mergedArgs.rangeSelect}
listLayout={mergedArgs.listLayout}
update={mergedArgs.update}
checkboxes={mergedArgs.checkboxes}
selectDisabled={mergedArgs.selectDisabled}
fetchStart={mergedArgs.fetchStart}
/>
);
});
@@ -174,6 +195,26 @@ describe('<Listbox />', () => {
});
});
it('should not call fetchStart unless fetching data', async () => {
sandbox
.stub(React, 'useRef')
.onFirstCall()
.returns({
loaderRef: {
current: {
_listRef: { state: { isScrolling: false } },
},
},
})
.callsFake((inp) => ({ current: inp }));
await render();
expect(fetchStart).not.called;
expect(setTimeoutStub).not.called;
const loadMoreItems = useCallbackStub.args[1][0];
expect(loadMoreItems).to.be.a('function');
});
it('should call with checkboxes true', async () => {
args.checkboxes = true;
await render();

View File

@@ -344,6 +344,10 @@ function nuked(configuration = {}) {
* @typedef { boolean | 'toggle' } SearchMode
*/
/**
* @typedef {function(promise)} PromiseFunction A callback function which receives a request promise as the first argument.
*/
/**
* @typedef {function(function)} ReceiverFunction A callback function which receives another function as input.
*/
@@ -372,10 +376,12 @@ function nuked(configuration = {}) {
* @param {boolean=} [options.rangeSelect=true] Enable range selection
* @param {boolean=} [options.dense=false] Reduces padding and text size
* @param {boolean=} [options.stateName="$"] Sets the state to make selections in
* @param {boolean=} [options.showGray=true] Render fields or checkboxes in shades of gray instead of white when their state is excluded or alternative.
* @param {object=} [options.properties={}] Properties object to extend default properties with
* @param {object} [options.sessionModel] Use a custom sessionModel.
* @param {object} [options.selectionsApi] Use a custom selectionsApi to customize how values are selected.
* @param {function():boolean} [options.selectDisabled=] Define a function which tells when selections are disabled (true) or enabled (false). By default, always returns false.
* @param {PromiseFunction} [options.fetchStart] A function called when the Listbox starts fetching data. Receives the fetch request promise as an argument.
* @param {ReceiverFunction} [options.update] A function which receives an update function which upon call will trigger a data fetch.
* @since 1.1.0
* @instance

View File

@@ -803,6 +803,18 @@
]
}
},
"PromiseFunction": {
"description": "A callback function which receives a request promise as the first argument.",
"kind": "alias",
"items": {
"kind": "function",
"params": [
{
"type": "promise"
}
]
}
},
"ReceiverFunction": {
"description": "A callback function which receives another function as input.",
"kind": "alias",
@@ -914,6 +926,12 @@
"defaultValue": false,
"type": "boolean"
},
"showGray": {
"description": "Render fields or checkboxes in shades of gray instead of white when their state is excluded or alternative.",
"optional": true,
"defaultValue": true,
"type": "boolean"
},
"properties": {
"description": "Properties object to extend default properties with",
"optional": true,
@@ -939,6 +957,11 @@
"type": "boolean"
}
},
"fetchStart": {
"description": "A function called when the Listbox starts fetching data. Receives the fetch request promise as an argument.",
"optional": true,
"type": "#/definitions/PromiseFunction"
},
"update": {
"description": "A function which receives an update function which upon call will trigger a data fetch.",
"optional": true,

View File

@@ -239,6 +239,11 @@ declare namespace stardust {
type SearchMode = boolean | "toggle";
/**
* A callback function which receives a request promise as the first argument.
*/
type PromiseFunction = ($: promise)=>void;
/**
* A callback function which receives another function as input.
*/
@@ -265,6 +270,7 @@ declare namespace stardust {
rangeSelect?: boolean;
dense?: boolean;
stateName?: boolean;
showGray?: boolean;
properties?: object;
sessionModel?: object;
selectionsApi?: object;
@@ -272,6 +278,7 @@ declare namespace stardust {
* Define a function which tells when selections are disabled (true) or enabled (false). By default, always returns false.
*/
"selectDisabled="?(): boolean;
fetchStart?: stardust.PromiseFunction;
update?: stardust.ReceiverFunction;
}): void;