Skip to content

Commit d70e490

Browse files
authored
Merge pull request #473 from performant-software/feature/rc470_record_detail_panel
RC #470 - Record detail panel
2 parents b98fe0e + 17aaaa9 commit d70e490

File tree

4 files changed

+267
-131
lines changed

4 files changed

+267
-131
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// @flow
2+
3+
import clsx from 'clsx';
4+
import React, {
5+
type Node,
6+
useCallback,
7+
useEffect,
8+
useMemo,
9+
useRef,
10+
useState
11+
} from 'react';
12+
import _ from 'underscore';
13+
import Button from './Button';
14+
import i18n from '../i18n/i18n';
15+
import RecordDetailItem from './RecordDetailItem';
16+
17+
type Item = {
18+
className?: string,
19+
icon?: string,
20+
text: string
21+
};
22+
23+
type Props = {
24+
children?: Node,
25+
classNames?: {
26+
root?: string,
27+
items?: string
28+
},
29+
items: Array<Item>
30+
};
31+
32+
const RecordDetailContent = (props: Props) => {
33+
const [expanded, setExpanded] = useState(false);
34+
const [showMore, setShowMore] = useState(false);
35+
const [contentHeight, setContentHeight] = useState(0);
36+
const [containerHeight, setContainerHeight] = useState(0);
37+
38+
const content = useRef(null);
39+
40+
/**
41+
* Sets the content and container heights on the state.
42+
*
43+
* @type {ResizeObserver}
44+
*/
45+
const observer = useMemo(() => new ResizeObserver((entries) => {
46+
const { target } = entries[0];
47+
48+
setContentHeight(target.scrollHeight);
49+
setContainerHeight(target.clientHeight);
50+
}), []);
51+
52+
/**
53+
* Sets the resize ref when the observer changes.
54+
*
55+
* @type {(function(*): void)|*}
56+
*/
57+
const sizeRef = useCallback((node) => {
58+
if (node) {
59+
content.current = node;
60+
observer.observe(node);
61+
} else {
62+
content.current = null;
63+
}
64+
}, [observer]);
65+
66+
/**
67+
* Toggles the "Show More" button based on the content and container heights.
68+
*/
69+
useEffect(() => {
70+
if (content.current) {
71+
setShowMore(contentHeight > containerHeight || expanded);
72+
}
73+
}, [contentHeight, containerHeight]);
74+
75+
/**
76+
* Disconnects the observer.
77+
*/
78+
useEffect(() => () => observer.disconnect(), []);
79+
80+
return (
81+
<div
82+
className={props.classNames?.root}
83+
>
84+
<div
85+
className={clsx(
86+
'flex',
87+
'flex-col',
88+
'relative',
89+
{ 'max-h-[250px]': !expanded }
90+
)}
91+
>
92+
<div
93+
className='overflow-hidden'
94+
ref={sizeRef}
95+
>
96+
<ul
97+
className={props.classNames?.items}
98+
>
99+
{ _.map(props.items, (item, idx) => (
100+
<RecordDetailItem
101+
className={item.className}
102+
key={idx}
103+
icon={item.icon}
104+
text={item.text}
105+
/>
106+
))}
107+
</ul>
108+
{ props.children }
109+
</div>
110+
{ showMore && !expanded && (
111+
<div
112+
className='absolute left-0 bottom-0 w-full h-[50px] bg-gradient-to-b from-white/50 to-white/100'
113+
/>
114+
)}
115+
</div>
116+
{ showMore && (
117+
<Button
118+
className='w-full justify-center mb-4'
119+
onClick={() => setExpanded((current) => (!current))}
120+
rounded
121+
>
122+
{ expanded
123+
? i18n.t('RecordDetailHeader.showLess')
124+
: i18n.t('RecordDetailHeader.showMore') }
125+
</Button>
126+
)}
127+
</div>
128+
);
129+
};
130+
131+
export default RecordDetailContent;
Lines changed: 28 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
11
// @flow
22

33
import clsx from 'clsx';
4-
import React, {
5-
useState, useRef, useEffect, useMemo
6-
} from 'react';
7-
import _ from 'underscore';
4+
import React from 'react';
85
import Button from './Button';
96
import RecordDetailTitle from './RecordDetailTitle';
10-
import RecordDetailItem from './RecordDetailItem';
117
import i18n from '../i18n/i18n';
128

139
type Props = {
14-
/**
15-
* Content to be rendered as the blurb
16-
*/
17-
children?: Node,
18-
1910
/**
2011
* Class names to apply to the main div, the title element, and the list element containing the detail items.
2112
*/
2213
classNames?: { items?: string, root?: string, title?: string },
2314

24-
/**
25-
* List of detail fields to be rendered above the blurb
26-
*/
27-
detailItems?: Array<{ text: string, icon?: string, className?: string }>,
28-
2915
/**
3016
* If a URL for a record detail page is provided, will render a button that links to it
3117
*/
@@ -42,96 +28,36 @@ type Props = {
4228
title: string,
4329
};
4430

45-
const RecordDetailHeader = (props: Props) => {
46-
const [expanded, setExpanded] = useState(false);
47-
const [showMore, setShowMore] = useState(false);
48-
const [contentHeight, setContentHeight] = useState(0);
49-
const [containerHeight, setContainerHeight] = useState(0);
50-
const content = useRef(null);
51-
52-
const observer = useMemo(
53-
() => new ResizeObserver((entries) => {
54-
setContentHeight(entries[0].target.scrollHeight);
55-
setContainerHeight(entries[0].target.clientHeight);
56-
}),
57-
[]
58-
);
59-
60-
const sizeRef = React.useCallback(
61-
(node) => {
62-
if (node) {
63-
content.current = node;
64-
observer.observe(node);
65-
} else {
66-
content.current = null;
67-
}
68-
},
69-
[observer]
70-
);
71-
72-
useEffect(() => {
73-
if (content.current) {
74-
setShowMore(contentHeight > containerHeight || expanded);
75-
}
76-
}, [contentHeight, containerHeight]);
77-
78-
useEffect(() => () => observer.disconnect(), []);
79-
80-
return (
81-
<div
82-
className={clsx(
83-
'flex',
84-
'flex-col',
85-
'gap-4',
86-
'px-6',
87-
'pt-6',
88-
'pb-4',
89-
props.classNames?.root
90-
)}
91-
>
92-
<RecordDetailTitle
93-
text={props.title}
94-
icon={props.icon}
95-
className={props.classNames?.title}
96-
/>
97-
{
98-
!!props.detailItems?.length && (
99-
<ul className={props.classNames?.items}>
100-
{
101-
_.map(props.detailItems, (item, idx) => (
102-
<RecordDetailItem
103-
text={item.text}
104-
icon={item.icon}
105-
className={item.className}
106-
key={idx}
107-
/>
108-
))
109-
}
110-
</ul>
111-
)
112-
}
113-
<div
114-
ref={sizeRef}
115-
className={clsx(
116-
{ 'line-clamp-6': !expanded }
117-
)}
31+
const RecordDetailHeader = (props: Props) => (
32+
<div
33+
className={clsx(
34+
'flex',
35+
'flex-col',
36+
'gap-4',
37+
'px-6',
38+
'pt-6',
39+
'pb-4',
40+
props.classNames?.root
41+
)}
42+
>
43+
<RecordDetailTitle
44+
text={props.title}
45+
icon={props.icon}
46+
className={props.classNames?.title}
47+
/>
48+
{ props.detailPageUrl && (
49+
<a
50+
href={props.detailPageUrl}
11851
>
119-
{ props.children }
120-
</div>
121-
{ showMore && (
122-
<Button rounded className='w-full justify-center' onClick={() => { setExpanded((current) => (!current)); }}>
123-
{ expanded ? i18n.t('RecordDetailHeader.showLess') : i18n.t('RecordDetailHeader.showMore') }
124-
</Button>
125-
)}
126-
{ props.detailPageUrl && (
127-
<a href={props.detailPageUrl}>
128-
<Button rounded className='w-full justify-center'>
52+
<Button
53+
className='w-full justify-center'
54+
rounded
55+
>
12956
{ i18n.t('RecordDetailHeader.viewDetails') }
13057
</Button>
13158
</a>
132-
)}
133-
</div>
134-
);
135-
};
59+
)}
60+
</div>
61+
);
13662

13763
export default RecordDetailHeader;

packages/core-data/src/components/RecordDetailPanel.js

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import clsx from 'clsx';
44
import React from 'react';
5-
import Icon from './Icon';
65
import AccordionItemsList from './AccordionItemsList';
6+
import Icon from './Icon';
7+
import LoadAnimation from './LoadAnimation';
8+
import RecordDetailBreadcrumbs from './RecordDetailBreadcrumbs';
9+
import RecordDetailContent from './RecordDetailContent';
710
import RecordDetailHeader from './RecordDetailHeader';
811
import type { RelatedRecordsList } from '../types/RelatedRecordsList';
9-
import RecordDetailBreadcrumbs from './RecordDetailBreadcrumbs';
10-
import LoadAnimation from './LoadAnimation';
1112

1213
type Props = {
1314
/**
@@ -89,7 +90,14 @@ const RecordDetailPanel = (props: Props) => (
8990
)}
9091
>
9192
{ props.onClose && (
92-
<div onClick={props.onClose} onKeyDown={props.onClose} tabIndex='0' role='button' aria-label='Close' className='absolute top-6 right-6 z-10 cursor-pointer'>
93+
<div
94+
aria-label='Close'
95+
className='absolute top-6 right-6 z-10 cursor-pointer'
96+
onClick={props.onClose}
97+
onKeyDown={props.onClose}
98+
role='button'
99+
tabIndex='0'
100+
>
93101
<Icon
94102
name='close'
95103
size={24}
@@ -106,28 +114,35 @@ const RecordDetailPanel = (props: Props) => (
106114
<RecordDetailHeader
107115
title={props.title}
108116
icon={props.icon}
109-
classNames={
110-
{
111-
root: clsx({ '!pt-16': props.breadcrumbs || props.onGoBack }, props.classNames?.header),
112-
title: clsx(props.classNames?.title, { 'pr-6': props.onClose }), // make sure there's space for the close icon
113-
items: props.classNames?.items
114-
}
115-
}
116-
detailItems={props.detailItems}
117+
classNames={{
118+
root: clsx({ '!pt-16': props.breadcrumbs || props.onGoBack }, props.classNames?.header),
119+
title: clsx(props.classNames?.title, { 'pr-6': props.onClose }), // make sure there's space for the close icon
120+
items: props.classNames?.items
121+
}}
117122
detailPageUrl={props.detailPageUrl}
118-
>
119-
{ props.children }
120-
</RecordDetailHeader>
123+
/>
121124
</div>
122-
{ props.loading
123-
? <div className='py-4 px-8'><LoadAnimation /></div>
124-
: (
125-
<AccordionItemsList
126-
className={clsx(props.classNames?.relatedRecords)}
127-
items={props.relations}
128-
count={props.count}
129-
/>
130-
) }
125+
<RecordDetailContent
126+
classNames={{
127+
root: 'py-4 px-8',
128+
items: props.classNames?.items
129+
}}
130+
items={props.detailItems}
131+
>
132+
{ props.children }
133+
</RecordDetailContent>
134+
{ props.loading && (
135+
<div className='py-4 px-8'>
136+
<LoadAnimation />
137+
</div>
138+
)}
139+
{ !props.loading && (
140+
<AccordionItemsList
141+
className={clsx(props.classNames?.relatedRecords)}
142+
items={props.relations}
143+
count={props.count}
144+
/>
145+
)}
131146
</div>
132147
);
133148

0 commit comments

Comments
 (0)