Skip to content

Commit 1ef09af

Browse files
committed
feat(SortableTable): Adds gradient shadow on scroll
1 parent 25fb8db commit 1ef09af

File tree

4 files changed

+172
-3
lines changed

4 files changed

+172
-3
lines changed

src/components/Table/SortableTable.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface SortableTableProps<T> extends Omit<TableProps, 'children'> {
3030
rowExpanded?: (row: T) => React.ReactNode | boolean;
3131
rowOnClick?: (row: T, evt: React.MouseEvent) => void;
3232
truncate?: boolean;
33+
showScrollShadows?: boolean;
3334
}
3435

3536
declare class SortableTable<T> extends React.Component<SortableTableProps<T>, {}> {}

src/components/Table/SortableTable.js

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,46 @@ function getExpandableCell(row, expanded, onExpand) {
106106
}
107107

108108
class SortableTable extends React.Component {
109+
constructor(props) {
110+
super(props);
111+
this.state = {
112+
leftGradient: false,
113+
rightGradient: false,
114+
};
115+
this.scrollContainerRef = React.createRef();
116+
}
117+
118+
checkScroll = () => {
119+
const el = this.scrollContainerRef.current;
120+
if (!el) {
121+
return;
122+
}
123+
124+
const left = el.scrollLeft > 0;
125+
const right = el.scrollLeft < el.scrollWidth - el.clientWidth - 1;
126+
127+
this.setState({ leftGradient: left, rightGradient: right });
128+
};
129+
130+
componentDidMount() {
131+
if (this.props.showScrollShadows) {
132+
this.checkScroll();
133+
const el = this.scrollContainerRef.current;
134+
if (el) {
135+
el.addEventListener('scroll', this.checkScroll);
136+
window.addEventListener('resize', this.checkScroll);
137+
}
138+
}
139+
}
140+
141+
componentWillUnmount() {
142+
const el = this.scrollContainerRef.current;
143+
if (el) {
144+
el.removeEventListener('scroll', this.checkScroll);
145+
window.removeEventListener('resize', this.checkScroll);
146+
}
147+
}
148+
109149
static propTypes = {
110150
...Table.propTypes,
111151
columns: PropTypes.arrayOf(
@@ -143,6 +183,7 @@ class SortableTable extends React.Component {
143183
allSelected: PropTypes.bool,
144184
truncate: PropTypes.bool,
145185
renderRow: PropTypes.func,
186+
showScrollShadows: PropTypes.bool,
146187
// TODO? support sort type icons (FontAwesome has numeric, A->Z, Z->A)
147188
};
148189

@@ -154,6 +195,7 @@ class SortableTable extends React.Component {
154195
rowExpanded: () => false,
155196
truncate: false,
156197
renderRow: defaultRenderRow,
198+
showScrollShadows: false,
157199
};
158200

159201
render() {
@@ -174,6 +216,7 @@ class SortableTable extends React.Component {
174216
onExpand,
175217
rowExpanded,
176218
renderRow,
219+
showScrollShadows,
177220
...props
178221
} = this.props;
179222
const selectable = rowSelected;
@@ -222,7 +265,7 @@ class SortableTable extends React.Component {
222265
});
223266
}
224267

225-
return (
268+
const tableContent = (
226269
<Table style={tableStyle} {...props}>
227270
{showColgroup && (
228271
<colgroup>
@@ -278,6 +321,57 @@ class SortableTable extends React.Component {
278321
)}
279322
</Table>
280323
);
324+
325+
if (!showScrollShadows) {
326+
return tableContent;
327+
}
328+
329+
return (
330+
<div style={{ position: 'relative', width: '100%' }}>
331+
<div
332+
ref={this.scrollContainerRef}
333+
style={{
334+
overflowX: 'auto',
335+
width: '100%',
336+
maxWidth: '100%',
337+
}}
338+
>
339+
{tableContent}
340+
</div>
341+
342+
{/* Left gradient */}
343+
<div
344+
style={{
345+
position: 'absolute',
346+
left: 0,
347+
top: 0,
348+
width: '15px',
349+
height: '100%',
350+
background: 'linear-gradient(to right, #a0a0a0, transparent)',
351+
opacity: this.state.leftGradient ? 1 : 0,
352+
pointerEvents: 'none',
353+
zIndex: 1,
354+
transition: 'opacity 0.3s',
355+
}}
356+
/>
357+
358+
{/* Right gradient */}
359+
<div
360+
style={{
361+
position: 'absolute',
362+
right: 0,
363+
top: 0,
364+
width: '15px',
365+
height: '100%',
366+
background: 'linear-gradient(to left, #a0a0a0, transparent)',
367+
opacity: this.state.rightGradient ? 1 : 0,
368+
pointerEvents: 'none',
369+
zIndex: 1,
370+
transition: 'opacity 0.3s',
371+
}}
372+
/>
373+
</div>
374+
);
281375
}
282376
}
283377
export default SortableTable;

src/components/Table/SortableTable.spec.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,20 @@ describe('<SortableTable />', () => {
1212

1313
it('should accept all normal Table props', () => {
1414
const wrapper = mount(
15-
<SortableTable size="lg" bordered striped dark hover responsive columns={[]} rows={[]} />
15+
<SortableTable
16+
size="lg"
17+
bordered
18+
striped
19+
dark
20+
hover
21+
showScrollShadows
22+
columns={[]}
23+
rows={[]}
24+
/>
1625
);
1726
const table = wrapper.find('table');
1827

19-
assert(wrapper.render().hasClass('table-responsive'), 'responsive missing');
28+
// Note: showScrollShadows doesn't add a CSS class, it adds wrapper divs
2029
assert(table.hasClass('table-bordered'), 'bordered missing');
2130
assert(table.hasClass('table-striped'), 'striped missing');
2231
assert(table.hasClass('table-dark'), 'dark missing');
@@ -329,6 +338,70 @@ describe('<SortableTable />', () => {
329338
assert.equal(wrapper.find('tfoot td.whatever').length, 1, 'tfoot td.whatever incorrect');
330339
});
331340

341+
describe('Gradient shadow on scroll', () => {
342+
it('should render the gradient shadow when scrolling when showScrollShadows is true', () => {
343+
const columns = [{ header: 'Name', cell: (row) => row.name, key: 'name' }];
344+
const rows = [{ name: 'Test', key: '1' }];
345+
const wrapper = mount(<SortableTable columns={columns} rows={rows} showScrollShadows />);
346+
347+
assert(wrapper.find('table').exists());
348+
assert(wrapper.find('div').length > 1);
349+
});
350+
351+
it('should hide/show gradients based on scroll position', () => {
352+
const columns = [{ header: 'Name', cell: (row) => row.name, key: 'name' }];
353+
const rows = [{ name: 'Test', key: '1' }];
354+
const wrapper = mount(<SortableTable columns={columns} rows={rows} showScrollShadows />);
355+
const instance = wrapper.instance();
356+
357+
// Mock the scroll container element
358+
const mockElement = {
359+
scrollLeft: 0,
360+
scrollWidth: 1000,
361+
clientWidth: 500,
362+
};
363+
instance.scrollContainerRef.current = mockElement;
364+
365+
// Test initial state (no scroll)
366+
instance.checkScroll();
367+
assert.equal(instance.state.leftGradient, false, 'Left gradient should be hidden initially');
368+
assert.equal(
369+
instance.state.rightGradient,
370+
true,
371+
'Right gradient should be visible initially'
372+
);
373+
374+
// Test scrolled right
375+
mockElement.scrollLeft = 100;
376+
instance.checkScroll();
377+
assert.equal(
378+
instance.state.leftGradient,
379+
true,
380+
'Left gradient should be visible when scrolled right'
381+
);
382+
assert.equal(instance.state.rightGradient, true, 'Right gradient should still be visible');
383+
384+
// Test fully scrolled to end
385+
mockElement.scrollLeft = 500;
386+
instance.checkScroll();
387+
assert.equal(
388+
instance.state.leftGradient,
389+
true,
390+
'Left gradient should be visible when fully scrolled'
391+
);
392+
assert.equal(
393+
instance.state.rightGradient,
394+
false,
395+
'Right gradient should be hidden when fully scrolled'
396+
);
397+
});
398+
399+
it('should not render the gradient shadow when scrolling when showScrollShadows is false', () => {
400+
const wrapper = mount(<SortableTable columns={[]} rows={[]} showScrollShadows={false} />);
401+
assert(!wrapper.find('div[style*="position: relative"]').exists());
402+
});
403+
});
404+
332405
describe('Expandable column', () => {
333406
const columns = [
334407
{ header: 'Default', cell: () => '-', footer: '-' },

src/components/Table/SortableTable.stories.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ SortableTableExample.args = {
157157
bordered: false,
158158
hover: true,
159159
responsive: true,
160+
showScrollShadows: true,
160161
size: 'sm',
161162
striped: true,
162163
truncate: false,

0 commit comments

Comments
 (0)